@cardinali-studio/adminkit 2.2.21 → 2.2.23
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.css +15 -0
- package/dist/app/next.css +1 -0
- package/dist/app/routes/GallerySectionsEditor.d.ts +1 -1
- package/dist/app/routes/GallerySectionsEditor.js +14 -2
- package/dist/app/routes/PageEditor.d.ts +10 -0
- package/dist/app/routes/PageEditor.js +31 -8
- 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),
|
package/dist/app/app.css
CHANGED
|
@@ -707,6 +707,14 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
707
707
|
overflow: hidden; padding-inline: 2px; color: #8a8a91; text-align: center;
|
|
708
708
|
font: 400 11px/1.3 var(--ak-font); text-overflow: ellipsis; white-space: nowrap;
|
|
709
709
|
}
|
|
710
|
+
.pe-photo-alt {
|
|
711
|
+
align-self: center; display: inline-flex; align-items: center; gap: 5px; min-height: 24px;
|
|
712
|
+
padding: 3px 8px; border: 0; border-radius: 999px; background: var(--ak-inset);
|
|
713
|
+
color: var(--ak-ink-quiet); cursor: pointer; font: 500 10px/1 var(--ak-font);
|
|
714
|
+
}
|
|
715
|
+
.pe-photo-alt strong { color: #a66a13; font: 700 9px/1 var(--ak-font-mono); letter-spacing: .04em; }
|
|
716
|
+
.pe-photo-alt.is-set strong { color: #28745a; }
|
|
717
|
+
.pe-photo-alt:hover, .pe-photo-alt:focus-visible { background: var(--ak-hover); color: var(--ak-ink); outline: 0; }
|
|
710
718
|
.pe-photo-actions {
|
|
711
719
|
position: absolute; z-index: 3; top: 0; left: 50%; transform: translate(-50%, -18%);
|
|
712
720
|
display: flex; align-items: center; padding: 5px; border: 1px solid var(--ak-line);
|
|
@@ -744,6 +752,13 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
744
752
|
cursor: grabbing;
|
|
745
753
|
}
|
|
746
754
|
.pe-photo-overlay img { max-width: 100%; max-height: 100%; display: block; }
|
|
755
|
+
.pe-alt-modal { width: min(620px, 100%); }
|
|
756
|
+
.pe-alt-preview {
|
|
757
|
+
min-width: 0; display: grid; grid-template-columns: 92px minmax(0,1fr); gap: 14px; align-items: center;
|
|
758
|
+
padding: 10px; border-radius: 8px; background: var(--ak-inset);
|
|
759
|
+
}
|
|
760
|
+
.pe-alt-preview img { width: 92px; height: 72px; display: block; object-fit: contain; border-radius: 5px; background: var(--ak-bg); }
|
|
761
|
+
.pe-alt-preview span { overflow: hidden; color: var(--ak-ink-quiet); font: 500 12px/1.3 var(--ak-font); text-overflow: ellipsis; white-space: nowrap; }
|
|
747
762
|
|
|
748
763
|
/* Il masonry verticale usa il flusso CSS a colonne: riempie una colonna dall'alto verso il
|
|
749
764
|
basso prima di passare alla successiva. `.is-masonry` resta come alias visuale per markup
|
package/dist/app/next.css
CHANGED
|
@@ -707,6 +707,7 @@
|
|
|
707
707
|
border-radius: 10px;
|
|
708
708
|
box-shadow: var(--ak-lift-3);
|
|
709
709
|
}
|
|
710
|
+
:root[data-admin-interface="next"] .pe-photo-alt { border-radius: 999px; }
|
|
710
711
|
/* Tutte le azioni primarie usano la coppia di colori del tema. Alcuni componenti storici
|
|
711
712
|
* fissavano il testo a bianco: in dark l'accento diventa chiaro e richiede invece inchiostro
|
|
712
713
|
* scuro, come il pulsante di pubblicazione. */
|
|
@@ -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" })] })] })] })] }))] }));
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { PickerOptions } from "../../form/MediaPicker.js";
|
|
2
|
+
import type { GalleryImage } from "../../index.js";
|
|
2
3
|
import type { AssetFile } from "../../index.js";
|
|
3
4
|
import type { PageModel } from "../lib/pages";
|
|
5
|
+
import { type GalleryLayout } from "../lib/gallery-layout";
|
|
6
|
+
type CuratedItem = GalleryImage;
|
|
4
7
|
export declare function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, languages, activeLanguage, onLanguageChange, }: {
|
|
5
8
|
page: PageModel;
|
|
6
9
|
pages: PageModel[];
|
|
@@ -15,6 +18,12 @@ export declare function PageHeader({ page, pages, onChange, seoActive, onSeoTogg
|
|
|
15
18
|
activeLanguage?: string;
|
|
16
19
|
onLanguageChange?: (language: string) => void;
|
|
17
20
|
}): import("react").JSX.Element;
|
|
21
|
+
export declare function GalleryEditor({ value, onChange, pickMediaFiles, layout, }: {
|
|
22
|
+
value: CuratedItem[];
|
|
23
|
+
onChange: (items: CuratedItem[]) => void;
|
|
24
|
+
pickMediaFiles?: (o: PickerOptions) => Promise<AssetFile[] | null>;
|
|
25
|
+
layout?: GalleryLayout;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
18
27
|
export declare function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, activeLanguage, defaultLanguage, }: {
|
|
19
28
|
page: PageModel;
|
|
20
29
|
onChange: (next: PageModel) => void;
|
|
@@ -30,3 +39,4 @@ export declare function PageEditor({ page, onChange, onRefresh, pristine, pickMe
|
|
|
30
39
|
activeLanguage?: string;
|
|
31
40
|
defaultLanguage?: string;
|
|
32
41
|
}): import("react").JSX.Element;
|
|
42
|
+
export {};
|
|
@@ -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";
|
|
@@ -101,7 +101,7 @@ function imageName(src) {
|
|
|
101
101
|
return raw;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
function GalleryCard({ item, verticalMasonry = false, sortable = true, activeId, overId, activeIndex, overIndex, onReplace, onToggle, onRemove, }) {
|
|
104
|
+
function GalleryCard({ item, verticalMasonry = false, sortable = true, activeId, overId, activeIndex, overIndex, onReplace, onEditAlt, onToggle, onRemove, }) {
|
|
105
105
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
106
106
|
id: item.src,
|
|
107
107
|
disabled: !sortable,
|
|
@@ -119,9 +119,9 @@ function GalleryCard({ item, verticalMasonry = false, sortable = true, activeId,
|
|
|
119
119
|
isDragging ? "is-dragging" : "",
|
|
120
120
|
insertBefore ? "is-insert-before" : "",
|
|
121
121
|
insertAfter ? "is-insert-after" : "",
|
|
122
|
-
].filter(Boolean).join(" "), style: sortable ? { transform: CSS.Transform.toString(transform), transition } : undefined, children: [_jsxs("div", { className: "pe-photo-frame", style: frameStyle, ...(sortable ? attributes : {}), ...(sortable ? listeners : {}), children: [_jsx("img", { src: item.src, alt: imageName(item.src), draggable: false }), item.hidden && _jsx("span", { className: "pe-hidden-label", children: "Nascosta" })] }), _jsxs("div", { className: "pe-photo-actions", children: [_jsx("button", { type: "button", onClick: onReplace, title: "Cambia immagine", "aria-label": "Cambia immagine", children: _jsx(Icon.replace, {}) }), _jsx("button", { type: "button", onClick: onToggle, title: item.hidden ? "Rimetti online" : "Nascondi dalla gallery", "aria-label": item.hidden ? "Rimetti online" : "Nascondi dalla gallery", children: item.hidden ? _jsx(Icon.eye, {}) : _jsx(Icon.eyeOff, {}) }), _jsx("button", { type: "button", className: "is-danger", onClick: onRemove, title: "Rimuovi dalla gallery", "aria-label": "Rimuovi dalla gallery", children: _jsx(Icon.trash, {}) })] }), _jsx("figcaption", { title: imageName(item.src), children: imageName(item.src) })] }));
|
|
122
|
+
].filter(Boolean).join(" "), style: sortable ? { transform: CSS.Transform.toString(transform), transition } : undefined, children: [_jsxs("div", { className: "pe-photo-frame", style: frameStyle, ...(sortable ? attributes : {}), ...(sortable ? listeners : {}), children: [_jsx("img", { src: item.src, alt: imageName(item.src), draggable: false }), item.hidden && _jsx("span", { className: "pe-hidden-label", children: "Nascosta" })] }), _jsxs("div", { className: "pe-photo-actions", children: [_jsx("button", { type: "button", onClick: onReplace, title: "Cambia immagine", "aria-label": "Cambia immagine", children: _jsx(Icon.replace, {}) }), _jsx("button", { type: "button", onClick: onToggle, title: item.hidden ? "Rimetti online" : "Nascondi dalla gallery", "aria-label": item.hidden ? "Rimetti online" : "Nascondi dalla gallery", children: item.hidden ? _jsx(Icon.eye, {}) : _jsx(Icon.eyeOff, {}) }), _jsx("button", { type: "button", className: "is-danger", onClick: onRemove, title: "Rimuovi dalla gallery", "aria-label": "Rimuovi dalla gallery", children: _jsx(Icon.trash, {}) })] }), _jsx("figcaption", { title: imageName(item.src), children: imageName(item.src) }), _jsxs("button", { type: "button", className: `pe-photo-alt${item.alt?.trim() ? " is-set" : ""}`, onClick: onEditAlt, "aria-label": `Modifica testo alternativo di ${imageName(item.src)}`, children: [_jsx("strong", { children: "ALT" }), _jsx("span", { children: item.alt?.trim() ? "Modifica" : "Vuoto" })] })] }));
|
|
123
123
|
}
|
|
124
|
-
function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
124
|
+
export function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
125
125
|
const list = Array.isArray(value) ? value : [];
|
|
126
126
|
const masonry = layout !== "grid";
|
|
127
127
|
const verticalMasonry = layout === "masonry-vertical";
|
|
@@ -129,6 +129,7 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
|
129
129
|
const hiddenItems = masonry ? list.filter((item) => item.hidden) : [];
|
|
130
130
|
const [activeId, setActiveId] = useState(null);
|
|
131
131
|
const [overId, setOverId] = useState(null);
|
|
132
|
+
const [editingAlt, setEditingAlt] = useState(null);
|
|
132
133
|
const [addPositionOpen, setAddPositionOpen] = useState(false);
|
|
133
134
|
const addPositionRef = useRef(null);
|
|
134
135
|
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
@@ -165,9 +166,15 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
|
165
166
|
if (!picked?.length)
|
|
166
167
|
return;
|
|
167
168
|
const have = new Set(list.map((item) => item.src));
|
|
169
|
+
const locale = site().languages?.default ?? "default";
|
|
168
170
|
const additions = picked
|
|
169
171
|
.filter((file) => !have.has(file.url))
|
|
170
|
-
.map((file) => ({
|
|
172
|
+
.map((file) => ({
|
|
173
|
+
src: file.url,
|
|
174
|
+
w: file.w ?? null,
|
|
175
|
+
h: file.h ?? null,
|
|
176
|
+
alt: assetAltText(file, locale),
|
|
177
|
+
}));
|
|
171
178
|
if (additions.length)
|
|
172
179
|
onChange(position === "start" ? [...additions, ...list] : [...list, ...additions]);
|
|
173
180
|
};
|
|
@@ -180,8 +187,9 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
|
180
187
|
alert("Questa foto è già presente nella gallery.");
|
|
181
188
|
return;
|
|
182
189
|
}
|
|
190
|
+
const locale = site().languages?.default ?? "default";
|
|
183
191
|
onChange(list.map((item, itemIndex) => itemIndex === index
|
|
184
|
-
? { ...item, src: file.url, w: file.w ?? null, h: file.h ?? null }
|
|
192
|
+
? { ...item, src: file.url, w: file.w ?? null, h: file.h ?? null, alt: assetAltText(file, locale) }
|
|
185
193
|
: item));
|
|
186
194
|
};
|
|
187
195
|
const finishDrag = ({ active, over }) => {
|
|
@@ -208,7 +216,22 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
|
208
216
|
const remove = (src) => onChange(list.filter((item) => item.src !== src));
|
|
209
217
|
return (_jsxs("section", { className: `pe-gallery${masonry ? ` is-${layout}` : ""}`, "aria-label": "Foto della gallery", children: [_jsxs("div", { className: "pe-gallery-bar", children: [_jsxs("div", { className: "pe-gallery-summary", children: [_jsxs("strong", { children: [list.length, " ", list.length === 1 ? "foto" : "foto"] }), _jsx("span", { children: list.length === 0
|
|
210
218
|
? "Scegli le immagini da mostrare nella pagina."
|
|
211
|
-
: `${visible} online${visible < list.length ? ` · ${list.length - visible} nascoste` : ""}` }), layout === "masonry-vertical" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry verticale \u00B7 3 colonne \u00B7 ordine dall\u2019alto verso il basso" })), layout === "masonry-horizontal" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry orizzontale \u00B7 6 colonne \u00B7 ordine da sinistra a destra" }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] })) : (_jsxs("div", { className: "pe-add-group", ref: addPositionRef, children: [_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] }), _jsx("button", { type: "button", className: "pe-add-position", "aria-label": "Scegli dove aggiungere le foto", "aria-expanded": addPositionOpen, "aria-controls": "gallery-add-position", onClick: () => setAddPositionOpen((open) => !open), children: "\u2304" }), addPositionOpen && (_jsx("div", { className: "pe-add-menu", id: "gallery-add-position", children: _jsx("button", { type: "button", onClick: () => void add("end"), children: "Aggiungi in fondo" }) }))] }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-gallery-empty", onClick: () => void add("start"), children: [_jsx(Icon.gallery, {}), _jsx("strong", { children: "La gallery \u00E8 vuota" }), _jsx("span", { children: "Aggiungi le prime fotografie" })] })) : (_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: ({ active }) => setActiveId(String(active.id)), onDragOver: ({ over }) => setOverId(over ? String(over.id) : null), onDragCancel: () => { setActiveId(null); setOverId(null); }, onDragEnd: finishDrag, children: [_jsx(SortableContext, { items: shownItems.map((item) => item.src), strategy: rectSortingStrategy, children: _jsx("div", { className: "pe-photo-grid", children: shownItems.map((item) => (_jsx(GalleryCard, { item: item, verticalMasonry: verticalMasonry, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) }) }), _jsx(DragOverlay, { children: activeItem && (_jsx("div", { className: "pe-photo-overlay", children: _jsx("img", { src: activeItem.src, alt: "" }) })) }), masonry && hiddenItems.length > 0 && (_jsxs("section", { className: "pe-hidden-gallery", "aria-label": "Foto nascoste", children: [_jsxs("div", { className: "pe-hidden-gallery-head", children: [_jsx("strong", { children: "Nascoste" }), _jsx("span", { children: "Non occupano spazio nella gallery pubblica." })] }), _jsx("div", { className: "pe-hidden-grid", children: hiddenItems.map((item) => (_jsx(GalleryCard, { item: item, sortable: false, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) })] }))] }))
|
|
219
|
+
: `${visible} online${visible < list.length ? ` · ${list.length - visible} nascoste` : ""}` }), layout === "masonry-vertical" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry verticale \u00B7 3 colonne \u00B7 ordine dall\u2019alto verso il basso" })), layout === "masonry-horizontal" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry orizzontale \u00B7 6 colonne \u00B7 ordine da sinistra a destra" }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] })) : (_jsxs("div", { className: "pe-add-group", ref: addPositionRef, children: [_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] }), _jsx("button", { type: "button", className: "pe-add-position", "aria-label": "Scegli dove aggiungere le foto", "aria-expanded": addPositionOpen, "aria-controls": "gallery-add-position", onClick: () => setAddPositionOpen((open) => !open), children: "\u2304" }), addPositionOpen && (_jsx("div", { className: "pe-add-menu", id: "gallery-add-position", children: _jsx("button", { type: "button", onClick: () => void add("end"), children: "Aggiungi in fondo" }) }))] }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-gallery-empty", onClick: () => void add("start"), children: [_jsx(Icon.gallery, {}), _jsx("strong", { children: "La gallery \u00E8 vuota" }), _jsx("span", { children: "Aggiungi le prime fotografie" })] })) : (_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: ({ active }) => setActiveId(String(active.id)), onDragOver: ({ over }) => setOverId(over ? String(over.id) : null), onDragCancel: () => { setActiveId(null); setOverId(null); }, onDragEnd: finishDrag, children: [_jsx(SortableContext, { items: shownItems.map((item) => item.src), strategy: rectSortingStrategy, children: _jsx("div", { className: "pe-photo-grid", children: shownItems.map((item) => (_jsx(GalleryCard, { item: item, verticalMasonry: verticalMasonry, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onEditAlt: () => setEditingAlt({ src: item.src, value: item.alt ?? "" }), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) }) }), _jsx(DragOverlay, { children: activeItem && (_jsx("div", { className: "pe-photo-overlay", children: _jsx("img", { src: activeItem.src, alt: "" }) })) }), masonry && hiddenItems.length > 0 && (_jsxs("section", { className: "pe-hidden-gallery", "aria-label": "Foto nascoste", children: [_jsxs("div", { className: "pe-hidden-gallery-head", children: [_jsx("strong", { children: "Nascoste" }), _jsx("span", { children: "Non occupano spazio nella gallery pubblica." })] }), _jsx("div", { className: "pe-hidden-grid", children: hiddenItems.map((item) => (_jsx(GalleryCard, { item: item, sortable: false, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onEditAlt: () => setEditingAlt({ src: item.src, value: item.alt ?? "" }), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) })] }))] })), editingAlt && (_jsx("div", { className: "pe-modal", role: "presentation", onMouseDown: (event) => {
|
|
220
|
+
if (event.target === event.currentTarget)
|
|
221
|
+
setEditingAlt(null);
|
|
222
|
+
}, children: _jsxs("div", { className: "pe-modal-card pe-alt-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "pe-alt-dialog", children: [_jsxs("header", { children: [_jsx("h2", { id: "pe-alt-dialog", children: "Testo alternativo" }), _jsx("button", { type: "button", onClick: () => setEditingAlt(null), "aria-label": "Chiudi", children: "\u00D7" })] }), _jsxs("div", { className: "pe-modal-fields", children: [_jsxs("div", { className: "pe-alt-preview", children: [_jsx("img", { src: editingAlt.src, alt: "" }), _jsx("span", { children: imageName(editingAlt.src) })] }), _jsxs("label", { children: [_jsx("span", { children: "Descrizione dell\u2019immagine" }), _jsx("input", { autoFocus: true, value: editingAlt.value, maxLength: 300, placeholder: "Es. Chef che impiatta un dessert al cioccolato", onChange: (event) => setEditingAlt({ ...editingAlt, value: event.target.value }), onKeyDown: (event) => {
|
|
223
|
+
if (event.key !== "Enter")
|
|
224
|
+
return;
|
|
225
|
+
onChange(list.map((item) => item.src === editingAlt.src
|
|
226
|
+
? { ...item, alt: editingAlt.value.trim() }
|
|
227
|
+
: item));
|
|
228
|
+
setEditingAlt(null);
|
|
229
|
+
} }), _jsx("small", { children: "Descrivi ci\u00F2 che \u00E8 utile nel contesto della pagina. Lascialo vuoto soltanto se la foto \u00E8 decorativa." })] })] }), _jsxs("footer", { children: [_jsx("button", { type: "button", className: "ak-btn", onClick: () => setEditingAlt(null), children: "Annulla" }), _jsx("button", { type: "button", className: "ak-btn ak-btn-primary", onClick: () => {
|
|
230
|
+
onChange(list.map((item) => item.src === editingAlt.src
|
|
231
|
+
? { ...item, alt: editingAlt.value.trim() }
|
|
232
|
+
: item));
|
|
233
|
+
setEditingAlt(null);
|
|
234
|
+
}, children: "Applica" })] })] }) }))] }));
|
|
212
235
|
}
|
|
213
236
|
export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, activeLanguage, defaultLanguage, }) {
|
|
214
237
|
const [bodyState, setBodyState] = useState("idle");
|
|
@@ -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;
|