@cardinali-studio/adminkit 2.2.21 → 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/routes/GallerySectionsEditor.d.ts +1 -1
- package/dist/app/routes/GallerySectionsEditor.js +14 -2
- package/dist/app/routes/PageEditor.js +11 -4
- package/dist/app/routes/PortfolioWorksEditor.d.ts +1 -1
- package/dist/app/routes/PortfolioWorksEditor.js +11 -3
- 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 +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export declare class ApiError extends Error {
|
|
|
6
6
|
details: unknown;
|
|
7
7
|
constructor(message: string, code: string, status: number, details?: unknown);
|
|
8
8
|
}
|
|
9
|
+
export interface AssetAltText {
|
|
10
|
+
/** Vero significa che il sito deve emettere deliberatamente alt="". */
|
|
11
|
+
decorative: boolean;
|
|
12
|
+
/** Chiave "default" nei siti monolingui, codice lingua nei siti multilingui. */
|
|
13
|
+
texts: Record<string, string>;
|
|
14
|
+
}
|
|
9
15
|
export interface AssetFile {
|
|
10
16
|
key: string;
|
|
11
17
|
name: string;
|
|
@@ -14,9 +20,13 @@ export interface AssetFile {
|
|
|
14
20
|
uploaded?: string;
|
|
15
21
|
capturedAt?: string;
|
|
16
22
|
type?: string;
|
|
23
|
+
alt?: AssetAltText;
|
|
17
24
|
w?: number | null;
|
|
18
25
|
h?: number | null;
|
|
19
26
|
}
|
|
27
|
+
/** Risolve il testo Media da copiare nel singolo utilizzo editoriale. Il campo della pagina
|
|
28
|
+
* resta un override: cambiare in seguito il default del file non riscrive contenuti esistenti. */
|
|
29
|
+
export declare function assetAltText(file: Pick<AssetFile, "alt">, locale?: string): string;
|
|
20
30
|
export interface AssetFolder {
|
|
21
31
|
prefix: string;
|
|
22
32
|
name: string;
|
|
@@ -57,11 +67,15 @@ export interface RepoPage {
|
|
|
57
67
|
sha?: string;
|
|
58
68
|
}>;
|
|
59
69
|
}
|
|
60
|
-
export type
|
|
70
|
+
export type ContentDocumentPublishTarget = {
|
|
61
71
|
documentId: string;
|
|
62
72
|
expectedDraftRevisionId: string | null;
|
|
63
73
|
includeSiteAccess?: boolean;
|
|
64
74
|
};
|
|
75
|
+
export type ContentMediaPublishTarget = {
|
|
76
|
+
mediaOperationId: string;
|
|
77
|
+
};
|
|
78
|
+
export type ContentPublishTarget = ContentDocumentPublishTarget | ContentMediaPublishTarget;
|
|
65
79
|
export type ContentPublishDocument = {
|
|
66
80
|
documentId: string;
|
|
67
81
|
kind: string;
|
|
@@ -203,6 +217,17 @@ export declare const api: {
|
|
|
203
217
|
dependentDocuments?: number;
|
|
204
218
|
remainingAhead?: number;
|
|
205
219
|
}>;
|
|
220
|
+
contentPublishMedia: (mediaOperationId: string) => Promise<{
|
|
221
|
+
ok: true;
|
|
222
|
+
published: boolean;
|
|
223
|
+
releaseId?: string;
|
|
224
|
+
attemptId?: string;
|
|
225
|
+
message?: string;
|
|
226
|
+
verified?: boolean;
|
|
227
|
+
observedReleaseId?: string | null;
|
|
228
|
+
documents?: number;
|
|
229
|
+
remainingAhead?: number;
|
|
230
|
+
}>;
|
|
206
231
|
publishState: () => Promise<{
|
|
207
232
|
ok: true;
|
|
208
233
|
configured: boolean;
|
|
@@ -257,6 +282,9 @@ export declare const api: {
|
|
|
257
282
|
error: string;
|
|
258
283
|
}[];
|
|
259
284
|
warning?: string;
|
|
285
|
+
operationId?: string;
|
|
286
|
+
requiresPublication?: boolean;
|
|
287
|
+
completed?: number;
|
|
260
288
|
}>;
|
|
261
289
|
/** Riscrive i riferimenti, poi elimina gli originali le cui copie sono state verificate. */
|
|
262
290
|
moveApplyRefs: (applyRefs: Record<string, string>) => Promise<{
|
|
@@ -274,7 +302,12 @@ export declare const api: {
|
|
|
274
302
|
ok: true;
|
|
275
303
|
operations: MediaOperationStatus[];
|
|
276
304
|
}>;
|
|
277
|
-
|
|
305
|
+
saveMediaAlt: (key: string, alt: AssetAltText) => Promise<{
|
|
306
|
+
ok: true;
|
|
307
|
+
key: string;
|
|
308
|
+
alt: AssetAltText;
|
|
309
|
+
}>;
|
|
310
|
+
resumeMediaOperation: (operationId: string, releaseId: string | null) => Promise<{
|
|
278
311
|
ok: true;
|
|
279
312
|
completed: number;
|
|
280
313
|
failed: number;
|
|
@@ -313,6 +346,9 @@ export declare const api: {
|
|
|
313
346
|
d1?: boolean;
|
|
314
347
|
deferred?: boolean;
|
|
315
348
|
operationId?: string;
|
|
349
|
+
requiresPublication?: boolean;
|
|
350
|
+
completed?: number;
|
|
351
|
+
failed?: number;
|
|
316
352
|
}>;
|
|
317
353
|
requests: () => Promise<{
|
|
318
354
|
ok: true;
|
package/dist/api.js
CHANGED
|
@@ -34,6 +34,13 @@ const post = (url, data) => call(url, {
|
|
|
34
34
|
headers: { "Content-Type": "application/json" },
|
|
35
35
|
body: JSON.stringify(data),
|
|
36
36
|
});
|
|
37
|
+
/** Risolve il testo Media da copiare nel singolo utilizzo editoriale. Il campo della pagina
|
|
38
|
+
* resta un override: cambiare in seguito il default del file non riscrive contenuti esistenti. */
|
|
39
|
+
export function assetAltText(file, locale = "default") {
|
|
40
|
+
if (file.alt?.decorative)
|
|
41
|
+
return "";
|
|
42
|
+
return file.alt?.texts[locale] ?? file.alt?.texts.default ?? "";
|
|
43
|
+
}
|
|
37
44
|
/** Builds the only D1 Save wire shape. The Worker recomputes these hashes too,
|
|
38
45
|
* so retries are deterministic rather than client-trusted. */
|
|
39
46
|
export async function contentSaveRequest(command) {
|
|
@@ -67,6 +74,7 @@ export const api = {
|
|
|
67
74
|
* materialize, while the panel only displays the pending immutable inputs. */
|
|
68
75
|
contentPublishState: () => call("/admin/api/content/publish"),
|
|
69
76
|
contentPublish: (target) => post("/admin/api/content/publish", target),
|
|
77
|
+
contentPublishMedia: (mediaOperationId) => post("/admin/api/content/publish", { mediaOperationId }),
|
|
70
78
|
// ---- pubblicazione
|
|
71
79
|
/* Salvare scrive sulla bozza e non fa partire nessuna build; è questo che manda online. Due
|
|
72
80
|
* gesti separati, quindi due chiamate: una chiede quanto c'è di non pubblicato, l'altra lo
|
|
@@ -99,6 +107,11 @@ export const api = {
|
|
|
99
107
|
/** Riscrive i riferimenti, poi elimina gli originali le cui copie sono state verificate. */
|
|
100
108
|
moveApplyRefs: (applyRefs) => post("/admin/api/assets/move", { applyRefs }),
|
|
101
109
|
mediaOperations: () => call("/admin/api/assets/media"),
|
|
110
|
+
saveMediaAlt: (key, alt) => call("/admin/api/assets/media", {
|
|
111
|
+
method: "PATCH",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({ key, alt }),
|
|
114
|
+
}),
|
|
102
115
|
resumeMediaOperation: (operationId, releaseId) => post("/admin/api/assets/media", { action: "resume", operationId, releaseId }),
|
|
103
116
|
cancelMediaOperation: (operationId) => post("/admin/api/assets/media", { action: "cancel", operationId }),
|
|
104
117
|
usage: (target) => post("/admin/api/assets/usage", target),
|
|
@@ -31,10 +31,12 @@ import { useEffect, useRef, useState } from "react";
|
|
|
31
31
|
import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from "@dnd-kit/core";
|
|
32
32
|
import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
|
|
33
33
|
import { CSS } from "@dnd-kit/utilities";
|
|
34
|
+
import { assetAltText } from "../../index.js";
|
|
34
35
|
import { GALLERY_BLOCK_ID, emptySection } from "../../index.js";
|
|
35
36
|
import { normalizeSections, withGalleryBlock } from "../../normalize.js";
|
|
36
37
|
import { ParagraphEditor } from "../ui/ParagraphEditor";
|
|
37
38
|
import { Icon } from "../ui/icons";
|
|
39
|
+
import { site } from "../site";
|
|
38
40
|
function sectionId() {
|
|
39
41
|
return `section-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
|
40
42
|
}
|
|
@@ -58,8 +60,18 @@ function SortableSection({ section, index, onPatch, onDuplicate, onRemove, rende
|
|
|
58
60
|
const chooseImage = async () => {
|
|
59
61
|
const selected = await pickMediaFiles?.({ multiple: false });
|
|
60
62
|
const file = selected?.[0];
|
|
61
|
-
if (file)
|
|
62
|
-
|
|
63
|
+
if (file) {
|
|
64
|
+
const locale = site().languages?.default ?? "default";
|
|
65
|
+
onPatch({
|
|
66
|
+
image: file.url,
|
|
67
|
+
w: file.w ?? null,
|
|
68
|
+
h: file.h ?? null,
|
|
69
|
+
/* Il valore del Media è il default iniziale; questo campo resta modificabile e quindi
|
|
70
|
+
* costituisce l'override del singolo utilizzo. Una decorativa produce deliberatamente
|
|
71
|
+
* alt="", non il nome del file. */
|
|
72
|
+
alt: assetAltText(file, locale),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
63
75
|
};
|
|
64
76
|
return (_jsxs("article", { ref: setNodeRef, className: ["gs-card", `is-${section.type}`, isDragging ? "is-dragging" : ""]
|
|
65
77
|
.filter(Boolean).join(" "), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("header", { className: "gs-card-head", children: [_jsx("button", { type: "button", className: "gs-drag", title: "Trascina per riordinare", "aria-label": `Riordina sezione ${labelOf(section.type)}`, ...attributes, ...listeners, children: _jsx(Icon.grip, {}) }), _jsx("span", { className: "gs-card-icon", children: _jsx(SectionIcon, { type: section.type }) }), _jsx("strong", { children: labelOf(section.type) }), _jsx("span", { className: "gs-position", children: index + 1 }), !isGallery && (_jsx("button", { type: "button", className: "gs-head-action", onClick: onDuplicate, children: "Duplica" })), (!isGallery || section.images.length > 0) && (_jsx("button", { type: "button", className: "gs-head-action is-danger", onClick: onRemove, children: "Elimina" }))] }), isGallery && (_jsx("div", { className: "gs-gallery", children: renderGallery?.(section.images, (images) => onPatch({ images })) })), section.type === "header" && (_jsxs("div", { className: "gs-fields", children: [_jsxs("label", { children: [_jsxs("span", { children: ["Occhiello ", _jsx("small", { children: "facoltativo" })] }), _jsx("input", { value: section.eyebrow, maxLength: 120, placeholder: "Es. Fotografia gastronomica", onChange: (event) => onPatch({ eyebrow: event.target.value }) })] }), _jsxs("label", { children: [_jsx("span", { children: "Titolo" }), _jsx("input", { value: section.heading, maxLength: 200, placeholder: "Titolo della sezione", onChange: (event) => onPatch({ heading: event.target.value }) })] }), _jsxs("label", { children: [_jsxs("span", { children: ["Introduzione ", _jsx("small", { children: "facoltativa" })] }), _jsx("textarea", { rows: 4, value: section.lead, maxLength: 800, placeholder: "Una breve apertura sotto il titolo.", onChange: (event) => onPatch({ lead: event.target.value }) })] })] })), section.type === "text" && (_jsx("div", { className: "gs-fields", children: _jsxs("label", { children: [_jsx("span", { children: "Testo" }), _jsx(ParagraphEditor, { value: section.paragraphs, onChange: (paragraphs) => onPatch({ paragraphs }) }), _jsx("small", { children: "Seleziona una parola per metterla in grassetto, in corsivo o per collegarla." })] }) })), section.type === "image" && (_jsxs("div", { className: "gs-image-editor", children: [_jsx("button", { type: "button", className: "gs-image-preview", onClick: () => void chooseImage(), children: section.image ? _jsx("img", { src: section.image, alt: "" }) : (_jsxs("span", { children: [_jsx(Icon.media, {}), " Scegli un\u2019immagine"] })) }), _jsxs("div", { className: "gs-image-fields", children: [_jsxs("div", { className: "gs-image-actions", children: [_jsx("button", { type: "button", className: "btn-soft", onClick: () => void chooseImage(), children: section.image ? "Cambia immagine" : "Scegli immagine" }), section.image && (_jsx("button", { type: "button", className: "btn-soft is-quiet", onClick: () => onPatch({ image: "", w: null, h: null }), children: "Rimuovi" }))] }), _jsxs("label", { children: [_jsx("span", { children: "Testo alternativo" }), _jsx("input", { value: section.alt, maxLength: 300, placeholder: "Descrive l\u2019immagine a chi non pu\u00F2 vederla", onChange: (event) => onPatch({ alt: event.target.value }) })] }), _jsxs("label", { children: [_jsxs("span", { children: ["Didascalia ", _jsx("small", { children: "facoltativa" })] }), _jsx("input", { value: section.caption, maxLength: 300, onChange: (event) => onPatch({ caption: event.target.value }) })] }), _jsxs("div", { className: "gs-layout", children: [_jsx("span", { children: "Dimensione" }), _jsxs("div", { className: "seg", role: "group", "aria-label": "Dimensione dell\u2019immagine", children: [_jsx("button", { type: "button", className: "seg-btn" + (section.layout === "wide" ? " is-on" : ""), onClick: () => onPatch({ layout: "wide" }), children: "Larga" }), _jsx("button", { type: "button", className: "seg-btn" + (section.layout === "contained" ? " is-on" : ""), onClick: () => onPatch({ layout: "contained" }), children: "Contenuta" })] })] })] })] }))] }));
|
|
@@ -13,8 +13,8 @@ import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter,
|
|
|
13
13
|
import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable";
|
|
14
14
|
import { CSS } from "@dnd-kit/utilities";
|
|
15
15
|
import { ProseField, SchemaForm } from "../../form/index.js";
|
|
16
|
-
import { api } from "../../index.js";
|
|
17
|
-
import { pageType } from "../site.js";
|
|
16
|
+
import { api, assetAltText } from "../../index.js";
|
|
17
|
+
import { pageType, site } from "../site.js";
|
|
18
18
|
import { normalizeGalleryLayout } from "../lib/gallery-layout";
|
|
19
19
|
import { uniqueSlug } from "../lib/slug";
|
|
20
20
|
import { proseStats } from "../lib/text";
|
|
@@ -165,9 +165,15 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
|
165
165
|
if (!picked?.length)
|
|
166
166
|
return;
|
|
167
167
|
const have = new Set(list.map((item) => item.src));
|
|
168
|
+
const locale = site().languages?.default ?? "default";
|
|
168
169
|
const additions = picked
|
|
169
170
|
.filter((file) => !have.has(file.url))
|
|
170
|
-
.map((file) => ({
|
|
171
|
+
.map((file) => ({
|
|
172
|
+
src: file.url,
|
|
173
|
+
w: file.w ?? null,
|
|
174
|
+
h: file.h ?? null,
|
|
175
|
+
alt: assetAltText(file, locale),
|
|
176
|
+
}));
|
|
171
177
|
if (additions.length)
|
|
172
178
|
onChange(position === "start" ? [...additions, ...list] : [...list, ...additions]);
|
|
173
179
|
};
|
|
@@ -180,8 +186,9 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
|
180
186
|
alert("Questa foto è già presente nella gallery.");
|
|
181
187
|
return;
|
|
182
188
|
}
|
|
189
|
+
const locale = site().languages?.default ?? "default";
|
|
183
190
|
onChange(list.map((item, itemIndex) => itemIndex === index
|
|
184
|
-
? { ...item, src: file.url, w: file.w ?? null, h: file.h ?? null }
|
|
191
|
+
? { ...item, src: file.url, w: file.w ?? null, h: file.h ?? null, alt: assetAltText(file, locale) }
|
|
185
192
|
: item));
|
|
186
193
|
};
|
|
187
194
|
const finishDrag = ({ active, over }) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AssetFile } from "../../index.js";
|
|
2
2
|
import { normalizeWorks, type Work, type WorkCard, type WorkCardType, type WorkInfo } from "../../normalize.js";
|
|
3
3
|
export type { WorkCardType, WorkInfo, WorkCard, Work };
|
|
4
4
|
export { normalizeWorks };
|
|
@@ -31,6 +31,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
31
31
|
import { DndContext, closestCenter } from "@dnd-kit/core";
|
|
32
32
|
import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
|
|
33
33
|
import { CSS } from "@dnd-kit/utilities";
|
|
34
|
+
import { assetAltText } from "../../index.js";
|
|
34
35
|
import { ParagraphEditor } from "../ui/ParagraphEditor";
|
|
35
36
|
/* La casella immagine sta in `ui/`: la usano copertina e card qui, e il ritratto della pagina
|
|
36
37
|
"chi sono". Vedi il commento in cima a quel file. */
|
|
@@ -41,6 +42,7 @@ import { OpenCard } from "../ui/OpenCard";
|
|
|
41
42
|
import { useDragSensors } from "../ui/dnd";
|
|
42
43
|
import { movedOpenIndex, removedOpenIndex } from "../lib/reorder";
|
|
43
44
|
import { Icon } from "../ui/icons";
|
|
45
|
+
import { site } from "../site.js";
|
|
44
46
|
import { normalizeWorks, emptyWork, emptyCard, CARD_KINDS, } from "../../normalize.js";
|
|
45
47
|
export { normalizeWorks };
|
|
46
48
|
const str = (value) => (typeof value === "string" ? value : "");
|
|
@@ -69,7 +71,10 @@ function SortableCard({ id, card, index, onPatch, onRemove, pickMediaFiles }) {
|
|
|
69
71
|
const chooseImage = async () => {
|
|
70
72
|
const file = (await pickMediaFiles?.({ multiple: false }))?.[0];
|
|
71
73
|
if (file)
|
|
72
|
-
onPatch({
|
|
74
|
+
onPatch({
|
|
75
|
+
image: file.url,
|
|
76
|
+
alt: assetAltText(file, site().languages?.default ?? "default"),
|
|
77
|
+
});
|
|
73
78
|
};
|
|
74
79
|
return (_jsxs("article", { ref: setNodeRef, className: "pw-card" + (isDragging ? " is-dragging" : ""), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("header", { className: "pw-card-head", children: [_jsx("button", { type: "button", className: "pw-grip", "aria-label": `Riordina card ${index + 1}`, ...attributes, ...listeners, children: _jsx(Icon.grip, {}) }), _jsx("span", { className: "pw-card-kind", children: kind.label }), _jsx("div", { className: "seg", role: "group", "aria-label": "Tipo di card", children: CARD_KINDS.map((option) => (_jsx("button", { type: "button", className: "seg-btn" + (card.type === option.type ? " is-on" : ""), onClick: () => onPatch({ type: option.type }), title: option.hint, children: option.label }, option.type))) }), _jsx("button", { type: "button", className: "pw-icon-btn is-danger", "aria-label": "Elimina card", onClick: onRemove, children: _jsx(Icon.trash, {}) })] }), _jsxs("div", { className: "pw-card-body" + (wantsImage ? " is-split" : ""), children: [wantsImage && (_jsx(MediaSlot, { value: card.image, ratio: "wide", empty: "Scegli un\u2019immagine", onPick: () => void chooseImage(), onClear: () => onPatch({ image: "" }) })), _jsxs("div", { className: "pw-card-text", children: [wantsImage && (_jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Testo alternativo" }), _jsx("input", { className: "pw-field", value: card.alt, maxLength: 200, placeholder: "Descrive l\u2019immagine a chi non pu\u00F2 vederla", onChange: (event) => onPatch({ alt: event.target.value }) })] })), wantsText && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "pw-label", children: [_jsxs("span", { children: ["Occhiello ", _jsx("small", { children: "facoltativo" })] }), _jsx("input", { className: "pw-field", value: card.kicker, maxLength: 60, onChange: (event) => onPatch({ kicker: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Titolo" }), _jsx("input", { className: "pw-field", value: card.heading, maxLength: 120, onChange: (event) => onPatch({ heading: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Paragrafi" }), _jsx(ParagraphEditor, { className: "pw-field pw-textarea", rows: 5, value: card.body, placeholder: "Scrivi il testo della card.\nVai a capo per iniziare un paragrafo nuovo.", onChange: (body) => onPatch({ body }) })] })] }))] })] })] }));
|
|
75
80
|
}
|
|
@@ -100,7 +105,10 @@ function SortableWork({ id, work, index, total, open, onToggle, onPatch, onDupli
|
|
|
100
105
|
const chooseCover = async () => {
|
|
101
106
|
const file = (await pickMediaFiles?.({ multiple: false }))?.[0];
|
|
102
107
|
if (file)
|
|
103
|
-
onPatch({
|
|
108
|
+
onPatch({
|
|
109
|
+
cover: file.url,
|
|
110
|
+
coverAlt: assetAltText(file, site().languages?.default ?? "default"),
|
|
111
|
+
});
|
|
104
112
|
};
|
|
105
113
|
const pieces = [
|
|
106
114
|
work.cards.length ? `${work.cards.length} ${work.cards.length === 1 ? "card" : "card"}` : "",
|
|
@@ -109,7 +117,7 @@ function SortableWork({ id, work, index, total, open, onToggle, onPatch, onDupli
|
|
|
109
117
|
return (
|
|
110
118
|
/* Il guscio (maniglia, miniatura, titolo, apri/chiudi) sta in `ui/OpenCard`: è lo stesso
|
|
111
119
|
delle edizioni dei workshop, e tenerlo qui avrebbe voluto dire ricopiarlo là. */
|
|
112
|
-
_jsxs(OpenCard, { id: id, index: index, total: total, open: open, thumb: work.cover, title: work.title, empty: "Lavoro senza titolo", meta: pieces.join(" · "), kind: "questo lavoro", onToggle: onToggle, actions: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "btn-soft is-quiet", onClick: onDuplicate, children: "Duplica" }), _jsx("button", { type: "button", className: "btn-soft is-danger", onClick: onRemove, children: "Elimina" })] }), children: [_jsxs("div", { className: "pw-identity", children: [_jsx(MediaSlot, { value: work.cover, ratio: "cover", empty: "Scegli la copertina", onPick: () => void chooseCover(), onClear: () => onPatch({ cover: "" }) }), _jsxs("div", { className: "pw-identity-fields", children: [_jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Titolo" }), _jsx("input", { className: "pw-field pw-field-title", value: work.title, maxLength: 120, placeholder: "Il nome del lavoro", onChange: (event) => onPatch({ title: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Descrizione" }), _jsx(ParagraphEditor, { className: "pw-field pw-textarea", rows: 7, value: work.description, placeholder: "Racconta il lavoro.\nVai a capo per iniziare un paragrafo nuovo.", onChange: (description) => onPatch({ description }) }), _jsx("small", { children: "Seleziona una parola per metterla in grassetto, in corsivo o per collegarla." })] })] })] }), _jsx(InfoList, { items: work.info, onChange: (info) => onPatch({ info }) }), _jsx(CardList, { items: work.cards, onChange: (cards) => onPatch({ cards }), pickMediaFiles: pickMediaFiles })] }));
|
|
120
|
+
_jsxs(OpenCard, { id: id, index: index, total: total, open: open, thumb: work.cover, title: work.title, empty: "Lavoro senza titolo", meta: pieces.join(" · "), kind: "questo lavoro", onToggle: onToggle, actions: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "btn-soft is-quiet", onClick: onDuplicate, children: "Duplica" }), _jsx("button", { type: "button", className: "btn-soft is-danger", onClick: onRemove, children: "Elimina" })] }), children: [_jsxs("div", { className: "pw-identity", children: [_jsx(MediaSlot, { value: work.cover, ratio: "cover", empty: "Scegli la copertina", onPick: () => void chooseCover(), onClear: () => onPatch({ cover: "" }) }), _jsxs("div", { className: "pw-identity-fields", children: [_jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Titolo" }), _jsx("input", { className: "pw-field pw-field-title", value: work.title, maxLength: 120, placeholder: "Il nome del lavoro", onChange: (event) => onPatch({ title: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Testo alternativo della copertina" }), _jsx("input", { className: "pw-field", value: work.coverAlt, maxLength: 200, placeholder: "Descrive la copertina a chi non pu\u00F2 vederla", onChange: (event) => onPatch({ coverAlt: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Descrizione" }), _jsx(ParagraphEditor, { className: "pw-field pw-textarea", rows: 7, value: work.description, placeholder: "Racconta il lavoro.\nVai a capo per iniziare un paragrafo nuovo.", onChange: (description) => onPatch({ description }) }), _jsx("small", { children: "Seleziona una parola per metterla in grassetto, in corsivo o per collegarla." })] })] })] }), _jsx(InfoList, { items: work.info, onChange: (info) => onPatch({ info }) }), _jsx(CardList, { items: work.cards, onChange: (cards) => onPatch({ cards }), pickMediaFiles: pickMediaFiles })] }));
|
|
113
121
|
}
|
|
114
122
|
/* ---- L'elenco ------------------------------------------------------------------------------ */
|
|
115
123
|
export function PortfolioWorksEditor({ value, onChange, pickMediaFiles }) {
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
/* I widget. Piccoli di proposito: il valore del sistema sta nello schema, non qui.
|
|
3
3
|
* Nessuno di questi conosce il sito — etichette, opzioni e aiuti arrivano da `ui`. */
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
|
+
import { assetAltText } from "../../api.js";
|
|
5
6
|
import { registerField } from "../registry.js";
|
|
6
7
|
import { ProseField } from "../prose/ProseField.js";
|
|
7
8
|
function Wrap({ meta, error, children }) {
|
|
@@ -100,7 +101,7 @@ registerField("r2Curated", ({ meta, value, onChange, error, pickMediaFiles }) =>
|
|
|
100
101
|
const have = new Set(list.map((it) => it.src));
|
|
101
102
|
const add = picked
|
|
102
103
|
.filter((f) => !have.has(f.url))
|
|
103
|
-
.map((f) => ({ src: f.url, w: f.w ?? null, h: f.h ?? null }));
|
|
104
|
+
.map((f) => ({ src: f.url, w: f.w ?? null, h: f.h ?? null, alt: assetAltText(f) }));
|
|
104
105
|
if (add.length)
|
|
105
106
|
onChange([...list, ...add]);
|
|
106
107
|
}, children: "+ Aggiungi foto" })] }));
|
|
@@ -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;
|