@cardinali-studio/adminkit 2.2.22 → 2.2.24

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/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. */
@@ -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 {};
@@ -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 }));
@@ -215,7 +216,22 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
215
216
  const remove = (src) => onChange(list.filter((item) => item.src !== src));
216
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
217
218
  ? "Scegli le immagini da mostrare nella pagina."
218
- : `${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" })] })] }) }))] }));
219
235
  }
220
236
  export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, activeLanguage, defaultLanguage, }) {
221
237
  const [bodyState, setBodyState] = useState("idle");
@@ -2,10 +2,10 @@
2
2
  * validator remains the authority for every draft revision generated by an
3
3
  * explicit Media operation. Git is never consulted from this module. */
4
4
  import {
5
- canonicalChecksum, canonicalJson, contentSaveRequestChecksum, D1ContentRepository,
5
+ canonicalChecksum, contentSaveRequestChecksum, D1ContentRepository,
6
6
  } from "../../persistence.js";
7
7
  import {
8
- D1MediaOperationRepository, mediaPublicUrl, prepareMediaCopy, rewriteMediaValue,
8
+ canonicalR2Metadata, D1MediaOperationRepository, mediaPublicUrl, prepareMediaCopy, rewriteMediaValue,
9
9
  } from "../../media.js";
10
10
  import { validateContentDocument } from "../site.js";
11
11
  import { isKey, moveToTrash, sanitizeName } from "./assets.js";
@@ -137,7 +137,7 @@ async function inspectSource(bucket, entry) {
137
137
  ...entry,
138
138
  objectChecksum,
139
139
  etag: object.etag ?? null,
140
- metadataJson: canonicalJson({ httpMetadata: object.httpMetadata ?? {}, customMetadata: object.customMetadata ?? {} }),
140
+ metadataJson: canonicalR2Metadata(object.httpMetadata ?? {}, object.customMetadata ?? {}),
141
141
  };
142
142
  }
143
143
 
package/dist/media.d.ts CHANGED
@@ -177,6 +177,9 @@ export type MediaR2Bucket = {
177
177
  };
178
178
  export type MediaPreparationJournal = Pick<D1MediaOperationRepository, "recordPreparedEntry">;
179
179
  export type MediaFinalizationJournal = Pick<D1MediaOperationRepository, "beginFinalization" | "recordFinalizedEntry">;
180
+ /** Cloudflare may expose unset R2 metadata fields as enumerable `undefined`
181
+ * values. Persist the JSON-visible representation, not the runtime wrapper. */
182
+ export declare function canonicalR2Metadata(httpMetadata?: MediaR2Object["httpMetadata"], customMetadata?: MediaR2Object["customMetadata"]): string;
180
183
  /** Copy-first R2 effect. No existing destination is overwritten: an identical
181
184
  * prior copy is an idempotent retry, while any opaque collision stops safely. */
182
185
  export declare function prepareMediaCopy(input: {
package/dist/media.js CHANGED
@@ -449,6 +449,11 @@ export class D1MediaOperationRepository {
449
449
  }
450
450
  }
451
451
  const MEDIA_CHECKSUM_METADATA = "adminkit-sha256";
452
+ /** Cloudflare may expose unset R2 metadata fields as enumerable `undefined`
453
+ * values. Persist the JSON-visible representation, not the runtime wrapper. */
454
+ export function canonicalR2Metadata(httpMetadata = {}, customMetadata = {}) {
455
+ return canonicalJson(JSON.parse(JSON.stringify({ httpMetadata, customMetadata })));
456
+ }
452
457
  async function bytesChecksum(bytes) {
453
458
  const digest = await crypto.subtle.digest("SHA-256", bytes);
454
459
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -477,7 +482,7 @@ export async function prepareMediaCopy(input) {
477
482
  return input.journal.recordPreparedEntry({
478
483
  operationId: input.operationId, entryId: input.entryId, now: input.now, objectChecksum: checksum,
479
484
  etag: verified.etag ?? null,
480
- metadataJson: canonicalJson({ httpMetadata: source.httpMetadata ?? {}, customMetadata: source.customMetadata ?? {} }),
485
+ metadataJson: canonicalR2Metadata(source.httpMetadata ?? {}, source.customMetadata ?? {}),
481
486
  });
482
487
  }
483
488
  /** Cleanup starts only after the release is published. Failures are recorded as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardinali-studio/adminkit",
3
- "version": "2.2.22",
3
+ "version": "2.2.24",
4
4
  "type": "module",
5
5
  "description": "Motore riusabile di un pannello di amministrazione per siti Hugo su Cloudflare Pages: codec del front matter guidato dallo schema, form React, libreria media su R2. NIENTE di specifico di un sito qui dentro.",
6
6
  "license": "UNLICENSED",