@cardinali-studio/adminkit 2.2.15 → 2.2.17
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.js +1 -1
- 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/MediaFolders.js +25 -12
- package/dist/form/media/MediaGrid.d.ts +3 -1
- package/dist/form/media/MediaGrid.js +26 -9
- package/dist/form/media/UploadDialog.d.ts +7 -0
- package/dist/form/media/UploadDialog.js +47 -8
- package/dist/form/media/media-upload-queue.d.ts +5 -0
- package/dist/form/media/media-upload-queue.js +3 -3
- package/dist/form/media/useMediaLibrary.d.ts +15 -4
- package/dist/form/media/useMediaLibrary.js +54 -18
- package/dist/theme.css +15 -0
- package/package.json +1 -1
package/dist/app/App.js
CHANGED
|
@@ -244,7 +244,7 @@ export function App() {
|
|
|
244
244
|
: deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
|
|
245
245
|
: _jsx(DeliveriesList, { rows: deliveries.rows, current: activeDeliveryId || quickSelectedId, onOpen: (id) => { setQuickSelectedId(undefined); go(`/deliveries/${id}`); } }) }), _jsxs("div", { className: "dl-col-foot", children: [deliveries.rows && _jsx(DeliveriesTotal, { rows: deliveries.rows }), _jsxs("button", { className: "col-item" + (deliverySettingsOpen ? " is-on" : ""), onClick: () => { setQuickSelectedId(undefined); go("/deliveries/settings"); }, children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] })), section === "bookings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: bookings.rows ? `${bookings.rows.length} appuntamenti` : "Prenotazioni" }) }), _jsx("div", { className: "col-body", children: bookings.error ? _jsx("p", { className: "ak-error", children: bookings.error })
|
|
246
246
|
: !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
247
|
-
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.
|
|
247
|
+
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
|
|
248
248
|
/* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
|
|
249
249
|
rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
|
|
250
250
|
il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
|
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
|
}
|
|
@@ -12,9 +12,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* stesso giro. Il bersaglio si illumina mentre ci passi sopra: senza, non si capisce se il drop
|
|
13
13
|
* sta per funzionare, e uno spostamento andato a vuoto sembra un bug.
|
|
14
14
|
*
|
|
15
|
-
* «
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* «Senza cartella» accetta un file esterno come destinazione ESPLICITA di upload; per gli
|
|
16
|
+
* spostamenti interni continua ad accettare solo CARTELLE, perché la Function rifiuta una
|
|
17
|
+
* destinazione vuota per un file esistente.
|
|
18
18
|
*/
|
|
19
19
|
import { useEffect, useState } from "react";
|
|
20
20
|
export const DRAG_TYPE = "application/x-adminkit-keys";
|
|
@@ -70,7 +70,8 @@ const IcoSettings = () => (_jsxs("svg", { className: "mf-ico", viewBox: "0 0 16
|
|
|
70
70
|
export function MediaFolders({ lib, trash, settings }) {
|
|
71
71
|
const [over, setOver] = useState(null);
|
|
72
72
|
const [draggingFolder, setDraggingFolder] = useState(false);
|
|
73
|
-
const
|
|
73
|
+
const canMove = lib.mode === "manage" && !lib.busy && !lib.mutating;
|
|
74
|
+
const canUpload = !lib.busy && !lib.mutating;
|
|
74
75
|
/* Nel cestino non si è in nessuna cartella: senza questo, «Tutte» resterebbe accesa insieme a
|
|
75
76
|
«Cestino» e la colonna indicherebbe due posti nello stesso momento. */
|
|
76
77
|
const here = (p) => !trash?.active && (p === "" ? lib.prefix === "" : lib.prefix.startsWith(p));
|
|
@@ -80,19 +81,34 @@ export function MediaFolders({ lib, trash, settings }) {
|
|
|
80
81
|
trash.leave(); lib.go(p); };
|
|
81
82
|
const drop = (dest) => (e) => {
|
|
82
83
|
setOver(null);
|
|
83
|
-
if (
|
|
84
|
+
if (canUpload && e.dataTransfer.files.length) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
if (lib.mode === "pick")
|
|
87
|
+
lib.openUpload(e.dataTransfer.files, dest);
|
|
88
|
+
else
|
|
89
|
+
void lib.upload(e.dataTransfer.files, dest);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!canMove)
|
|
84
93
|
return;
|
|
85
94
|
e.preventDefault();
|
|
86
95
|
const ids = readDragged(e.dataTransfer);
|
|
87
96
|
if (ids.length)
|
|
88
97
|
void lib.move(ids, dest);
|
|
89
98
|
};
|
|
99
|
+
const dragOver = (dest) => (e) => {
|
|
100
|
+
const files = e.dataTransfer.types.includes("Files");
|
|
101
|
+
if ((files && canUpload) || (!files && canMove)) {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
setOver(dest);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
90
106
|
/* Vero mentre si trascina una cartella, ovunque nella pagina. Serve a far comparire il
|
|
91
107
|
bersaglio del primo livello solo in quel momento: una riga fissa «porta qui» sarebbe rumore
|
|
92
108
|
per il 99% del tempo, e prima quel bersaglio non esisteva proprio — la sola via era «Tutte»,
|
|
93
109
|
che nessuno poteva indovinare. */
|
|
94
110
|
useEffect(() => {
|
|
95
|
-
if (!
|
|
111
|
+
if (!canMove)
|
|
96
112
|
return;
|
|
97
113
|
const start = (e) => setDraggingFolder(!!e.dataTransfer?.types.includes(DRAG_FOLDERS));
|
|
98
114
|
const end = () => setDraggingFolder(false);
|
|
@@ -104,14 +120,11 @@ export function MediaFolders({ lib, trash, settings }) {
|
|
|
104
120
|
removeEventListener("dragend", end);
|
|
105
121
|
removeEventListener("drop", end);
|
|
106
122
|
};
|
|
107
|
-
}, [
|
|
108
|
-
return (_jsxs("div", { className: "mf", children: [_jsx("ul", { className: "mf-list", children: _jsx("li", { className: "mf-item mf-loose" + (here("") ? " is-on" : ""), children: _jsx("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(""), children: _jsx("span", { className: "mf-name", children: "Senza cartella" }) }) }) }), _jsx("ul", { className: "mf-list", children: lib.folders.map((f) => (_jsxs("li", {
|
|
123
|
+
}, [canMove]);
|
|
124
|
+
return (_jsxs("div", { className: "mf", children: [_jsx("ul", { className: "mf-list", children: _jsx("li", { className: "mf-item mf-loose" + (here("") ? " is-on" : "") + (over === "" ? " is-over" : ""), onDragOver: dragOver(""), onDragLeave: () => setOver((p) => (p === "" ? null : p)), onDrop: drop(""), children: _jsx("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(""), children: _jsx("span", { className: "mf-name", children: "Senza cartella" }) }) }) }), _jsx("ul", { className: "mf-list", children: lib.folders.map((f) => (_jsxs("li", {
|
|
109
125
|
/* Accesa anche quando si è DENTRO una sua sottocartella: la colonna risponde a
|
|
110
126
|
«dove sono», e stando in food/piatti-2024/ la risposta è ancora food. */
|
|
111
127
|
className: "mf-item"
|
|
112
128
|
+ (here(f.prefix) ? " is-on" : "")
|
|
113
|
-
+ (over === f.prefix ? " is-over" : ""), onDragOver: (e) => {
|
|
114
|
-
e.preventDefault();
|
|
115
|
-
setOver(f.prefix);
|
|
116
|
-
} }, onDragLeave: () => setOver((p) => (p === f.prefix ? null : p)), onDrop: drop(f.prefix), children: [_jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(f.prefix), children: [_jsx("span", { className: "mf-name", children: f.label || f.name }), f.count != null && _jsx("span", { className: "mf-count", children: f.count })] }), canDrop && _jsx(FolderMenu, { lib: lib, prefix: f.prefix, label: f.label || f.name })] }, f.prefix))) }), draggingFolder && (_jsx("div", { className: "mf-root-drop" + (over === "__root" ? " is-over" : ""), onDragOver: (e) => { e.preventDefault(); setOver("__root"); }, onDragLeave: () => setOver((p) => (p === "__root" ? null : p)), onDrop: drop(""), children: "Porta al primo livello" })), canDrop && (_jsx("button", { className: "ak-btn ak-btn-quiet mf-new", disabled: lib.busy || lib.mutating, onClick: () => void lib.newFolder(""), children: "+ Cartella" })), trash && (_jsx("ul", { className: "mf-list mf-tail", children: _jsx("li", { className: "mf-item" + (trash.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: trash.open, children: [_jsx(IcoTrash, {}), _jsx("span", { className: "mf-name", children: "Cestino" })] }) }) })), settings && (_jsx("ul", { className: "mf-list mf-settings-tail", children: _jsx("li", { className: "mf-item" + (settings.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: settings.open, children: [_jsx(IcoSettings, {}), _jsx("span", { className: "mf-name", children: "Impostazioni" })] }) }) }))] }));
|
|
129
|
+
+ (over === f.prefix ? " is-over" : ""), onDragOver: dragOver(f.prefix), onDragLeave: () => setOver((p) => (p === f.prefix ? null : p)), onDrop: drop(f.prefix), children: [_jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(f.prefix), children: [_jsx("span", { className: "mf-name", children: f.label || f.name }), f.count != null && _jsx("span", { className: "mf-count", children: f.count })] }), canMove && _jsx(FolderMenu, { lib: lib, prefix: f.prefix, label: f.label || f.name })] }, f.prefix))) }), draggingFolder && (_jsx("div", { className: "mf-root-drop" + (over === "__root" ? " is-over" : ""), onDragOver: (e) => { e.preventDefault(); setOver("__root"); }, onDragLeave: () => setOver((p) => (p === "__root" ? null : p)), onDrop: drop(""), children: "Porta al primo livello" })), canMove && (_jsx("button", { className: "ak-btn ak-btn-quiet mf-new", disabled: lib.busy || lib.mutating, onClick: () => void lib.newFolder(""), children: "+ Cartella" })), trash && (_jsx("ul", { className: "mf-list mf-tail", children: _jsx("li", { className: "mf-item" + (trash.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: trash.open, children: [_jsx(IcoTrash, {}), _jsx("span", { className: "mf-name", children: "Cestino" })] }) }) })), settings && (_jsx("ul", { className: "mf-list mf-settings-tail", children: _jsx("li", { className: "mf-item" + (settings.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: settings.open, children: [_jsx(IcoSettings, {}), _jsx("span", { className: "mf-name", children: "Impostazioni" })] }) }) }))] }));
|
|
117
130
|
}
|
|
@@ -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;
|
|
@@ -58,7 +58,7 @@ function MoveMenu({ lib, keys }) {
|
|
|
58
58
|
* seleziona insieme alle foto, e si trascina — dentro un'altra cartella qui, o sulla colonna di
|
|
59
59
|
* sinistra per riportarla al primo livello. Ed è anche un BERSAGLIO: ci si lasciano cadere foto
|
|
60
60
|
* o altre cartelle. */
|
|
61
|
-
function FolderCell({ lib, folder, manage }) {
|
|
61
|
+
function FolderCell({ lib, folder, manage, onExternalDrop }) {
|
|
62
62
|
const [over, setOver] = useState(false);
|
|
63
63
|
const on = lib.selected.has(folder.prefix);
|
|
64
64
|
const name = folder.label || folder.name;
|
|
@@ -84,6 +84,11 @@ function FolderCell({ lib, folder, manage }) {
|
|
|
84
84
|
"è roba nostra": il tipo dei nostri dati non è leggibile in ogni passaggio del trascinamento,
|
|
85
85
|
e un bersaglio che non si accende sembra un bersaglio che non funziona. */
|
|
86
86
|
onDragOver: (e) => {
|
|
87
|
+
if (!manage && !locked && e.dataTransfer.types.includes("Files")) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
setOver(true);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
87
92
|
if (!manage || locked || e.dataTransfer.types.includes("Files"))
|
|
88
93
|
return;
|
|
89
94
|
/* Si accende anche quando la cartella trascinata è questa stessa: durante il
|
|
@@ -93,6 +98,13 @@ function FolderCell({ lib, folder, manage }) {
|
|
|
93
98
|
setOver(true);
|
|
94
99
|
}, onDragLeave: () => setOver(false), onDrop: (e) => {
|
|
95
100
|
setOver(false);
|
|
101
|
+
if (!manage && !locked && e.dataTransfer.files.length) {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
e.stopPropagation();
|
|
104
|
+
onExternalDrop();
|
|
105
|
+
lib.openUpload(e.dataTransfer.files, folder.prefix);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
96
108
|
if (!manage || locked)
|
|
97
109
|
return;
|
|
98
110
|
const ids = readDragged(e.dataTransfer);
|
|
@@ -103,7 +115,7 @@ function FolderCell({ lib, folder, manage }) {
|
|
|
103
115
|
}, children: [_jsxs("div", { className: "mg-frame", onClick: () => { if (!locked)
|
|
104
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" }) }))] }));
|
|
105
117
|
}
|
|
106
|
-
export function MediaGrid({ lib, onPick, onCancel }) {
|
|
118
|
+
export function MediaGrid({ lib, notice, onPick, onCancel }) {
|
|
107
119
|
const manage = lib.mode === "manage";
|
|
108
120
|
const sel = lib.selected;
|
|
109
121
|
const allOn = lib.allSelected;
|
|
@@ -116,7 +128,7 @@ export function MediaGrid({ lib, onPick, onCancel }) {
|
|
|
116
128
|
zona di caricamento non c'entra. Il controllo è "NON è roba nostra" invece di "è un file":
|
|
117
129
|
`types` non contiene "Files" allo stesso modo in ogni browser, e con quel controllo il
|
|
118
130
|
caricamento non partiva affatto dove il nome era diverso. */
|
|
119
|
-
const external = (e) =>
|
|
131
|
+
const external = (e) => !locked && !e.dataTransfer.types.includes(DRAG_TYPE) && !e.dataTransfer.types.includes(DRAG_FOLDERS);
|
|
120
132
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mg-wrap" + (dragDepth > 0 ? " is-dropping" : ""), "aria-busy": lib.mutating ? "true" : "false",
|
|
121
133
|
/* Trascinare file dal Finder sulla griglia carica, senza passare dal dialogo: è il gesto
|
|
122
134
|
più corto e va lasciato funzionare. Il dialogo serve a chi cerca un bottone.
|
|
@@ -130,13 +142,18 @@ export function MediaGrid({ lib, onPick, onCancel }) {
|
|
|
130
142
|
e.preventDefault(); }, onDragLeave: (e) => { if (external(e))
|
|
131
143
|
setDragDepth((n) => Math.max(0, n - 1)); }, onDrop: (e) => {
|
|
132
144
|
setDragDepth(0);
|
|
133
|
-
if (
|
|
145
|
+
if (locked || !e.dataTransfer.files.length)
|
|
134
146
|
return;
|
|
135
147
|
e.preventDefault();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
if (manage)
|
|
149
|
+
void lib.upload(e.dataTransfer.files);
|
|
150
|
+
else
|
|
151
|
+
lib.openUpload(e.dataTransfer.files);
|
|
152
|
+
}, children: [dragDepth > 0 && (_jsx("div", { className: "mg-dropzone", children: manage
|
|
153
|
+
? _jsxs("span", { children: ["Rilascia per caricare in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella" })] })
|
|
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
|
+
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 === "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) => {
|
|
140
157
|
const on = sel.has(f.key);
|
|
141
158
|
return (_jsxs("figure", { className: "mg-item" + (on ? " is-on" : ""), draggable: manage && !locked, onDragStart: (e) => {
|
|
142
159
|
if (locked) {
|
|
@@ -159,5 +176,5 @@ export function MediaGrid({ lib, onPick, onCancel }) {
|
|
|
159
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));
|
|
160
177
|
}) }), !lib.busy && lib.shown.length === 0 && (_jsx("p", { className: "ak-hint", children: lib.prefix === "" && lib.folders.length
|
|
161
178
|
? "Nessuna foto fuori dalle cartelle. Scegline una qui a fianco."
|
|
162
|
-
: "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 }),
|
|
179
|
+
: "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 })] }));
|
|
163
180
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { MediaLibrary } from "./useMediaLibrary.js";
|
|
2
|
+
/** Le destinazioni che il picker può dichiarare senza inventarsi un secondo browser di
|
|
3
|
+
* cartelle: primo livello, cartella aperta e sue figlie. I duplicati spariscono mantenendo
|
|
4
|
+
* l'ordine più comprensibile. */
|
|
5
|
+
export declare function uploadDestinations(lib: MediaLibrary): {
|
|
6
|
+
prefix: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}[];
|
|
2
9
|
export declare function UploadDialog({ lib }: {
|
|
3
10
|
lib: MediaLibrary;
|
|
4
11
|
}): import("react").JSX.Element | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/* Il dialogo di caricamento.
|
|
2
|
+
/* Il dialogo di caricamento, condiviso da Media e picker.
|
|
3
3
|
*
|
|
4
4
|
* Dice sempre DOVE finiranno le foto. Il caricamento va nella cartella che si sta guardando, e
|
|
5
5
|
* senza scriverlo è una scommessa: si carica dalla vista «Tutte» e le foto atterrano nella
|
|
@@ -9,21 +9,54 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
* il messaggio resta sullo schermo invece di sparire con la finestra.
|
|
10
10
|
*/
|
|
11
11
|
import { useRef, useState } from "react";
|
|
12
|
+
const UNSET = "__adminkit_unset_destination__";
|
|
13
|
+
/** Le destinazioni che il picker può dichiarare senza inventarsi un secondo browser di
|
|
14
|
+
* cartelle: primo livello, cartella aperta e sue figlie. I duplicati spariscono mantenendo
|
|
15
|
+
* l'ordine più comprensibile. */
|
|
16
|
+
export function uploadDestinations(lib) {
|
|
17
|
+
const out = [{ prefix: "", label: "Senza cartella" }];
|
|
18
|
+
const seen = new Set([""]);
|
|
19
|
+
const add = (prefix, label) => {
|
|
20
|
+
if (seen.has(prefix))
|
|
21
|
+
return;
|
|
22
|
+
seen.add(prefix);
|
|
23
|
+
out.push({ prefix, label });
|
|
24
|
+
};
|
|
25
|
+
for (const folder of lib.folders)
|
|
26
|
+
add(folder.prefix, folder.label || folder.name);
|
|
27
|
+
if (lib.prefix)
|
|
28
|
+
add(lib.prefix, lib.labelOf(lib.prefix));
|
|
29
|
+
for (const folder of lib.subfolders) {
|
|
30
|
+
const parent = lib.prefix ? `${lib.labelOf(lib.prefix)} › ` : "";
|
|
31
|
+
add(folder.prefix, parent + (folder.label || folder.name));
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
12
35
|
export function UploadDialog({ lib }) {
|
|
13
36
|
const input = useRef(null);
|
|
14
37
|
const [over, setOver] = useState(false);
|
|
15
|
-
|
|
38
|
+
const draft = lib.uploadDraft;
|
|
39
|
+
if (!draft)
|
|
16
40
|
return null;
|
|
41
|
+
const picker = lib.mode === "pick";
|
|
17
42
|
const send = async (list) => {
|
|
18
43
|
if (lib.mutating || !list || !list.length)
|
|
19
44
|
return;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
if (picker)
|
|
46
|
+
return lib.setUploadFiles(list);
|
|
47
|
+
/* Media conserva il gesto storico: scegliere o lasciare i file li accoda subito nella
|
|
48
|
+
cartella aperta. Nel picker restano invece in attesa finché la destinazione è esplicita. */
|
|
49
|
+
if (await lib.upload(list, draft.destination ?? lib.prefix))
|
|
50
|
+
lib.closeUpload();
|
|
51
|
+
};
|
|
52
|
+
const start = async () => {
|
|
53
|
+
if (!picker || draft.destination === null || !draft.files.length)
|
|
54
|
+
return;
|
|
55
|
+
if (await lib.upload(draft.files, draft.destination))
|
|
56
|
+
lib.closeUpload();
|
|
24
57
|
};
|
|
25
58
|
return (_jsx("div", { className: "up", onClick: (e) => { if (!lib.mutating && e.target === e.currentTarget)
|
|
26
|
-
lib.
|
|
59
|
+
lib.closeUpload(); }, children: _jsxs("div", { className: "up-panel", children: [_jsx("button", { className: "up-close", disabled: lib.mutating, onClick: lib.closeUpload, "aria-label": "Chiudi", children: "\u00D7" }), _jsxs("div", { className: "up-drop" + (over ? " is-over" : ""),
|
|
27
60
|
/* `stopPropagation` oltre a `preventDefault`: il dialogo è una sovrapposizione, ma
|
|
28
61
|
per React resta un nodo dell'albero e l'evento continuerebbe a salire. Oggi sopra
|
|
29
62
|
non c'è più nessuna zona di rilascio (il dialogo è fuori da `.mg-wrap`), e questa
|
|
@@ -39,5 +72,11 @@ export function UploadDialog({ lib }) {
|
|
|
39
72
|
e.stopPropagation();
|
|
40
73
|
setOver(false);
|
|
41
74
|
void send(e.dataTransfer.files);
|
|
42
|
-
}, children: [_jsxs("svg", { className: "up-cloud", viewBox: "0 0 48 32", width: "56", height: "38", "aria-hidden": "true", children: [_jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", d: "M38 30a9 9 0 0 0 .6-18A13 13 0 0 0 13 13.6 8 8 0 0 0 10 30" }), _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M24 30V12m-6 6 6-6 6 6" })] }), _jsx("p", { className: "up-hint", children:
|
|
75
|
+
}, children: [_jsxs("svg", { className: "up-cloud", viewBox: "0 0 48 32", width: "56", height: "38", "aria-hidden": "true", children: [_jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", d: "M38 30a9 9 0 0 0 .6-18A13 13 0 0 0 13 13.6 8 8 0 0 0 10 30" }), _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M24 30V12m-6 6 6-6 6 6" })] }), _jsx("p", { className: "up-hint", children: picker && draft.files.length
|
|
76
|
+
? `${draft.files.length} ${draft.files.length === 1 ? "immagine pronta" : "immagini pronte"}`
|
|
77
|
+
: "Trascina qui le foto da caricare" }), picker && draft.files.length > 0 && (_jsxs("p", { className: "up-files", title: draft.files.map((file) => file.name).join("\n"), children: [draft.files.slice(0, 3).map((file) => file.name).join(", "), draft.files.length > 3 ? ` e altre ${draft.files.length - 3}` : ""] })), _jsx("input", { ref: input, type: "file", multiple: true, accept: picker ? "image/*" : "image/*,video/*", hidden: true, disabled: lib.mutating, onChange: (e) => {
|
|
78
|
+
const files = Array.from(e.target.files ?? []);
|
|
79
|
+
e.target.value = ""; // lo stesso file può essere scelto di nuovo dopo un errore
|
|
80
|
+
void send(files);
|
|
81
|
+
} }), _jsx("button", { className: "ak-btn ak-btn-primary", disabled: lib.mutating, onClick: () => input.current?.click(), children: picker && draft.files.length ? "Cambia i file" : "Scegli i file" })] }), picker ? (_jsxs("label", { className: "up-dest-field", children: [_jsx("span", { children: "Destinazione" }), _jsxs("select", { className: "ak-input", value: draft.destination === null ? UNSET : draft.destination, onChange: (event) => lib.setUploadDestination(event.target.value === UNSET ? null : event.target.value), children: [_jsx("option", { value: UNSET, children: "Scegli una cartella\u2026" }), uploadDestinations(lib).map((destination) => (_jsx("option", { value: destination.prefix, children: destination.label }, destination.prefix || "__root")))] }), _jsx("small", { children: "La cartella aperta o ricordata non viene usata automaticamente." })] })) : (_jsxs("p", { className: "up-dest", children: ["Finiranno in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella (la radice del bucket)" })] })), lib.progress && (_jsxs("p", { className: "ak-hint", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026"] })), lib.error && _jsx("p", { className: "ak-error", children: lib.error }), picker && (_jsxs("div", { className: "up-actions", children: [_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: lib.mutating, onClick: lib.closeUpload, children: "Annulla" }), _jsxs("button", { className: "ak-btn ak-btn-primary", disabled: lib.mutating || !draft.files.length || draft.destination === null, onClick: () => void start(), children: ["Carica ", draft.files.length > 0 ? `(${draft.files.length})` : ""] })] }))] }) }));
|
|
43
82
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AssetFile } from "../../api.js";
|
|
1
2
|
export type MediaUploadState = "running" | "done" | "failed";
|
|
2
3
|
export interface MediaUploadJob {
|
|
3
4
|
id: string;
|
|
@@ -12,6 +13,10 @@ export interface MediaUploadJob {
|
|
|
12
13
|
failed: number;
|
|
13
14
|
autoWebp: boolean;
|
|
14
15
|
errors: string[];
|
|
16
|
+
/** I file come esistono DAVVERO nel bucket dopo conversione, naming e upload. Il picker li
|
|
17
|
+
* usa per selezionare i nuovi arrivi: non può ricostruirne l'URL dal nome originale, perché
|
|
18
|
+
* una conversione può cambiare estensione e `uniqueKey` può aggiungere un suffisso. */
|
|
19
|
+
uploaded: AssetFile[];
|
|
15
20
|
}
|
|
16
21
|
interface StartInput {
|
|
17
22
|
prefix: string;
|
|
@@ -45,7 +45,7 @@ export function startMediaUpload(input) {
|
|
|
45
45
|
jobs = [...jobs, {
|
|
46
46
|
id, title: input.title, prefix: input.prefix, state: "running", total: input.files.length,
|
|
47
47
|
conversionDone: 0, uploadDone: 0, converted: 0, originals: 0, failed: 0,
|
|
48
|
-
autoWebp: input.autoWebp, errors: [],
|
|
48
|
+
autoWebp: input.autoWebp, errors: [], uploaded: [],
|
|
49
49
|
}];
|
|
50
50
|
emit();
|
|
51
51
|
void run(id, input);
|
|
@@ -80,9 +80,9 @@ async function run(id, input) {
|
|
|
80
80
|
if (!prepared)
|
|
81
81
|
return;
|
|
82
82
|
try {
|
|
83
|
-
await uploadWithRetry(input.prefix, prepared.upload, prepared.capturedAt);
|
|
83
|
+
const uploaded = await uploadWithRetry(input.prefix, prepared.upload, prepared.capturedAt);
|
|
84
84
|
const job = current(id);
|
|
85
|
-
patch(id, { uploadDone: job.uploadDone + 1 });
|
|
85
|
+
patch(id, { uploadDone: job.uploadDone + 1, uploaded: [...job.uploaded, uploaded] });
|
|
86
86
|
}
|
|
87
87
|
catch (error) {
|
|
88
88
|
const job = current(id);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AssetFile, type AssetFolder } from "../../api.js";
|
|
2
2
|
import type { MediaOperationStatus } from "../../media.js";
|
|
3
|
+
import { type MediaUploadJob } from "./media-upload-queue.js";
|
|
3
4
|
export type MediaOperationState = "idle" | "planning" | "awaiting_confirmation" | "moving" | "deleting" | "renaming" | "creating_folder" | "journal";
|
|
4
5
|
export type SortKey = "uploaded" | "capturedAt" | "name" | "size";
|
|
5
6
|
export declare const SORT_LABELS: Record<SortKey, string>;
|
|
@@ -31,7 +32,13 @@ export interface MediaLibrary {
|
|
|
31
32
|
} | null;
|
|
32
33
|
sortKey: SortKey;
|
|
33
34
|
sortDir: "asc" | "desc";
|
|
34
|
-
|
|
35
|
+
uploadDraft: {
|
|
36
|
+
files: File[];
|
|
37
|
+
destination: string | null;
|
|
38
|
+
} | null;
|
|
39
|
+
/** Il job nato da QUESTO picker. Gli upload sono globali, ma solo questo va riportato qui
|
|
40
|
+
* per mostrare il progresso e selezionare i file appena arrivati. */
|
|
41
|
+
pickUploadJob: MediaUploadJob | null;
|
|
35
42
|
viewing: number | null;
|
|
36
43
|
autoWebp: boolean;
|
|
37
44
|
settingsBusy: boolean;
|
|
@@ -41,9 +48,13 @@ export interface MediaLibrary {
|
|
|
41
48
|
setAll(on: boolean): void;
|
|
42
49
|
setSort(k: SortKey): void;
|
|
43
50
|
flipDir(): void;
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
/** Accoda lo STESSO lavoro usato da Media. `destination` è esplicita nel picker; in gestione
|
|
52
|
+
* il ripiego resta la cartella aperta, che è il comportamento storico. */
|
|
53
|
+
upload(files: File[] | FileList, destination?: string): Promise<string | null>;
|
|
54
|
+
openUpload(files?: File[] | FileList, destination?: string): void;
|
|
55
|
+
closeUpload(): void;
|
|
56
|
+
setUploadFiles(files: File[] | FileList): void;
|
|
57
|
+
setUploadDestination(destination: string | null): void;
|
|
47
58
|
setAutoWebp(on: boolean): Promise<void>;
|
|
48
59
|
view(i: number | null): void;
|
|
49
60
|
step(delta: number): void;
|
|
@@ -12,7 +12,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
12
12
|
import { api, ApiError } from "../../api.js";
|
|
13
13
|
import { storageKey } from "../../config.js";
|
|
14
14
|
import { contentPersistenceMode } from "../../app/site.js";
|
|
15
|
-
import { startMediaUpload, useMediaUploadCompletions } from "./media-upload-queue.js";
|
|
15
|
+
import { startMediaUpload, useMediaUploadCompletions, useMediaUploads, } from "./media-upload-queue.js";
|
|
16
16
|
import { invalidMediaMoveReason, isValidMediaMoveDestination, mediaFolderSlug, mediaSelectableItems, mediaSelectionState, normalizeMediaSelection, summarizeMediaSelection, tryBeginMediaMutation, } from "./media-model.js";
|
|
17
17
|
import { executeMediaMovePlan, mediaMoveConfirmation, mediaMoveNeedsConfirmation, } from "./media-move.js";
|
|
18
18
|
/* Quanti file per richiesta nelle operazioni lunghe (cestina, sposta). Ogni file costa al
|
|
@@ -42,12 +42,14 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
42
42
|
const [error, setError] = useState(null);
|
|
43
43
|
const [mediaOperations, setMediaOperations] = useState([]);
|
|
44
44
|
const [progress, setProgress] = useState(null);
|
|
45
|
-
const [
|
|
45
|
+
const [uploadDraft, setUploadDraft] = useState(null);
|
|
46
|
+
const [pickUploadJobId, setPickUploadJobId] = useState(null);
|
|
46
47
|
const [viewing, setViewing] = useState(null);
|
|
47
48
|
const [autoWebp, setAutoWebpState] = useState(true);
|
|
48
49
|
const [settingsBusy, setSettingsBusy] = useState(false);
|
|
49
50
|
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
|
50
51
|
const mediaUploadCompletions = useMediaUploadCompletions();
|
|
52
|
+
const mediaUploads = useMediaUploads();
|
|
51
53
|
const seenMediaUploadCompletions = useRef(mediaUploadCompletions);
|
|
52
54
|
/* Lo stato React disabilita la UI; la ref chiude anche la finestra nello stesso batch React,
|
|
53
55
|
* prima che il render successivo possa applicare i disabled. */
|
|
@@ -151,8 +153,8 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
151
153
|
useEffect(() => { localStorage.setItem(storageKey("sortKey"), sortKey); localStorage.setItem(storageKey("sortDir"), sortDir); }, [sortKey, sortDir]);
|
|
152
154
|
/* Questa preferenza è del SITO, non del browser: il backend la conserva nel bucket Media.
|
|
153
155
|
* Finché non arriva la risposta vale il default ON, così anche i siti già esistenti hanno il
|
|
154
|
-
* nuovo comportamento senza migrazioni.
|
|
155
|
-
*
|
|
156
|
+
* nuovo comportamento senza migrazioni. In gestione si anticipa la richiesta; il picker la
|
|
157
|
+
* farà pigramente solo quando parte davvero un upload. */
|
|
156
158
|
useEffect(() => {
|
|
157
159
|
if (mode !== "manage" || !active || settingsLoaded)
|
|
158
160
|
return;
|
|
@@ -169,11 +171,32 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
169
171
|
if (!active || seenMediaUploadCompletions.current === mediaUploadCompletions)
|
|
170
172
|
return;
|
|
171
173
|
seenMediaUploadCompletions.current = mediaUploadCompletions;
|
|
174
|
+
/* Il job nato da questo picker ha un effetto dedicato qui sotto: deve andare nella propria
|
|
175
|
+
destinazione e selezionare i nuovi file. Un reload generico della cartella che si stava
|
|
176
|
+
guardando correrebbe in parallelo e potrebbe vincere per ultimo. */
|
|
177
|
+
if (mode === "pick" && pickUploadJobId)
|
|
178
|
+
return;
|
|
172
179
|
// Se nel frattempo c'è una mutazione, il suo reload finale comprende anche l'upload appena
|
|
173
180
|
// concluso. Un secondo load qui potrebbe invece cambiare busy e selezione a metà progresso.
|
|
174
181
|
if (loaded && !mutationRef.current)
|
|
175
182
|
void load(prefix);
|
|
176
|
-
}, [active, mediaUploadCompletions, loaded, load, prefix]);
|
|
183
|
+
}, [active, mediaUploadCompletions, loaded, load, prefix, mode, pickUploadJobId]);
|
|
184
|
+
const pickUploadJob = pickUploadJobId
|
|
185
|
+
? mediaUploads.find((job) => job.id === pickUploadJobId) ?? null
|
|
186
|
+
: null;
|
|
187
|
+
/* Quando il lavoro del picker finisce, apre la destinazione e seleziona gli oggetti restituiti
|
|
188
|
+
* dall'API. Sono quelli reali: magari `.webp`, magari `foto-2.webp`. In scelta singola se ne
|
|
189
|
+
* evidenzia uno solo, pur avendo correttamente caricato tutto il gruppo. */
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (mode !== "pick" || !pickUploadJobId || !pickUploadJob || pickUploadJob.state === "running")
|
|
192
|
+
return;
|
|
193
|
+
const uploaded = pickUploadJob.uploaded.map((file) => file.key);
|
|
194
|
+
const chosen = multiple ? uploaded : uploaded.slice(0, 1);
|
|
195
|
+
setPickUploadJobId(null);
|
|
196
|
+
if (!uploaded.length)
|
|
197
|
+
return;
|
|
198
|
+
void load(pickUploadJob.prefix).then(() => setSelected(new Set(chosen)));
|
|
199
|
+
}, [mode, multiple, pickUploadJobId, pickUploadJob, load]);
|
|
177
200
|
/* Chiudere o ricaricare mentre un'operazione è in corso la interrompe: il gruppo in volo non
|
|
178
201
|
* arriva mai. Ora ogni gruppo è definitivo, quindi si perde poco — ma vale comunque la pena
|
|
179
202
|
* chiedere, perché è esattamente il gesto che uno fa quando "non sta succedendo niente". */
|
|
@@ -226,12 +249,12 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
226
249
|
return setSelected(new Set());
|
|
227
250
|
setSelected(new Set(selectableIds));
|
|
228
251
|
}, [selectableIds]);
|
|
229
|
-
const upload = useCallback(async (list) => {
|
|
252
|
+
const upload = useCallback(async (list, requestedDestination) => {
|
|
230
253
|
if (mutationRef.current)
|
|
231
|
-
return
|
|
254
|
+
return null;
|
|
232
255
|
const arr = Array.from(list);
|
|
233
256
|
if (!arr.length)
|
|
234
|
-
return
|
|
257
|
+
return null;
|
|
235
258
|
let convert = autoWebp;
|
|
236
259
|
/* Evita la piccola finestra fra apertura dei Media e arrivo dell'impostazione: se sul sito
|
|
237
260
|
la conversione è OFF, un upload immediato non deve partire col default ON. */
|
|
@@ -244,20 +267,23 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
244
267
|
}
|
|
245
268
|
catch (e) {
|
|
246
269
|
setError(e instanceof ApiError ? e.message : String(e));
|
|
247
|
-
return
|
|
270
|
+
return null;
|
|
248
271
|
}
|
|
249
272
|
}
|
|
250
|
-
const
|
|
251
|
-
|
|
273
|
+
const target = requestedDestination ?? prefix;
|
|
274
|
+
const destination = target
|
|
275
|
+
? seenLabels.current[target] || target.replace(/\/$/, "").split("/").pop() || target
|
|
252
276
|
: "Senza cartella";
|
|
253
|
-
startMediaUpload({
|
|
254
|
-
prefix,
|
|
277
|
+
const id = startMediaUpload({
|
|
278
|
+
prefix: target,
|
|
255
279
|
title: `${arr.length} file · ${destination}`,
|
|
256
280
|
files: arr,
|
|
257
281
|
autoWebp: convert,
|
|
258
282
|
});
|
|
259
|
-
|
|
260
|
-
|
|
283
|
+
if (mode === "pick")
|
|
284
|
+
setPickUploadJobId(id);
|
|
285
|
+
return id;
|
|
286
|
+
}, [prefix, autoWebp, settingsLoaded, mode]);
|
|
261
287
|
const setAutoWebp = useCallback(async (on) => {
|
|
262
288
|
if (mutationRef.current)
|
|
263
289
|
return;
|
|
@@ -657,7 +683,7 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
657
683
|
mode, multiple, prefix, folders, subfolders, shown, selected, selectableIds,
|
|
658
684
|
selectedFiles: selectionSummary.files, selectedFolders: selectionSummary.folders,
|
|
659
685
|
allSelected, someSelected, busy, mutating, operationState, error, mediaOperations, progress,
|
|
660
|
-
sortKey, sortDir,
|
|
686
|
+
sortKey, sortDir, uploadDraft, pickUploadJob, viewing, autoWebp, settingsBusy,
|
|
661
687
|
go: (p) => { if (!mutationRef.current)
|
|
662
688
|
void load(p); },
|
|
663
689
|
reload: () => { if (!mutationRef.current)
|
|
@@ -666,8 +692,18 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
|
|
|
666
692
|
setSort: setSortKey,
|
|
667
693
|
flipDir: () => setSortDir((d) => (d === "asc" ? "desc" : "asc")),
|
|
668
694
|
upload,
|
|
669
|
-
|
|
670
|
-
|
|
695
|
+
openUpload: (files = [], destination) => {
|
|
696
|
+
if (mutationRef.current)
|
|
697
|
+
return;
|
|
698
|
+
setUploadDraft({
|
|
699
|
+
files: Array.from(files),
|
|
700
|
+
destination: mode === "manage" ? (destination ?? prefix) : (destination === undefined ? null : destination),
|
|
701
|
+
});
|
|
702
|
+
},
|
|
703
|
+
closeUpload: () => { if (!mutationRef.current)
|
|
704
|
+
setUploadDraft(null); },
|
|
705
|
+
setUploadFiles: (files) => setUploadDraft((draft) => (draft ? { ...draft, files: Array.from(files) } : draft)),
|
|
706
|
+
setUploadDestination: (destination) => setUploadDraft((draft) => (draft ? { ...draft, destination } : draft)),
|
|
671
707
|
setAutoWebp,
|
|
672
708
|
view: setViewing,
|
|
673
709
|
step,
|
package/dist/theme.css
CHANGED
|
@@ -345,6 +345,7 @@ a.ak-btn { text-decoration: none; display: inline-flex; align-items: center; }
|
|
|
345
345
|
.mg-act:hover:not(:disabled) { text-decoration: underline; }
|
|
346
346
|
.mg-act:disabled { color: var(--ak-ink-quiet); cursor: default; }
|
|
347
347
|
.mg-act.is-danger { color: var(--ak-danger); }
|
|
348
|
+
.mg-upload { white-space: nowrap; }
|
|
348
349
|
.mg-menu-wrap { position: relative; display: inline-flex; }
|
|
349
350
|
.mg-menu {
|
|
350
351
|
position: absolute; top: calc(100% + 4px); left: 0; z-index: 30;
|
|
@@ -374,6 +375,11 @@ a.ak-btn { text-decoration: none; display: inline-flex; align-items: center; }
|
|
|
374
375
|
background: var(--ak-panel); border: 1px solid var(--ak-accent);
|
|
375
376
|
font: 500 var(--ak-fs-small)/1.4 var(--ak-font); color: var(--ak-ink);
|
|
376
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
|
+
}
|
|
377
383
|
.mg-journal { display: flex; flex-direction: column; gap: 6px; }
|
|
378
384
|
.mg-journal-row {
|
|
379
385
|
display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
|
|
@@ -507,7 +513,16 @@ a.ak-btn { text-decoration: none; display: inline-flex; align-items: center; }
|
|
|
507
513
|
.up-drop.is-over { border-color: var(--ak-accent); background: #fdf4f2; }
|
|
508
514
|
.up-cloud { color: var(--ak-ink-quiet); }
|
|
509
515
|
.up-hint { margin: 0; font: 400 13px/1.4 var(--ak-font); color: var(--ak-ink-quiet); }
|
|
516
|
+
.up-files {
|
|
517
|
+
max-width: 100%; margin: -7px 0 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
518
|
+
color: var(--ak-ink); font: 500 var(--ak-fs-small)/1.4 var(--ak-font);
|
|
519
|
+
}
|
|
510
520
|
.up-dest { margin: 0; font: 400 var(--ak-fs-small)/1.4 var(--ak-font); color: var(--ak-ink-quiet); }
|
|
521
|
+
.up-dest-field { display: flex; flex-direction: column; gap: 6px; }
|
|
522
|
+
.up-dest-field > span { font: 600 var(--ak-fs-small)/1.3 var(--ak-font); color: var(--ak-ink); }
|
|
523
|
+
.up-dest-field select { width: 100%; }
|
|
524
|
+
.up-dest-field small { color: var(--ak-ink-quiet); font: 400 11px/1.4 var(--ak-font); }
|
|
525
|
+
.up-actions { display: flex; justify-content: flex-end; gap: 7px; padding-top: 2px; }
|
|
511
526
|
|
|
512
527
|
/* ---- Cestino ------------------------------------------------------------------------ */
|
|
513
528
|
.tr { display: flex; flex-direction: column; gap: 10px; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardinali-studio/adminkit",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.17",
|
|
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",
|