@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 +15 -0
- package/dist/app/next.css +1 -0
- package/dist/app/routes/PageEditor.d.ts +10 -0
- package/dist/app/routes/PageEditor.js +20 -4
- package/dist/functions/_shared/d1-media.js +3 -3
- package/dist/media.d.ts +3 -0
- package/dist/media.js +6 -1
- package/package.json +1 -1
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,
|
|
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:
|
|
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:
|
|
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.
|
|
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",
|