@cardinali-studio/adminkit 2.2.16 → 2.2.18
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 +79 -1
- package/dist/app/routes/PageEditor.d.ts +2 -3
- package/dist/app/routes/PageEditor.js +64 -19
- package/dist/form/MediaBrowser.d.ts +3 -1
- package/dist/form/MediaBrowser.js +2 -2
- package/dist/form/MediaPicker.d.ts +7 -6
- package/dist/form/MediaPicker.js +2 -2
- package/dist/form/media/MediaGrid.d.ts +3 -1
- package/dist/form/media/MediaGrid.js +2 -2
- package/dist/functions/_shared/d1-media.js +3 -0
- package/dist/theme.css +5 -0
- package/package.json +1 -1
package/dist/app/app.css
CHANGED
|
@@ -641,9 +641,10 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
641
641
|
display: flex; align-items: center; justify-content: space-between; gap: 18px;
|
|
642
642
|
min-height: 48px; padding: 0 2px 18px; border-bottom: 1px solid var(--ak-line);
|
|
643
643
|
}
|
|
644
|
-
.pe-gallery-
|
|
644
|
+
.pe-gallery-summary { display: flex; flex-direction: column; gap: 4px; }
|
|
645
645
|
.pe-gallery-bar strong { font: 600 13px/1.2 var(--ak-font); }
|
|
646
646
|
.pe-gallery-bar span { color: var(--ak-ink-quiet); font: 400 11px/1.3 var(--ak-font); }
|
|
647
|
+
.pe-gallery-bar .pe-gallery-layout-note { color: var(--ak-ink); font-weight: 500; }
|
|
647
648
|
/* Stessa forma di `.gs-add`: sono due azioni primarie sulla STESSA schermata (aggiungi una
|
|
648
649
|
sezione, aggiungi delle foto) e vederle una contornata e una piena le fa sembrare di peso
|
|
649
650
|
diverso, che non è. La griglia delle foto per il resto non è stata ridisegnata. */
|
|
@@ -656,6 +657,24 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
656
657
|
}
|
|
657
658
|
.pe-add:hover { background: var(--ak-accent-hover); }
|
|
658
659
|
.pe-add:focus-visible { outline: 0; box-shadow: var(--ak-ring); }
|
|
660
|
+
.pe-add-group { position: relative; display: inline-flex; flex: none; }
|
|
661
|
+
.pe-add-group .pe-add { border-radius: 10px 0 0 10px; }
|
|
662
|
+
.pe-add-position {
|
|
663
|
+
width: 34px; border: 0; border-left: 1px solid rgba(255,255,255,.28); border-radius: 0 10px 10px 0;
|
|
664
|
+
background: var(--ak-accent); color: #fff; cursor: pointer; font: 700 16px/1 var(--ak-font);
|
|
665
|
+
transition: background-color .15s;
|
|
666
|
+
}
|
|
667
|
+
.pe-add-position:hover, .pe-add-position[aria-expanded="true"] { background: var(--ak-accent-hover); }
|
|
668
|
+
.pe-add-position:focus-visible { outline: 0; box-shadow: var(--ak-ring); position: relative; z-index: 1; }
|
|
669
|
+
.pe-add-menu {
|
|
670
|
+
position: absolute; top: calc(100% + 6px); right: 0; z-index: 3; min-width: 172px; padding: 5px;
|
|
671
|
+
border: 1px solid var(--ak-line); border-radius: 8px; background: var(--ak-panel); box-shadow: var(--ak-lift-3);
|
|
672
|
+
}
|
|
673
|
+
.pe-add-menu button {
|
|
674
|
+
width: 100%; border: 0; border-radius: 5px; background: transparent; color: var(--ak-ink); cursor: pointer;
|
|
675
|
+
padding: 8px 9px; text-align: left; font: 500 12px/1.2 var(--ak-font);
|
|
676
|
+
}
|
|
677
|
+
.pe-add-menu button:hover, .pe-add-menu button:focus-visible { background: var(--ak-hover); outline: 0; }
|
|
659
678
|
.pe-gallery-empty {
|
|
660
679
|
min-height: 300px; display: grid; place-content: center; justify-items: center; gap: 8px;
|
|
661
680
|
border: 1px dashed var(--ak-line); border-radius: 8px; background: var(--ak-panel);
|
|
@@ -725,6 +744,63 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
725
744
|
cursor: grabbing;
|
|
726
745
|
}
|
|
727
746
|
.pe-photo-overlay img { max-width: 100%; max-height: 100%; display: block; }
|
|
747
|
+
|
|
748
|
+
/* La modalità masonry replica il flusso del sito: le foto riempiono ogni colonna dall'alto
|
|
749
|
+
verso il basso e conservano il proprio rapporto d'aspetto. Le regole base qui sopra restano
|
|
750
|
+
quindi intatte per tutte le gallery con layout ordinario. */
|
|
751
|
+
.pe-gallery.is-masonry > .pe-photo-grid {
|
|
752
|
+
display: block; column-count: 3; column-gap: 1.5vw;
|
|
753
|
+
padding: 4px 2px 40px;
|
|
754
|
+
}
|
|
755
|
+
.pe-gallery.is-masonry > .pe-photo-grid > .pe-photo {
|
|
756
|
+
width: 100%; margin: 0 0 1.5vw; padding: 0;
|
|
757
|
+
display: inline-block; break-inside: avoid; -webkit-column-break-inside: avoid;
|
|
758
|
+
vertical-align: top;
|
|
759
|
+
}
|
|
760
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo-frame {
|
|
761
|
+
width: 100%; height: auto; display: block; overflow: hidden;
|
|
762
|
+
}
|
|
763
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo-frame img {
|
|
764
|
+
width: 100%; height: auto; max-width: none; max-height: none;
|
|
765
|
+
}
|
|
766
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo-actions {
|
|
767
|
+
top: 8px; transform: translate(-50%, 0);
|
|
768
|
+
}
|
|
769
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo:hover .pe-photo-actions,
|
|
770
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo:focus-within .pe-photo-actions {
|
|
771
|
+
transform: translate(-50%, -2px);
|
|
772
|
+
}
|
|
773
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo figcaption {
|
|
774
|
+
position: absolute; z-index: 2; right: 8px; bottom: 8px; left: 8px;
|
|
775
|
+
padding: 6px 8px; border-radius: 4px; background: rgba(20,20,22,.72);
|
|
776
|
+
color: #fff; text-align: left; opacity: 0; pointer-events: none;
|
|
777
|
+
transition: opacity .12s;
|
|
778
|
+
}
|
|
779
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo:hover figcaption,
|
|
780
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo:focus-within figcaption { opacity: 1; }
|
|
781
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-before::before,
|
|
782
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-after::after {
|
|
783
|
+
right: 0; left: 0; width: auto; height: 3px;
|
|
784
|
+
}
|
|
785
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-before::before {
|
|
786
|
+
top: -7px; bottom: auto;
|
|
787
|
+
}
|
|
788
|
+
.pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-after::after {
|
|
789
|
+
top: auto; bottom: -7px;
|
|
790
|
+
}
|
|
791
|
+
.pe-hidden-gallery {
|
|
792
|
+
display: flex; flex-direction: column; gap: 18px; padding: 24px 2px 8px;
|
|
793
|
+
border-top: 1px solid var(--ak-line);
|
|
794
|
+
}
|
|
795
|
+
.pe-hidden-gallery-head { display: flex; flex-direction: column; gap: 4px; }
|
|
796
|
+
.pe-hidden-gallery-head strong { font: 600 13px/1.2 var(--ak-font); }
|
|
797
|
+
.pe-hidden-gallery-head span { color: var(--ak-ink-quiet); font: 400 11px/1.3 var(--ak-font); }
|
|
798
|
+
.pe-hidden-grid {
|
|
799
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
|
800
|
+
gap: 28px 20px; align-items: start;
|
|
801
|
+
}
|
|
802
|
+
.pe-photo.is-static .pe-photo-frame { cursor: default; touch-action: auto; }
|
|
803
|
+
.pe-photo.is-static .pe-photo-frame:active { cursor: default; }
|
|
728
804
|
.pe-load-error {
|
|
729
805
|
margin: 0; padding: 9px 12px; border: 1px solid color-mix(in srgb,var(--ak-danger) 30%,var(--ak-line));
|
|
730
806
|
border-radius: 5px; color: var(--ak-danger); background: var(--ak-panel);
|
|
@@ -2054,6 +2130,8 @@ body:has(.uk) .pane-body { padding-bottom: 190px; }
|
|
|
2054
2130
|
}
|
|
2055
2131
|
|
|
2056
2132
|
@media (max-width: 900px) {
|
|
2133
|
+
.pe-gallery.is-masonry > .pe-photo-grid { column-count: 2; column-gap: 3vw; }
|
|
2134
|
+
.pe-gallery.is-masonry > .pe-photo-grid > .pe-photo { margin-bottom: 3vw; }
|
|
2057
2135
|
.settings-editor { padding-inline: 6px; gap: 34px; }
|
|
2058
2136
|
.st-row { grid-template-columns: minmax(0, 1fr); gap: 15px; padding: 22px 0; }
|
|
2059
2137
|
.seo-editor { padding-inline: 6px; }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PickerOptions } from "../../form/MediaPicker.js";
|
|
1
2
|
import type { AssetFile } from "../../index.js";
|
|
2
3
|
import type { PageModel } from "../lib/pages";
|
|
3
4
|
export declare function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, languages, activeLanguage, onLanguageChange, }: {
|
|
@@ -24,9 +25,7 @@ export declare function PageEditor({ page, onChange, onRefresh, pristine, pickMe
|
|
|
24
25
|
pickMedia?: (o: {
|
|
25
26
|
multiple: boolean;
|
|
26
27
|
}) => Promise<string[] | null>;
|
|
27
|
-
pickMediaFiles?: (o:
|
|
28
|
-
multiple: boolean;
|
|
29
|
-
}) => Promise<AssetFile[] | null>;
|
|
28
|
+
pickMediaFiles?: (o: PickerOptions) => Promise<AssetFile[] | null>;
|
|
30
29
|
/** La lingua è scelta nella testata della pagina, non è un'impostazione globale del pannello. */
|
|
31
30
|
activeLanguage?: string;
|
|
32
31
|
defaultLanguage?: string;
|
|
@@ -100,30 +100,65 @@ function imageName(src) {
|
|
|
100
100
|
return raw;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
function GalleryCard({ item, activeId, overId, activeIndex, overIndex, onReplace, onToggle, onRemove, }) {
|
|
104
|
-
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
105
|
-
|
|
103
|
+
function GalleryCard({ item, masonry = false, sortable = true, activeId, overId, activeIndex, overIndex, onReplace, onToggle, onRemove, }) {
|
|
104
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
105
|
+
id: item.src,
|
|
106
|
+
disabled: !sortable,
|
|
107
|
+
});
|
|
108
|
+
const isTarget = sortable && overId === item.src && activeId !== item.src;
|
|
106
109
|
const insertAfter = isTarget && activeIndex < overIndex;
|
|
107
110
|
const insertBefore = isTarget && activeIndex > overIndex;
|
|
111
|
+
const frameStyle = masonry && item.w && item.h
|
|
112
|
+
? { aspectRatio: `${item.w} / ${item.h}` }
|
|
113
|
+
: undefined;
|
|
108
114
|
return (_jsxs("figure", { ref: setNodeRef, className: [
|
|
109
115
|
"pe-photo",
|
|
116
|
+
sortable ? "" : "is-static",
|
|
110
117
|
item.hidden ? "is-hidden" : "",
|
|
111
118
|
isDragging ? "is-dragging" : "",
|
|
112
119
|
insertBefore ? "is-insert-before" : "",
|
|
113
120
|
insertAfter ? "is-insert-after" : "",
|
|
114
|
-
].filter(Boolean).join(" "), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("div", { className: "pe-photo-frame", ...attributes, ...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) })] }));
|
|
121
|
+
].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) })] }));
|
|
115
122
|
}
|
|
116
|
-
function GalleryEditor({ value, onChange, pickMediaFiles, }) {
|
|
123
|
+
function GalleryEditor({ value, onChange, pickMediaFiles, masonry = false, }) {
|
|
117
124
|
const list = Array.isArray(value) ? value : [];
|
|
125
|
+
const shownItems = masonry ? list.filter((item) => !item.hidden) : list;
|
|
126
|
+
const hiddenItems = masonry ? list.filter((item) => item.hidden) : [];
|
|
118
127
|
const [activeId, setActiveId] = useState(null);
|
|
119
128
|
const [overId, setOverId] = useState(null);
|
|
129
|
+
const [addPositionOpen, setAddPositionOpen] = useState(false);
|
|
130
|
+
const addPositionRef = useRef(null);
|
|
120
131
|
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
121
|
-
const activeIndex = activeId ?
|
|
122
|
-
const overIndex = overId ?
|
|
123
|
-
const activeItem = activeIndex >= 0 ?
|
|
132
|
+
const activeIndex = activeId ? shownItems.findIndex((item) => item.src === activeId) : -1;
|
|
133
|
+
const overIndex = overId ? shownItems.findIndex((item) => item.src === overId) : -1;
|
|
134
|
+
const activeItem = activeIndex >= 0 ? shownItems[activeIndex] : null;
|
|
124
135
|
const visible = list.filter((item) => !item.hidden).length;
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!addPositionOpen)
|
|
138
|
+
return;
|
|
139
|
+
const close = (event) => {
|
|
140
|
+
if (!addPositionRef.current?.contains(event.target))
|
|
141
|
+
setAddPositionOpen(false);
|
|
142
|
+
};
|
|
143
|
+
const closeOnEscape = (event) => {
|
|
144
|
+
if (event.key === "Escape")
|
|
145
|
+
setAddPositionOpen(false);
|
|
146
|
+
};
|
|
147
|
+
addEventListener("pointerdown", close);
|
|
148
|
+
addEventListener("keydown", closeOnEscape);
|
|
149
|
+
return () => {
|
|
150
|
+
removeEventListener("pointerdown", close);
|
|
151
|
+
removeEventListener("keydown", closeOnEscape);
|
|
152
|
+
};
|
|
153
|
+
}, [addPositionOpen]);
|
|
154
|
+
const add = async (position) => {
|
|
155
|
+
setAddPositionOpen(false);
|
|
156
|
+
const picked = await pickMediaFiles?.({
|
|
157
|
+
multiple: true,
|
|
158
|
+
notice: position === "start"
|
|
159
|
+
? "Le foto selezionate verranno aggiunte in cima alla gallery."
|
|
160
|
+
: "Le foto selezionate verranno aggiunte in fondo alla gallery.",
|
|
161
|
+
});
|
|
127
162
|
if (!picked?.length)
|
|
128
163
|
return;
|
|
129
164
|
const have = new Set(list.map((item) => item.src));
|
|
@@ -131,7 +166,7 @@ function GalleryEditor({ value, onChange, pickMediaFiles, }) {
|
|
|
131
166
|
.filter((file) => !have.has(file.url))
|
|
132
167
|
.map((file) => ({ src: file.url, w: file.w ?? null, h: file.h ?? null }));
|
|
133
168
|
if (additions.length)
|
|
134
|
-
onChange([...list, ...additions]);
|
|
169
|
+
onChange(position === "start" ? [...additions, ...list] : [...list, ...additions]);
|
|
135
170
|
};
|
|
136
171
|
const replace = async (index) => {
|
|
137
172
|
const picked = await pickMediaFiles?.({ multiple: false });
|
|
@@ -151,16 +186,26 @@ function GalleryEditor({ value, onChange, pickMediaFiles, }) {
|
|
|
151
186
|
setOverId(null);
|
|
152
187
|
if (!over || active.id === over.id)
|
|
153
188
|
return;
|
|
154
|
-
const from =
|
|
155
|
-
const to =
|
|
156
|
-
if (from
|
|
189
|
+
const from = shownItems.findIndex((item) => item.src === active.id);
|
|
190
|
+
const to = shownItems.findIndex((item) => item.src === over.id);
|
|
191
|
+
if (from < 0 || to < 0)
|
|
192
|
+
return;
|
|
193
|
+
if (!masonry) {
|
|
157
194
|
onChange(arrayMove(list, from, to));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const reordered = arrayMove(shownItems, from, to);
|
|
198
|
+
let shownIndex = 0;
|
|
199
|
+
onChange(list.map((item) => item.hidden ? item : reordered[shownIndex++]));
|
|
158
200
|
};
|
|
159
|
-
|
|
201
|
+
const itemIndex = (src) => list.findIndex((item) => item.src === src);
|
|
202
|
+
const toggle = (src) => onChange(list.map((item) => item.src === src
|
|
203
|
+
? { ...item, hidden: item.hidden ? undefined : true }
|
|
204
|
+
: item));
|
|
205
|
+
const remove = (src) => onChange(list.filter((item) => item.src !== src));
|
|
206
|
+
return (_jsxs("section", { className: `pe-gallery${masonry ? " is-masonry" : ""}`, "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
|
|
160
207
|
? "Scegli le immagini da mostrare nella pagina."
|
|
161
|
-
: `${visible} online${visible < list.length ? ` · ${list.length - visible} nascoste` : ""}` })] }), _jsxs("button", { type: "button", className: "pe-add", onClick: () => void add(), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] })] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-gallery-empty", onClick: () => void add(), 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:
|
|
162
|
-
? { ...candidate, hidden: candidate.hidden ? undefined : true }
|
|
163
|
-
: candidate)), onRemove: () => onChange(list.filter((_, itemIndex) => itemIndex !== index)) }, item.src))) }) }), _jsx(DragOverlay, { children: activeItem && (_jsx("div", { className: "pe-photo-overlay", children: _jsx("img", { src: activeItem.src, alt: "" }) })) })] }))] }));
|
|
208
|
+
: `${visible} online${visible < list.length ? ` · ${list.length - visible} nascoste` : ""}` }), masonry && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry \u00B7 3 colonne \u00B7 ordine dall\u2019alto verso il basso" }))] }), 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 in cima ", _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, masonry: masonry, 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))) })] }))] }))] }));
|
|
164
209
|
}
|
|
165
210
|
export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, activeLanguage, defaultLanguage, }) {
|
|
166
211
|
const [bodyState, setBodyState] = useState("idle");
|
|
@@ -297,7 +342,7 @@ export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pic
|
|
|
297
342
|
(`sections[].images`) e dove la griglia esce sul sito. Non c'è più un pannello staccato in
|
|
298
343
|
fondo — c'era finché le foto stavano in un campo della pagina, e insieme a lui è sparito
|
|
299
344
|
il caso in cui la stessa griglia si vedeva due volte. */
|
|
300
|
-
return (_jsxs("div", { className: "ed ed-gallery", children: [bodyState === "error" && (_jsx("p", { className: "pe-load-error", children: "Non sono riuscito ad aggiornare i dati della pagina dal sito." })), _jsx(GallerySectionsEditor, { value: page.sections, onChange: (sections) => onChange({ ...page, sections }), renderGallery: (images, onImages) => (_jsx(GalleryEditor, { value: images, onChange: onImages, pickMediaFiles: pickMediaFiles })), pickMediaFiles: pickMediaFiles })] }));
|
|
345
|
+
return (_jsxs("div", { className: "ed ed-gallery", children: [bodyState === "error" && (_jsx("p", { className: "pe-load-error", children: "Non sono riuscito ad aggiornare i dati della pagina dal sito." })), _jsx(GallerySectionsEditor, { value: page.sections, onChange: (sections) => onChange({ ...page, sections }), renderGallery: (images, onImages) => (_jsx(GalleryEditor, { value: images, onChange: onImages, pickMediaFiles: pickMediaFiles, masonry: page.galleryLayout === "masonry" })), pickMediaFiles: pickMediaFiles })] }));
|
|
301
346
|
}
|
|
302
347
|
return (_jsxs("div", { className: "ed", children: [translationActive && (_jsx("p", { className: "pe-translation-note", children: page.translations?.[activeLanguage]?.exists || page._translations?.[activeLanguage]
|
|
303
348
|
? `Stai modificando ${activeLanguage.toUpperCase()}. I campi non traducibili restano visibili e vengono copiati dalla lingua predefinita al salvataggio.`
|
|
@@ -3,10 +3,12 @@ export interface MediaBrowserProps {
|
|
|
3
3
|
/** "manage" = tutto (upload, sposta, cancella). "pick" = solo scelta. */
|
|
4
4
|
mode: "manage" | "pick";
|
|
5
5
|
multiple?: boolean;
|
|
6
|
+
/** Spiega l'effetto della scelta senza attribuirlo alla libreria media. */
|
|
7
|
+
notice?: string;
|
|
6
8
|
onPick?: (files: AssetFile[]) => void;
|
|
7
9
|
onCancel?: () => void;
|
|
8
10
|
}
|
|
9
|
-
export declare function MediaBrowser({ mode, multiple, onPick, onCancel }: MediaBrowserProps): import("react").JSX.Element;
|
|
11
|
+
export declare function MediaBrowser({ mode, multiple, notice, onPick, onCancel }: MediaBrowserProps): import("react").JSX.Element;
|
|
10
12
|
export { useMediaLibrary } from "./media/useMediaLibrary.js";
|
|
11
13
|
export type { MediaLibrary, SortKey } from "./media/useMediaLibrary.js";
|
|
12
14
|
export { MediaFolders } from "./media/MediaFolders.js";
|
|
@@ -14,10 +14,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
import { useMediaLibrary } from "./media/useMediaLibrary.js";
|
|
15
15
|
import { MediaFolders } from "./media/MediaFolders.js";
|
|
16
16
|
import { MediaGrid } from "./media/MediaGrid.js";
|
|
17
|
-
export function MediaBrowser({ mode, multiple = true, onPick, onCancel }) {
|
|
17
|
+
export function MediaBrowser({ mode, multiple = true, notice, onPick, onCancel }) {
|
|
18
18
|
// `remember`: il modale riapre nella cartella dell'ultima volta (vedi useMediaLibrary).
|
|
19
19
|
const lib = useMediaLibrary({ mode, multiple, remember: mode === "pick" });
|
|
20
|
-
return (_jsxs("div", { className: "mb mb-" + mode, children: [_jsx("aside", { className: "mb-side", children: _jsx(MediaFolders, { lib: lib }) }), _jsx("section", { className: "mb-main", children: _jsx(MediaGrid, { lib: lib, onPick: onPick, onCancel: onCancel }) })] }));
|
|
20
|
+
return (_jsxs("div", { className: "mb mb-" + mode, children: [_jsx("aside", { className: "mb-side", children: _jsx(MediaFolders, { lib: lib }) }), _jsx("section", { className: "mb-main", children: _jsx(MediaGrid, { lib: lib, notice: notice, onPick: onPick, onCancel: onCancel }) })] }));
|
|
21
21
|
}
|
|
22
22
|
export { useMediaLibrary } from "./media/useMediaLibrary.js";
|
|
23
23
|
export { MediaFolders } from "./media/MediaFolders.js";
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { AssetFile } from "../api.js";
|
|
2
|
+
export interface PickerOptions {
|
|
3
|
+
multiple: boolean;
|
|
4
|
+
/** Contesto dell'azione che ha aperto il picker, visibile durante la scelta. */
|
|
5
|
+
notice?: string;
|
|
6
|
+
}
|
|
2
7
|
export declare function usePicker(): {
|
|
3
|
-
pickMedia: (opts:
|
|
4
|
-
|
|
5
|
-
}) => Promise<string[] | null>;
|
|
6
|
-
pickMediaFiles: (opts: {
|
|
7
|
-
multiple: boolean;
|
|
8
|
-
}) => Promise<AssetFile[] | null>;
|
|
8
|
+
pickMedia: (opts: PickerOptions) => Promise<string[] | null>;
|
|
9
|
+
pickMediaFiles: (opts: PickerOptions) => Promise<AssetFile[] | null>;
|
|
9
10
|
pickerElement: import("react").JSX.Element | null;
|
|
10
11
|
};
|
package/dist/form/MediaPicker.js
CHANGED
|
@@ -10,11 +10,11 @@ export function usePicker() {
|
|
|
10
10
|
* e serve a chi costruisce una gallery; `pickMedia` dà i soli percorsi, che è tutto ciò che
|
|
11
11
|
* vogliono sapere r2Image e r2Gallery. Tenere il caso semplice semplice evita che ogni widget
|
|
12
12
|
* debba conoscere la forma di un AssetFile per salvare una stringa. */
|
|
13
|
-
const pickMediaFiles = useCallback((opts) => new Promise((resolve) => setReq({
|
|
13
|
+
const pickMediaFiles = useCallback((opts) => new Promise((resolve) => setReq({ ...opts, resolve })), []);
|
|
14
14
|
const pickMedia = useCallback((opts) => pickMediaFiles(opts).then((fs) => (fs ? fs.map((f) => f.url) : null)), [pickMediaFiles]);
|
|
15
15
|
const element = req ? (_jsx("div", { className: "mb-modal", onClick: (e) => { if (e.target === e.currentTarget) {
|
|
16
16
|
req.resolve(null);
|
|
17
17
|
setReq(null);
|
|
18
|
-
} }, children: _jsx("div", { className: "mb-modal-panel", children: _jsx(MediaBrowser, { mode: "pick", multiple: req.multiple, onPick: (files) => { req.resolve(files); setReq(null); }, onCancel: () => { req.resolve(null); setReq(null); } }) }) })) : null;
|
|
18
|
+
} }, children: _jsx("div", { className: "mb-modal-panel", children: _jsx(MediaBrowser, { mode: "pick", multiple: req.multiple, notice: req.notice, onPick: (files) => { req.resolve(files); setReq(null); }, onCancel: () => { req.resolve(null); setReq(null); } }) }) })) : null;
|
|
19
19
|
return { pickMedia, pickMediaFiles, pickerElement: element };
|
|
20
20
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AssetFile } from "../../api.js";
|
|
2
2
|
import { type MediaLibrary } from "./useMediaLibrary.js";
|
|
3
|
-
export declare function MediaGrid({ lib, onPick, onCancel }: {
|
|
3
|
+
export declare function MediaGrid({ lib, notice, onPick, onCancel }: {
|
|
4
4
|
lib: MediaLibrary;
|
|
5
|
+
/** Solo nel picker: ricorda l'effetto della conferma fuori dalla libreria media. */
|
|
6
|
+
notice?: string;
|
|
5
7
|
/** Solo in modalità "pick": chi ha aperto il modale riceve i file scelti. */
|
|
6
8
|
onPick?: (files: AssetFile[]) => void;
|
|
7
9
|
onCancel?: () => void;
|
|
@@ -115,7 +115,7 @@ function FolderCell({ lib, folder, manage, onExternalDrop }) {
|
|
|
115
115
|
}, children: [_jsxs("div", { className: "mg-frame", onClick: () => { if (!locked)
|
|
116
116
|
lib.go(folder.prefix); }, children: [_jsx("svg", { className: "mg-folder-ico", viewBox: "0 0 48 40", "aria-hidden": "true", children: _jsx("path", { d: "M2 8a4 4 0 0 1 4-4h12l4 5h20a4 4 0 0 1 4 4v21a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8Z" }) }), manage && (_jsx("button", { className: "mg-check", "aria-label": on ? "Deseleziona" : "Seleziona", disabled: locked, onClick: (e) => { e.stopPropagation(); lib.toggle(folder.prefix); }, children: on ? "✓" : "" }))] }), _jsx("figcaption", { className: "mg-cap", title: name, children: name }), manage && (_jsx("span", { className: "mg-folder-menu", children: _jsx(FolderMenu, { lib: lib, prefix: folder.prefix, label: name, openTo: "auto" }) }))] }));
|
|
117
117
|
}
|
|
118
|
-
export function MediaGrid({ lib, onPick, onCancel }) {
|
|
118
|
+
export function MediaGrid({ lib, notice, onPick, onCancel }) {
|
|
119
119
|
const manage = lib.mode === "manage";
|
|
120
120
|
const sel = lib.selected;
|
|
121
121
|
const allOn = lib.allSelected;
|
|
@@ -153,7 +153,7 @@ export function MediaGrid({ lib, onPick, onCancel }) {
|
|
|
153
153
|
? _jsxs("span", { children: ["Rilascia per caricare in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella" })] })
|
|
154
154
|
: _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
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" })] })) }), 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 === "completed_with_warning" && operation.releaseId && (_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) => {
|
|
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 === "completed_with_warning" && operation.releaseId && (_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
157
|
const on = sel.has(f.key);
|
|
158
158
|
return (_jsxs("figure", { className: "mg-item" + (on ? " is-on" : ""), draggable: manage && !locked, onDragStart: (e) => {
|
|
159
159
|
if (locked) {
|
|
@@ -123,6 +123,9 @@ async function rewriteDocuments({ db, data, replacements = {}, removals = [] })
|
|
|
123
123
|
schemaVersion: row.schemaVersion, tombstone: false, payload: normalized.payload,
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
|
+
/* Nessun riferimento è un esito valido: l'operazione Media deve proseguire
|
|
127
|
+
* senza costruire una transazione editoriale vuota, che `saveMany` rifiuta. */
|
|
128
|
+
if (!commands.length) return { documents: [] };
|
|
126
129
|
const who = editor(data);
|
|
127
130
|
const repository = new D1ContentRepository(db);
|
|
128
131
|
/* Every command was fully parsed and site-validated before the first write.
|
package/dist/theme.css
CHANGED
|
@@ -375,6 +375,11 @@ a.ak-btn { text-decoration: none; display: inline-flex; align-items: center; }
|
|
|
375
375
|
background: var(--ak-panel); border: 1px solid var(--ak-accent);
|
|
376
376
|
font: 500 var(--ak-fs-small)/1.4 var(--ak-font); color: var(--ak-ink);
|
|
377
377
|
}
|
|
378
|
+
.mg-picker-notice {
|
|
379
|
+
margin: 0; padding: 8px 12px; border-radius: var(--ak-radius);
|
|
380
|
+
background: var(--ak-panel); border: 1px solid var(--ak-line);
|
|
381
|
+
color: var(--ak-ink); font: 500 var(--ak-fs-small)/1.4 var(--ak-font);
|
|
382
|
+
}
|
|
378
383
|
.mg-journal { display: flex; flex-direction: column; gap: 6px; }
|
|
379
384
|
.mg-journal-row {
|
|
380
385
|
display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardinali-studio/adminkit",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.18",
|
|
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",
|