@cardinali-studio/adminkit 2.2.20 → 2.2.22
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 +38 -2
- package/dist/api.js +13 -0
- package/dist/app/App.d.ts +1 -0
- package/dist/app/App.js +36 -15
- package/dist/app/NextAdminApp.d.ts +8 -0
- package/dist/app/NextAdminApp.js +3 -0
- package/dist/app/lib/admin-interface.d.ts +11 -0
- package/dist/app/lib/admin-interface.js +45 -0
- package/dist/app/next.css +826 -0
- package/dist/app/routes/AdminInterfaceSettings.d.ts +12 -0
- package/dist/app/routes/AdminInterfaceSettings.js +14 -0
- package/dist/app/routes/Deliveries.js +1 -1
- package/dist/app/routes/GallerySectionsEditor.d.ts +1 -1
- package/dist/app/routes/GallerySectionsEditor.js +14 -2
- package/dist/app/routes/HomeEditor.js +2 -2
- package/dist/app/routes/Overview.js +4 -4
- package/dist/app/routes/PageEditor.js +12 -5
- package/dist/app/routes/PortfolioWorksEditor.d.ts +1 -1
- package/dist/app/routes/PortfolioWorksEditor.js +11 -3
- package/dist/app/routes/Requests.js +2 -2
- package/dist/app/routes/SiteSettingsEditor.d.ts +4 -1
- package/dist/app/routes/SiteSettingsEditor.js +3 -2
- package/dist/app/ui/MediaSlot.js +1 -1
- package/dist/app/ui/OpenCard.js +22 -1
- package/dist/app/ui/icons.js +5 -2
- package/dist/form/fields/index.js +2 -1
- package/dist/form/media/Lightbox.js +33 -3
- package/dist/form/media/MediaGrid.js +9 -6
- package/dist/form/media/media-model.d.ts +2 -0
- package/dist/form/media/media-model.js +8 -0
- package/dist/form/media/useMediaLibrary.d.ts +11 -2
- package/dist/form/media/useMediaLibrary.js +89 -24
- package/dist/functions/_shared/d1-media.js +105 -91
- package/dist/functions/_shared/media-alt.js +61 -0
- package/dist/functions/api/assets/delete.js +20 -3
- package/dist/functions/api/assets/media.js +57 -2
- package/dist/functions/api/assets/move.js +5 -1
- package/dist/functions/api/assets/upload.js +3 -1
- package/dist/functions/api/assets.js +5 -2
- package/dist/gallery.d.ts +2 -0
- package/dist/media.d.ts +19 -0
- package/dist/media.js +132 -0
- package/dist/normalize.d.ts +1 -0
- package/dist/normalize.js +2 -1
- package/dist/persistence.d.ts +8 -0
- package/dist/persistence.js +78 -6
- package/dist/theme.css +42 -0
- package/package.json +2 -2
|
@@ -23,14 +23,21 @@ export function Lightbox({ lib }) {
|
|
|
23
23
|
const i = lib.viewing;
|
|
24
24
|
const file = i === null ? undefined : lib.shown[i];
|
|
25
25
|
const [info, setInfo] = useState(false);
|
|
26
|
+
const [altOpen, setAltOpen] = useState(false);
|
|
27
|
+
const [altDraft, setAltDraft] = useState({ decorative: false, texts: {} });
|
|
28
|
+
const [altSaved, setAltSaved] = useState(false);
|
|
26
29
|
const [usage, setUsage] = useState(null);
|
|
27
30
|
const [usageError, setUsageError] = useState(false);
|
|
31
|
+
const altLocales = lib.altLocales ?? ["default"];
|
|
32
|
+
const defaultAltLocale = lib.defaultAltLocale ?? "default";
|
|
28
33
|
// Tastiera: Esc chiude, le frecce scorrono. Su un visore a schermo intero è il modo in cui
|
|
29
34
|
// ci si aspetta di muoversi, e senza si è costretti a mirare due bottoni piccoli.
|
|
30
35
|
useEffect(() => {
|
|
31
36
|
if (i === null)
|
|
32
37
|
return;
|
|
33
38
|
const on = (e) => {
|
|
39
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
|
|
40
|
+
return;
|
|
34
41
|
if (e.key === "Escape")
|
|
35
42
|
lib.view(null);
|
|
36
43
|
else if (e.key === "ArrowRight")
|
|
@@ -41,6 +48,10 @@ export function Lightbox({ lib }) {
|
|
|
41
48
|
addEventListener("keydown", on);
|
|
42
49
|
return () => removeEventListener("keydown", on);
|
|
43
50
|
}, [i, lib]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setAltDraft(file?.alt ?? { decorative: false, texts: {} });
|
|
53
|
+
setAltSaved(false);
|
|
54
|
+
}, [file?.key]);
|
|
44
55
|
// L'uso si richiede per la foto che si sta guardando, non una volta per tutte.
|
|
45
56
|
useEffect(() => {
|
|
46
57
|
setUsage(null);
|
|
@@ -62,9 +73,28 @@ export function Lightbox({ lib }) {
|
|
|
62
73
|
if (i === null || !file)
|
|
63
74
|
return null;
|
|
64
75
|
const folder = lib.prefix.replace(/\/$/, "") || "tutte le foto";
|
|
76
|
+
const lightboxAlt = file.alt?.decorative
|
|
77
|
+
? ""
|
|
78
|
+
: file.alt?.texts[defaultAltLocale] || file.name;
|
|
65
79
|
return (_jsxs("div", { className: "lb", onClick: (e) => { if (e.target === e.currentTarget)
|
|
66
|
-
lib.view(null); }, children: [_jsxs("header", { className: "lb-top", children: [_jsxs("button", { className: "lb-btn", onClick: () => lib.view(null), children: ["\u2039 Torna a ", folder] }), _jsxs("div", { className: "lb-id", children: [_jsx("span", { className: "lb-name", children: file.name }), _jsxs("span", { className: "lb-count", children: [i + 1, " / ", lib.shown.length] })] }), _jsxs("div", { className: "lb-tools", children: [_jsx("a", { className: "lb-btn", href: file.url, download: file.name, title: "Scarica", children: _jsx(IcoDown, {}) }), _jsx("button", { className: "lb-btn" + (info ? " is-on" : ""), onClick: () => setInfo((v) => !v), title: "Informazioni", children: _jsx(IcoInfo, {}) }), lib.mode === "manage" && (_jsx("button", { className: "lb-btn", onClick: () => void lib.remove([file.key]), title: "Cestina", children: _jsx(IcoTrash, {}) }))] })] }), _jsxs("div", { className: "lb-stage", onClick: (e) => { if (e.target === e.currentTarget)
|
|
67
|
-
lib.view(null); }, children: [_jsx("button", { className: "lb-nav lb-prev", disabled: i === 0, onClick: () => lib.step(-1), "aria-label": "Precedente", children: "\u2039" }), _jsx("img", { className: "lb-img", src: file.url, alt:
|
|
80
|
+
lib.view(null); }, children: [_jsxs("header", { className: "lb-top", children: [_jsxs("button", { className: "lb-btn", onClick: () => lib.view(null), children: ["\u2039 Torna a ", folder] }), _jsxs("div", { className: "lb-id", children: [_jsx("span", { className: "lb-name", children: file.name }), _jsxs("span", { className: "lb-count", children: [i + 1, " / ", lib.shown.length] })] }), _jsxs("div", { className: "lb-tools", children: [_jsx("a", { className: "lb-btn", href: file.url, download: file.name, title: "Scarica", children: _jsx(IcoDown, {}) }), lib.mode === "manage" && (file.type ?? "").startsWith("image/") && (_jsx("button", { className: "lb-btn lb-alt-button" + (altOpen ? " is-on" : ""), onClick: () => { setAltOpen((value) => !value); setInfo(false); lib.clearError(); }, title: "Testo alternativo", children: "ALT" })), _jsx("button", { className: "lb-btn" + (info ? " is-on" : ""), onClick: () => { setInfo((v) => !v); setAltOpen(false); }, title: "Informazioni", children: _jsx(IcoInfo, {}) }), lib.mode === "manage" && (_jsx("button", { className: "lb-btn", onClick: () => void lib.remove([file.key]), title: "Cestina", children: _jsx(IcoTrash, {}) }))] })] }), _jsxs("div", { className: "lb-stage", onClick: (e) => { if (e.target === e.currentTarget)
|
|
81
|
+
lib.view(null); }, children: [_jsx("button", { className: "lb-nav lb-prev", disabled: i === 0, onClick: () => lib.step(-1), "aria-label": "Precedente", children: "\u2039" }), _jsx("img", { className: "lb-img", src: file.url, alt: lightboxAlt }), _jsx("button", { className: "lb-nav lb-next", disabled: i === lib.shown.length - 1, onClick: () => lib.step(1), "aria-label": "Successiva", children: "\u203A" })] }), info && (_jsx("aside", { className: "lb-info", children: _jsxs("dl", { children: [_jsx("dt", { children: "File" }), _jsx("dd", { children: file.name }), _jsx("dt", { children: "Cartella" }), _jsx("dd", { children: lib.prefix || "(radice)" }), _jsx("dt", { children: "Dimensioni" }), _jsx("dd", { children: file.w && file.h ? `${file.w} × ${file.h} px` : "—" }), _jsx("dt", { children: "Peso" }), _jsx("dd", { children: fmtSize(file.size) }), _jsx("dt", { children: "Scatto" }), _jsx("dd", { children: fmtDate(file.capturedAt) }), _jsx("dt", { children: "Caricata" }), _jsx("dd", { children: fmtDate(file.uploaded) }), _jsx("dt", { children: "Usata in" }), _jsx("dd", { children: usageError
|
|
68
82
|
? "controllo non disponibile"
|
|
69
|
-
: usage === null ? "…" : usage.length ? usage.join(", ") : "nessuna sorgente" })] }) }))] }))
|
|
83
|
+
: usage === null ? "…" : usage.length ? usage.join(", ") : "nessuna sorgente" })] }) })), altOpen && lib.mode === "manage" && (_jsxs("aside", { className: "lb-alt", "aria-label": "Testo alternativo dell'immagine", children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("strong", { children: "Testo alternativo" }), _jsx("span", { children: "Predefinito per questo file" })] }), _jsx("button", { type: "button", onClick: () => setAltOpen(false), "aria-label": "Chiudi", children: "\u00D7" })] }), _jsxs("label", { className: "lb-alt-decorative", children: [_jsx("input", { type: "checkbox", checked: altDraft.decorative, disabled: lib.mutating, onChange: (event) => {
|
|
84
|
+
setAltSaved(false);
|
|
85
|
+
setAltDraft((current) => ({ ...current, decorative: event.target.checked }));
|
|
86
|
+
} }), _jsxs("span", { children: [_jsx("strong", { children: "Immagine decorativa" }), _jsxs("small", { children: ["Non aggiunge informazioni: il sito user\u00E0 ", _jsx("code", { children: "alt=\"\"" }), "."] })] })] }), !altDraft.decorative && (_jsx("div", { className: "lb-alt-fields", children: altLocales.map((locale) => {
|
|
87
|
+
const value = altDraft.texts[locale] ?? "";
|
|
88
|
+
return (_jsxs("label", { children: [_jsxs("span", { children: [locale === "default" ? "Testo" : `Testo · ${locale.toUpperCase()}`, _jsxs("small", { children: [value.length, " caratteri"] })] }), _jsx("textarea", { rows: 3, value: value, disabled: lib.mutating, placeholder: "Descrivi il significato dell\u2019immagine nel contesto della pagina", onChange: (event) => {
|
|
89
|
+
setAltSaved(false);
|
|
90
|
+
setAltDraft((current) => ({
|
|
91
|
+
...current,
|
|
92
|
+
texts: { ...current.texts, [locale]: event.target.value },
|
|
93
|
+
}));
|
|
94
|
+
} })] }, locale));
|
|
95
|
+
}) })), _jsx("p", { className: "lb-alt-guidance", children: "Scrivi una frase breve e naturale. Evita \u201Cimmagine di\u201D, ripetizioni e liste di parole chiave. Lo stesso file pu\u00F2 essere sovrascritto dal singolo contenuto quando il contesto lo richiede." }), lib.error && _jsx("p", { className: "lb-alt-error", children: lib.error }), altSaved && _jsx("p", { className: "lb-alt-saved", role: "status", children: "Testo alternativo salvato." }), _jsx("footer", { children: _jsx("button", { type: "button", className: "ak-btn ak-btn-primary", disabled: lib.mutating, onClick: async () => {
|
|
96
|
+
const saved = await lib.saveAlt(file.key, altDraft);
|
|
97
|
+
if (saved)
|
|
98
|
+
setAltSaved(true);
|
|
99
|
+
}, children: lib.operationState === "saving_alt" ? "Salvataggio…" : "Salva" }) })] }))] }));
|
|
70
100
|
}
|
|
@@ -23,14 +23,14 @@ import { SORT_LABELS } from "./useMediaLibrary.js";
|
|
|
23
23
|
import { DRAG_TYPE, DRAG_FOLDERS, readDragged, FolderMenu } from "./MediaFolders.js";
|
|
24
24
|
import { Lightbox } from "./Lightbox.js";
|
|
25
25
|
import { UploadDialog } from "./UploadDialog.js";
|
|
26
|
-
import { summarizeMediaSelection } from "./media-model.js";
|
|
26
|
+
import { mediaAltStatus, summarizeMediaSelection } from "./media-model.js";
|
|
27
27
|
const JOURNAL_LABELS = {
|
|
28
28
|
preparing: "Preparo le copie Media.",
|
|
29
|
-
ready: "Media pronta
|
|
29
|
+
ready: "Modifica Media pronta per la pubblicazione.",
|
|
30
30
|
blocked_in_use: "Operazione bloccata: una bozza o la versione online usa ancora la sorgente.",
|
|
31
31
|
bound: "Pubblicazione con Media in corso.",
|
|
32
32
|
finalizing: "Pubblicazione riuscita: completo la pulizia Media.",
|
|
33
|
-
completed_with_warning: "
|
|
33
|
+
completed_with_warning: "Modifica applicata con alcuni Media da completare.",
|
|
34
34
|
failed: "Operazione Media interrotta prima della pubblicazione.",
|
|
35
35
|
};
|
|
36
36
|
/* Scaricare più file significa più download: il browser ne consente una raffica breve dallo
|
|
@@ -121,6 +121,7 @@ export function MediaGrid({ lib, notice, onPick, onCancel }) {
|
|
|
121
121
|
const allOn = lib.allSelected;
|
|
122
122
|
const locked = lib.busy || lib.mutating;
|
|
123
123
|
const selection = summarizeMediaSelection(sel);
|
|
124
|
+
const altLocales = lib.altLocales ?? ["default"];
|
|
124
125
|
/* `dragDepth` e non un booleano: `dragenter`/`dragleave` scattano anche passando da un figlio
|
|
125
126
|
all'altro, quindi con un booleano la zona si spegne mentre ci si muove sopra. Si conta. */
|
|
126
127
|
const [dragDepth, setDragDepth] = useState(0);
|
|
@@ -152,9 +153,11 @@ export function MediaGrid({ lib, notice, onPick, onCancel }) {
|
|
|
152
153
|
}, children: [dragDepth > 0 && (_jsx("div", { className: "mg-dropzone", children: manage
|
|
153
154
|
? _jsxs("span", { children: ["Rilascia per caricare in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella" })] })
|
|
154
155
|
: _jsx("span", { children: "Rilascia le immagini \u2014 sceglierai la cartella prima del caricamento" }) })), _jsx("header", { className: "mg-bar", children: sel.size === 0 ? (_jsxs(_Fragment, { children: [(manage || lib.multiple) && (_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: false, disabled: locked || !lib.selectableIds.length, ref: (el) => { if (el)
|
|
155
|
-
el.indeterminate = false; }, onChange: () => lib.setAll(true) }), _jsx("span", { children: "Seleziona tutto" })] })), !manage && (_jsx("button", { className: "ak-btn ak-btn-quiet mg-upload", disabled: locked, onClick: () => lib.openUpload(), children: "Carica nuove foto" })), _jsx("span", { className: "ak-spacer" }), _jsxs("label", { className: "ak-hint", children: ["ordina per", " ", _jsx("select", { className: "mg-sort", value: lib.sortKey, disabled: locked, onChange: (e) => lib.setSort(e.target.value), children: Object.entries(SORT_LABELS).map(([k, l]) => _jsx("option", { value: k, children: l }, k)) }), _jsx("button", { className: "ak-icon", disabled: locked, onClick: lib.flipDir, title: "Inverti l'ordine", children: lib.sortDir === "asc" ? "↑" : "↓" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: allOn, disabled: locked, ref: (el) => { if (el)
|
|
156
|
-
el.indeterminate = lib.someSelected; }, onChange: () => lib.setAll(!allOn) }), _jsx("span", { children: selection.label })] }), !allOn && lib.selectableIds.length > 0 && (_jsxs("button", { className: "mg-act is-link", disabled: locked, onClick: () => lib.setAll(true), children: ["Seleziona tutti gli elementi (", lib.selectableIds.length, ")"] })), manage && (_jsxs(_Fragment, { children: [lib.selectedFiles.length > 0 && (_jsxs("button", { className: "mg-act", disabled: locked, onClick: () => download(lib.shown.filter((f) => sel.has(f.key)).map((f) => ({ url: f.url, name: f.name }))), children: ["Scarica ", lib.selectedFiles.length, " file"] })), _jsx(MoveMenu, { lib: lib, keys: [...sel] }), _jsx("button", { className: "mg-act is-danger", disabled: locked, onClick: () => void lib.remove([...sel]), children: "Elimina" })] })), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "mg-act", disabled: locked, onClick: () => lib.setAll(false), children: "Deseleziona" })] })) }), notice && _jsx("p", { className: "mg-picker-notice", children: notice }), lib.error && _jsx("p", { className: "ak-error", onClick: lib.clearError, children: lib.error }), manage && lib.mediaOperations.length > 0 && (_jsx("section", { className: "mg-journal", "aria-live": "polite", "aria-label": "Stato operazioni Media", children: lib.mediaOperations.map((operation) => (_jsxs("div", { className: `mg-journal-row is-${operation.state}`, children: [_jsx("span", { className: "mg-journal-badge", children: operation.type === "move" ? "Spostamento" : "Eliminazione" }), _jsx("span", { children: JOURNAL_LABELS[operation.state] ?? "Operazione Media in attesa." }), operation.failedEntries > 0 && _jsxs("span", { className: "ak-hint", children: [operation.failedEntries, " file da riprovare"] }), _jsx("span", { className: "ak-spacer" }), operation.state === "
|
|
156
|
+
el.indeterminate = false; }, onChange: () => lib.setAll(true) }), _jsx("span", { children: "Seleziona tutto" })] })), !manage && (_jsx("button", { className: "ak-btn ak-btn-quiet mg-upload", disabled: locked, onClick: () => lib.openUpload(), children: "Carica nuove foto" })), _jsx("span", { className: "ak-spacer" }), manage && (_jsxs("label", { className: "ak-hint", children: ["testo alt", " ", _jsxs("select", { className: "mg-sort", value: lib.altFilter, disabled: locked, onChange: (event) => lib.setAltFilter(event.target.value), children: [_jsx("option", { value: "all", children: "tutti" }), _jsx("option", { value: "missing", children: "da completare" }), _jsx("option", { value: "described", children: "completo" }), _jsx("option", { value: "decorative", children: "decorativo" })] })] })), _jsxs("label", { className: "ak-hint", children: ["ordina per", " ", _jsx("select", { className: "mg-sort", value: lib.sortKey, disabled: locked, onChange: (e) => lib.setSort(e.target.value), children: Object.entries(SORT_LABELS).map(([k, l]) => _jsx("option", { value: k, children: l }, k)) }), _jsx("button", { className: "ak-icon", disabled: locked, onClick: lib.flipDir, title: "Inverti l'ordine", children: lib.sortDir === "asc" ? "↑" : "↓" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: allOn, disabled: locked, ref: (el) => { if (el)
|
|
157
|
+
el.indeterminate = lib.someSelected; }, onChange: () => lib.setAll(!allOn) }), _jsx("span", { children: selection.label })] }), !allOn && lib.selectableIds.length > 0 && (_jsxs("button", { className: "mg-act is-link", disabled: locked, onClick: () => lib.setAll(true), children: ["Seleziona tutti gli elementi (", lib.selectableIds.length, ")"] })), manage && (_jsxs(_Fragment, { children: [lib.selectedFiles.length > 0 && (_jsxs("button", { className: "mg-act", disabled: locked, onClick: () => download(lib.shown.filter((f) => sel.has(f.key)).map((f) => ({ url: f.url, name: f.name }))), children: ["Scarica ", lib.selectedFiles.length, " file"] })), _jsx(MoveMenu, { lib: lib, keys: [...sel] }), _jsx("button", { className: "mg-act is-danger", disabled: locked, onClick: () => void lib.remove([...sel]), children: "Elimina" })] })), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "mg-act", disabled: locked, onClick: () => lib.setAll(false), children: "Deseleziona" })] })) }), notice && _jsx("p", { className: "mg-picker-notice", children: notice }), lib.error && _jsx("p", { className: "ak-error", onClick: lib.clearError, children: lib.error }), manage && lib.mediaOperations.length > 0 && (_jsx("section", { className: "mg-journal", "aria-live": "polite", "aria-label": "Stato operazioni Media", children: lib.mediaOperations.map((operation) => (_jsxs("div", { className: `mg-journal-row is-${operation.state}`, children: [_jsx("span", { className: "mg-journal-badge", children: operation.type === "move" ? "Spostamento" : "Eliminazione" }), _jsx("span", { children: JOURNAL_LABELS[operation.state] ?? "Operazione Media in attesa." }), operation.failedEntries > 0 && _jsxs("span", { className: "ak-hint", children: [operation.failedEntries, " file da riprovare"] }), _jsx("span", { className: "ak-spacer" }), operation.state === "ready" && !operation.releaseId && (_jsx("button", { className: "mg-act", disabled: locked, onClick: () => void lib.publishMedia(operation), children: "Riprova pubblicazione" })), operation.state === "completed_with_warning" && (_jsx("button", { className: "mg-act", disabled: locked, onClick: () => void lib.resumeMedia(operation), children: "Riprendi" })), ["preparing", "ready", "blocked_in_use", "failed"].includes(operation.state) && !operation.releaseId && (_jsx("button", { className: "mg-act is-danger", disabled: locked, onClick: () => void lib.cancelMedia(operation), children: "Annulla" }))] }, operation.operationId))) })), lib.progress && (_jsxs("p", { className: "mg-progress", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026 \u2014 non chiudere la pagina"] })), lib.pickUploadJob?.state === "running" && (_jsxs("p", { className: "mg-progress", children: ["Nuove immagini: preparo ", lib.pickUploadJob.conversionDone, "/", lib.pickUploadJob.total, ", carico ", lib.pickUploadJob.uploadDone, "/", lib.pickUploadJob.total, "\u2026"] })), lib.prefix !== "" && (manage || lib.subfolders.length > 0) && (_jsxs("section", { className: "mg-section", children: [_jsxs("div", { className: "mg-section-head", children: [_jsx("h2", { className: "mg-section-title", children: "Cartelle" }), manage && (_jsx("button", { className: "mg-act", disabled: locked, onClick: () => void lib.newFolder(lib.prefix), children: "+ Cartella" }))] }), lib.subfolders.length > 0 && (_jsx("div", { className: "mg", children: lib.subfolders.map((f) => (_jsx(FolderCell, { lib: lib, folder: f, manage: manage, onExternalDrop: () => setDragDepth(0) }, f.prefix))) }))] })), lib.prefix !== "" && (manage || lib.subfolders.length > 0) && lib.shown.length > 0 && (_jsx("h2", { className: "mg-section-title", children: "Foto" })), _jsx("div", { className: "mg", children: lib.shown.map((f, i) => {
|
|
157
158
|
const on = sel.has(f.key);
|
|
159
|
+
const isImage = (f.type ?? "").startsWith("image/");
|
|
160
|
+
const altStatus = isImage ? mediaAltStatus(f, altLocales) : null;
|
|
158
161
|
return (_jsxs("figure", { className: "mg-item" + (on ? " is-on" : ""), draggable: manage && !locked, onDragStart: (e) => {
|
|
159
162
|
if (locked) {
|
|
160
163
|
e.preventDefault();
|
|
@@ -173,7 +176,7 @@ export function MediaGrid({ lib, notice, onPick, onCancel }) {
|
|
|
173
176
|
(manage ? lib.view(i) : lib.toggle(f.key)); }, children: [_jsx("img", { className: "mg-img", src: f.url, alt: "", loading: "lazy",
|
|
174
177
|
/* Il rapporto è noto prima che l'immagine arrivi (lo dà l'elenco): la cella
|
|
175
178
|
riserva subito l'ingombro giusto e la griglia non salta mentre si carica. */
|
|
176
|
-
style: { aspectRatio: f.w && f.h ? `${f.w} / ${f.h}` : "3 / 2" } }), _jsx("button", { className: "mg-check", "aria-label": on ? "Deseleziona" : "Seleziona", disabled: locked, onClick: (e) => { e.stopPropagation(); lib.toggle(f.key); }, children: on ? "✓" : "" })] }), _jsx("figcaption", { className: "mg-cap", title: f.name, children: f.name })] }, f.key));
|
|
179
|
+
style: { aspectRatio: f.w && f.h ? `${f.w} / ${f.h}` : "3 / 2" } }), manage && altStatus && (_jsx("span", { className: `mg-alt-badge is-${altStatus}`, children: altStatus === "missing" ? "ALT da completare" : altStatus === "decorative" ? "Decorativa" : "ALT completo" })), _jsx("button", { className: "mg-check", "aria-label": on ? "Deseleziona" : "Seleziona", disabled: locked, onClick: (e) => { e.stopPropagation(); lib.toggle(f.key); }, children: on ? "✓" : "" })] }), _jsx("figcaption", { className: "mg-cap", title: f.name, children: f.name })] }, f.key));
|
|
177
180
|
}) }), !lib.busy && lib.shown.length === 0 && (_jsx("p", { className: "ak-hint", children: lib.prefix === "" && lib.folders.length
|
|
178
181
|
? "Nessuna foto fuori dalle cartelle. Scegline una qui a fianco."
|
|
179
182
|
: "Nessun file qui." })), lib.mode === "pick" && (_jsxs("footer", { className: "mg-foot", children: [_jsxs("span", { className: "ak-hint", children: [sel.size, " selezionate"] }), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "ak-btn ak-btn-quiet", disabled: locked, onClick: () => onCancel?.(), children: "Annulla" }), _jsxs("button", { className: "ak-btn ak-btn-primary", disabled: locked || !sel.size, onClick: () => onPick?.(lib.shown.filter((f) => sel.has(f.key))), children: ["Usa ", sel.size > 0 ? `(${sel.size})` : ""] })] }))] }), _jsx(Lightbox, { lib: lib }), _jsx(UploadDialog, { lib: lib })] }));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AssetFile, AssetFolder } from "../../api.js";
|
|
2
2
|
export type MediaSelectionKind = "file" | "folder";
|
|
3
|
+
export type MediaAltStatus = "missing" | "described" | "decorative";
|
|
4
|
+
export declare function mediaAltStatus(file: Pick<AssetFile, "alt">, locales: readonly string[]): MediaAltStatus;
|
|
3
5
|
export interface MediaSelectableItem {
|
|
4
6
|
id: string;
|
|
5
7
|
kind: MediaSelectionKind;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export function mediaAltStatus(file, locales) {
|
|
2
|
+
if (file.alt?.decorative)
|
|
3
|
+
return "decorative";
|
|
4
|
+
const texts = file.alt?.texts ?? {};
|
|
5
|
+
return locales.every((locale) => typeof texts[locale] === "string" && texts[locale].trim())
|
|
6
|
+
? "described"
|
|
7
|
+
: "missing";
|
|
8
|
+
}
|
|
1
9
|
/** L'unico dominio su cui "Seleziona tutto" e la relativa checkbox possono ragionare. */
|
|
2
10
|
export function mediaSelectableItems({ mode, prefix, shown, subfolders, }) {
|
|
3
11
|
const visibleFolders = mode === "manage" && prefix !== ""
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { api, type AssetFile, type AssetFolder } from "../../api.js";
|
|
1
|
+
import { api, type AssetAltText, 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
4
|
/** D1 registra l'intera cancellazione nel preflight: non esistono né il token Git né i
|
|
@@ -7,9 +7,12 @@ import { type MediaUploadJob } from "./media-upload-queue.js";
|
|
|
7
7
|
export declare function deleteMediaKeys(keys: string[], remove: typeof api.remove, onProgress?: (done: number) => void): Promise<{
|
|
8
8
|
done: number;
|
|
9
9
|
refsToken?: string;
|
|
10
|
+
operationId?: string;
|
|
11
|
+
requiresPublication?: boolean;
|
|
10
12
|
}>;
|
|
11
|
-
export type MediaOperationState = "idle" | "planning" | "awaiting_confirmation" | "moving" | "deleting" | "renaming" | "creating_folder" | "journal";
|
|
13
|
+
export type MediaOperationState = "idle" | "planning" | "awaiting_confirmation" | "moving" | "deleting" | "renaming" | "creating_folder" | "saving_alt" | "journal";
|
|
12
14
|
export type SortKey = "uploaded" | "capturedAt" | "name" | "size";
|
|
15
|
+
export type AltFilter = "all" | "missing" | "described" | "decorative";
|
|
13
16
|
export declare const SORT_LABELS: Record<SortKey, string>;
|
|
14
17
|
export interface MediaLibrary {
|
|
15
18
|
mode: "manage" | "pick";
|
|
@@ -39,6 +42,9 @@ export interface MediaLibrary {
|
|
|
39
42
|
} | null;
|
|
40
43
|
sortKey: SortKey;
|
|
41
44
|
sortDir: "asc" | "desc";
|
|
45
|
+
altFilter: AltFilter;
|
|
46
|
+
altLocales: readonly string[];
|
|
47
|
+
defaultAltLocale: string;
|
|
42
48
|
uploadDraft: {
|
|
43
49
|
files: File[];
|
|
44
50
|
destination: string | null;
|
|
@@ -55,6 +61,7 @@ export interface MediaLibrary {
|
|
|
55
61
|
setAll(on: boolean): void;
|
|
56
62
|
setSort(k: SortKey): void;
|
|
57
63
|
flipDir(): void;
|
|
64
|
+
setAltFilter(filter: AltFilter): void;
|
|
58
65
|
/** Accoda lo STESSO lavoro usato da Media. `destination` è esplicita nel picker; in gestione
|
|
59
66
|
* il ripiego resta la cartella aperta, che è il comportamento storico. */
|
|
60
67
|
upload(files: File[] | FileList, destination?: string): Promise<string | null>;
|
|
@@ -63,6 +70,7 @@ export interface MediaLibrary {
|
|
|
63
70
|
setUploadFiles(files: File[] | FileList): void;
|
|
64
71
|
setUploadDestination(destination: string | null): void;
|
|
65
72
|
setAutoWebp(on: boolean): Promise<void>;
|
|
73
|
+
saveAlt(key: string, alt: AssetAltText): Promise<boolean>;
|
|
66
74
|
view(i: number | null): void;
|
|
67
75
|
step(delta: number): void;
|
|
68
76
|
/** File e cartelle insieme: una cartella si riconosce dalla chiave, che finisce con "/". */
|
|
@@ -76,6 +84,7 @@ export interface MediaLibrary {
|
|
|
76
84
|
/** Cestina la cartella e tutto ciò che contiene. */
|
|
77
85
|
removeFolder(prefix: string): Promise<void>;
|
|
78
86
|
resumeMedia(operation: MediaOperationStatus): Promise<void>;
|
|
87
|
+
publishMedia(operation: MediaOperationStatus): Promise<void>;
|
|
79
88
|
cancelMedia(operation: MediaOperationStatus): Promise<void>;
|
|
80
89
|
/** Il nome da mostrare per un prefisso: l'etichetta se c'è, altrimenti lo slug. */
|
|
81
90
|
labelOf(prefix: string): string;
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
12
12
|
import { api, ApiError } from "../../api.js";
|
|
13
13
|
import { storageKey } from "../../config.js";
|
|
14
|
-
import { contentPersistenceMode } from "../../app/site.js";
|
|
14
|
+
import { contentPersistenceMode, site } from "../../app/site.js";
|
|
15
15
|
import { startMediaUpload, useMediaUploadCompletions, useMediaUploads, } from "./media-upload-queue.js";
|
|
16
|
-
import { invalidMediaMoveReason, isValidMediaMoveDestination, mediaFolderSlug, mediaSelectableItems, mediaSelectionState, normalizeMediaSelection, summarizeMediaSelection, tryBeginMediaMutation, } from "./media-model.js";
|
|
16
|
+
import { invalidMediaMoveReason, isValidMediaMoveDestination, mediaFolderSlug, mediaAltStatus, mediaSelectableItems, mediaSelectionState, normalizeMediaSelection, summarizeMediaSelection, tryBeginMediaMutation, } from "./media-model.js";
|
|
17
17
|
import { executeMediaMovePlan, mediaMoveConfirmation, mediaMoveNeedsConfirmation, } from "./media-move.js";
|
|
18
18
|
/* Quanti file per richiesta nelle operazioni lunghe (cestina, sposta). Ogni file costa al
|
|
19
19
|
* server tre operazioni su R2 — legge, riscrive altrove, cancella — su oggetti da qualche MB.
|
|
@@ -29,7 +29,11 @@ export async function deleteMediaKeys(keys, remove, onProgress = () => { }) {
|
|
|
29
29
|
if (preflight.d1 === true) {
|
|
30
30
|
const done = preflight.trashed ?? keys.length;
|
|
31
31
|
onProgress(done);
|
|
32
|
-
return {
|
|
32
|
+
return {
|
|
33
|
+
done,
|
|
34
|
+
operationId: preflight.operationId,
|
|
35
|
+
requiresPublication: preflight.requiresPublication === true,
|
|
36
|
+
};
|
|
33
37
|
}
|
|
34
38
|
if (!preflight.refsToken) {
|
|
35
39
|
throw new Error("Il server non ha confermato la verifica dei riferimenti.");
|
|
@@ -92,6 +96,9 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
92
96
|
// tutti i giorni non ha motivo di azzerarsi per una riorganizzazione dei sorgenti.
|
|
93
97
|
const [sortKey, setSortKey] = useState(() => localStorage.getItem(storageKey("sortKey")) || "uploaded");
|
|
94
98
|
const [sortDir, setSortDir] = useState(() => localStorage.getItem(storageKey("sortDir")) || "desc");
|
|
99
|
+
const [altFilter, setAltFilter] = useState("all");
|
|
100
|
+
const altLocales = site().languages?.available ?? ["default"];
|
|
101
|
+
const defaultAltLocale = site().languages?.default ?? "default";
|
|
95
102
|
/* I nomi per esteso già visti. Servono al percorso in alto: dentro `food/piatti-2024/`
|
|
96
103
|
* l'elenco delle cartelle non contiene più né l'una né l'altra — `folders` ha il primo livello
|
|
97
104
|
* e `subfolders` i FIGLI di dove sei — quindi senza memoria il percorso mostrerebbe gli slug,
|
|
@@ -230,13 +237,16 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
230
237
|
return () => removeEventListener("beforeunload", warn);
|
|
231
238
|
}, [progress]);
|
|
232
239
|
const shown = useMemo(() => {
|
|
233
|
-
|
|
240
|
+
let list = mode === "pick" ? files.filter((f) => (f.type ?? "").startsWith("image/")) : files.slice();
|
|
241
|
+
if (mode === "manage" && altFilter !== "all") {
|
|
242
|
+
list = list.filter((file) => (file.type ?? "").startsWith("image/") && mediaAltStatus(file, altLocales) === altFilter);
|
|
243
|
+
}
|
|
234
244
|
const dir = sortDir === "asc" ? 1 : -1;
|
|
235
245
|
return list.sort((a, b) => {
|
|
236
246
|
const x = a[sortKey] ?? "", y = b[sortKey] ?? "";
|
|
237
247
|
return (x < y ? -1 : x > y ? 1 : 0) * dir;
|
|
238
248
|
});
|
|
239
|
-
}, [files, sortKey, sortDir, mode]);
|
|
249
|
+
}, [files, sortKey, sortDir, mode, altFilter, altLocales]);
|
|
240
250
|
const selectableItems = useMemo(() => mediaSelectableItems({ mode, prefix, shown, subfolders }), [mode, prefix, shown, subfolders]);
|
|
241
251
|
const selectableIds = useMemo(() => selectableItems.map((item) => item.id), [selectableItems]);
|
|
242
252
|
const selectableIdSet = useMemo(() => new Set(selectableIds), [selectableIds]);
|
|
@@ -326,6 +336,29 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
326
336
|
setSettingsBusy(false);
|
|
327
337
|
}
|
|
328
338
|
}, [autoWebp]);
|
|
339
|
+
const saveAlt = useCallback(async (key, alt) => {
|
|
340
|
+
if (!beginMutation("saving_alt"))
|
|
341
|
+
return false;
|
|
342
|
+
setBusy(true);
|
|
343
|
+
setError(null);
|
|
344
|
+
try {
|
|
345
|
+
const result = await api.saveMediaAlt(key, alt);
|
|
346
|
+
setFiles((current) => current.map((file) => file.key === key ? { ...file, alt: result.alt } : file));
|
|
347
|
+
/* Se si sta facendo audit dei mancanti, al salvataggio la foto può uscire dal filtro:
|
|
348
|
+
* chiudere il visore evita che il suo indice finisca per indicare la foto successiva. */
|
|
349
|
+
if (altFilter !== "all")
|
|
350
|
+
setViewing(null);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
setError(error instanceof ApiError ? error.message : String(error));
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
setBusy(false);
|
|
359
|
+
endMutation();
|
|
360
|
+
}
|
|
361
|
+
}, [altFilter, beginMutation, endMutation]);
|
|
329
362
|
const labelOf = useCallback((p) => {
|
|
330
363
|
const f = folders.find((x) => x.prefix === p) ?? subfolders.find((x) => x.prefix === p);
|
|
331
364
|
return f?.label || seenLabels.current[p] || p.replace(/\/$/, "").split("/").pop() || "";
|
|
@@ -411,21 +444,17 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
411
444
|
throw new Error("Prima di eliminare queste foto devi sostituirle nel contenuto:\n"
|
|
412
445
|
+ u.blockers.map((b) => `• ${b.label}: ${b.reason}`).join("\n"));
|
|
413
446
|
}
|
|
414
|
-
/*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
* riferimenti nelle pagine resta sulla bozza finché non si pubblica — quindi per quel
|
|
419
|
-
* tempo le pagine online chiedono una foto che non c'è più.
|
|
420
|
-
*
|
|
421
|
-
* La finestra esisteva anche prima (durava la build, un paio di minuti); ora dura fino
|
|
422
|
-
* alla pubblicazione, cioè quanto decide chi sta lavorando. Non si aggiunge macchinario
|
|
423
|
-
* per chiuderla: si dice, e si dice SOLO quando è vero — un avviso che compare sempre
|
|
424
|
-
* smette di essere letto, e allora non avvisa più di niente. */
|
|
447
|
+
/* Quando una foto è online la conferma deve descrivere l'intera operazione: Admin
|
|
448
|
+
* pubblicherà una revisione isolata delle sole pagine coinvolte e toccherà il file
|
|
449
|
+
* soltanto dopo. Sostituire prima resta la strada raccomandata perché una rimozione
|
|
450
|
+
* valida per lo schema può comunque cambiare l'impaginazione. */
|
|
425
451
|
const msg = u.count
|
|
426
|
-
? `Eliminare ${quante}?\n\
|
|
427
|
-
+
|
|
428
|
-
+
|
|
452
|
+
? `Eliminare ${quante}?\n\n${u.count === 1 ? "Una delle immagini è utilizzata" : `${u.count} immagini sono utilizzate`} in:\n`
|
|
453
|
+
+ u.pages.map((page) => `• ${page}`).join("\n")
|
|
454
|
+
+ `\n\nAdmin rimuoverà le immagini da queste pagine e pubblicherà automaticamente soltanto questa modifica. `
|
|
455
|
+
+ `Gallerie, griglie o sezioni potrebbero cambiare impaginazione.\n\n`
|
|
456
|
+
+ `Ti consigliamo di annullare e sostituire prima le immagini nelle pagine indicate. `
|
|
457
|
+
+ `Confermando, procederai comunque con la rimozione e la pubblicazione.`
|
|
429
458
|
: `Eliminare ${quante}?`;
|
|
430
459
|
if (!confirm(`${msg}\n\nLe foto vanno nel cestino e si possono recuperare.`))
|
|
431
460
|
return;
|
|
@@ -435,6 +464,18 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
435
464
|
const result = await deleteMediaKeys(keys, api.remove, (done) => setProgress({ done, total: keys.length, verb: "Cestino" }));
|
|
436
465
|
refsToken = result.refsToken;
|
|
437
466
|
const done = result.done;
|
|
467
|
+
if (result.operationId && result.requiresPublication) {
|
|
468
|
+
setProgress({ done: keys.length, total: keys.length, verb: "Pubblico" });
|
|
469
|
+
try {
|
|
470
|
+
await api.contentPublishMedia(result.operationId);
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
const apiError = error;
|
|
474
|
+
failure = apiError.code === "deployment_lane_busy" || apiError.code === "release_in_progress"
|
|
475
|
+
? "C'è già una pubblicazione in corso. L'eliminazione è al sicuro: usa «Riprova pubblicazione» quando termina."
|
|
476
|
+
: `Eliminazione preparata, ma la pubblicazione non è partita: ${apiError.message}`;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
438
479
|
if (done < keys.length) {
|
|
439
480
|
failure = `Cestinate ${done} foto su ${keys.length}: le altre sono ancora lì, riprova.`;
|
|
440
481
|
}
|
|
@@ -641,9 +682,27 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
641
682
|
}, [prefix, load, moveFolder, beginMutation, endMutation]);
|
|
642
683
|
/** Cestina una cartella con tutto quello che contiene. È `remove` con una lista di uno. */
|
|
643
684
|
const removeFolder = useCallback((target) => remove([target]), [remove]);
|
|
644
|
-
const
|
|
645
|
-
if (!
|
|
685
|
+
const publishMedia = useCallback(async (operation) => {
|
|
686
|
+
if (!beginMutation("journal"))
|
|
646
687
|
return;
|
|
688
|
+
setBusy(true);
|
|
689
|
+
setError(null);
|
|
690
|
+
let failure = null;
|
|
691
|
+
try {
|
|
692
|
+
await api.contentPublishMedia(operation.operationId);
|
|
693
|
+
}
|
|
694
|
+
catch (e) {
|
|
695
|
+
failure = e instanceof ApiError ? e.message : String(e);
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
setBusy(false);
|
|
699
|
+
await load(prefix);
|
|
700
|
+
endMutation();
|
|
701
|
+
if (failure)
|
|
702
|
+
setError(`Pubblicazione Media non riuscita: ${failure}`);
|
|
703
|
+
}
|
|
704
|
+
}, [beginMutation, endMutation, load, prefix]);
|
|
705
|
+
const resumeMedia = useCallback(async (operation) => {
|
|
647
706
|
if (!beginMutation("journal"))
|
|
648
707
|
return;
|
|
649
708
|
setBusy(true);
|
|
@@ -697,7 +756,8 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
697
756
|
mode, multiple, prefix, folders, subfolders, shown, selected, selectableIds,
|
|
698
757
|
selectedFiles: selectionSummary.files, selectedFolders: selectionSummary.folders,
|
|
699
758
|
allSelected, someSelected, busy, mutating, operationState, error, mediaOperations, progress,
|
|
700
|
-
sortKey, sortDir,
|
|
759
|
+
sortKey, sortDir, altFilter, altLocales, defaultAltLocale,
|
|
760
|
+
uploadDraft, pickUploadJob, viewing, autoWebp, settingsBusy,
|
|
701
761
|
go: (p) => { if (!mutationRef.current)
|
|
702
762
|
void load(p); },
|
|
703
763
|
reload: () => { if (!mutationRef.current)
|
|
@@ -705,6 +765,11 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
705
765
|
toggle, setAll,
|
|
706
766
|
setSort: setSortKey,
|
|
707
767
|
flipDir: () => setSortDir((d) => (d === "asc" ? "desc" : "asc")),
|
|
768
|
+
setAltFilter: (filter) => {
|
|
769
|
+
setAltFilter(filter);
|
|
770
|
+
setSelected(new Set());
|
|
771
|
+
setViewing(null);
|
|
772
|
+
},
|
|
708
773
|
upload,
|
|
709
774
|
openUpload: (files = [], destination) => {
|
|
710
775
|
if (mutationRef.current)
|
|
@@ -718,12 +783,12 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
718
783
|
setUploadDraft(null); },
|
|
719
784
|
setUploadFiles: (files) => setUploadDraft((draft) => (draft ? { ...draft, files: Array.from(files) } : draft)),
|
|
720
785
|
setUploadDestination: (destination) => setUploadDraft((draft) => (draft ? { ...draft, destination } : draft)),
|
|
721
|
-
setAutoWebp,
|
|
786
|
+
setAutoWebp, saveAlt,
|
|
722
787
|
view: setViewing,
|
|
723
788
|
step,
|
|
724
789
|
move,
|
|
725
790
|
canMoveTo: (ids, dest) => !mutationRef.current && isValidMediaMoveDestination(ids, dest),
|
|
726
|
-
remove, newFolder, renameFolder, removeFolder, resumeMedia, cancelMedia, labelOf,
|
|
791
|
+
remove, newFolder, renameFolder, removeFolder, resumeMedia, publishMedia, cancelMedia, labelOf,
|
|
727
792
|
clearError: () => setError(null),
|
|
728
793
|
};
|
|
729
794
|
}
|