@cardinali-studio/adminkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +195 -0
- package/dist/api.js +118 -0
- package/dist/app/App.d.ts +3 -0
- package/dist/app/App.js +320 -0
- package/dist/app/app.css +2068 -0
- package/dist/app/index.d.ts +18 -0
- package/dist/app/index.js +33 -0
- package/dist/app/lib/bookings.d.ts +132 -0
- package/dist/app/lib/bookings.js +79 -0
- package/dist/app/lib/deliveries.d.ts +95 -0
- package/dist/app/lib/deliveries.js +104 -0
- package/dist/app/lib/delivery-drop.d.ts +1 -0
- package/dist/app/lib/delivery-drop.js +72 -0
- package/dist/app/lib/delivery-upload.d.ts +78 -0
- package/dist/app/lib/delivery-upload.js +585 -0
- package/dist/app/lib/groups.d.ts +9 -0
- package/dist/app/lib/groups.js +32 -0
- package/dist/app/lib/home.d.ts +11 -0
- package/dist/app/lib/home.js +67 -0
- package/dist/app/lib/overview.d.ts +82 -0
- package/dist/app/lib/overview.js +68 -0
- package/dist/app/lib/pages.d.ts +24 -0
- package/dist/app/lib/pages.js +156 -0
- package/dist/app/lib/publish.d.ts +18 -0
- package/dist/app/lib/publish.js +63 -0
- package/dist/app/lib/reorder.d.ts +3 -0
- package/dist/app/lib/reorder.js +31 -0
- package/dist/app/lib/requests.d.ts +10 -0
- package/dist/app/lib/requests.js +67 -0
- package/dist/app/lib/router.d.ts +1 -0
- package/dist/app/lib/router.js +14 -0
- package/dist/app/lib/settings.d.ts +29 -0
- package/dist/app/lib/settings.js +82 -0
- package/dist/app/lib/slug.d.ts +3 -0
- package/dist/app/lib/slug.js +23 -0
- package/dist/app/lib/text.d.ts +1 -0
- package/dist/app/lib/text.js +13 -0
- package/dist/app/lib/theme.d.ts +7 -0
- package/dist/app/lib/theme.js +43 -0
- package/dist/app/lib/upload-queue.d.ts +35 -0
- package/dist/app/lib/upload-queue.js +167 -0
- package/dist/app/routes/AboutEditor.d.ts +15 -0
- package/dist/app/routes/AboutEditor.js +122 -0
- package/dist/app/routes/Bookings.d.ts +21 -0
- package/dist/app/routes/Bookings.js +134 -0
- package/dist/app/routes/Deliveries.d.ts +37 -0
- package/dist/app/routes/Deliveries.js +563 -0
- package/dist/app/routes/GallerySectionsEditor.d.ts +12 -0
- package/dist/app/routes/GallerySectionsEditor.js +107 -0
- package/dist/app/routes/HomeEditor.d.ts +10 -0
- package/dist/app/routes/HomeEditor.js +188 -0
- package/dist/app/routes/Overview.d.ts +17 -0
- package/dist/app/routes/Overview.js +318 -0
- package/dist/app/routes/PageActions.d.ts +12 -0
- package/dist/app/routes/PageActions.js +93 -0
- package/dist/app/routes/PageEditor.d.ts +23 -0
- package/dist/app/routes/PageEditor.js +233 -0
- package/dist/app/routes/PageSeoEditor.d.ts +13 -0
- package/dist/app/routes/PageSeoEditor.js +102 -0
- package/dist/app/routes/PagesList.d.ts +10 -0
- package/dist/app/routes/PagesList.js +76 -0
- package/dist/app/routes/PortfolioWorksEditor.d.ts +12 -0
- package/dist/app/routes/PortfolioWorksEditor.js +139 -0
- package/dist/app/routes/Requests.d.ts +13 -0
- package/dist/app/routes/Requests.js +30 -0
- package/dist/app/routes/SiteSettingsEditor.d.ts +21 -0
- package/dist/app/routes/SiteSettingsEditor.js +66 -0
- package/dist/app/routes/UploadDock.d.ts +1 -0
- package/dist/app/routes/UploadDock.js +66 -0
- package/dist/app/routes/WorkshopEditionsEditor.d.ts +12 -0
- package/dist/app/routes/WorkshopEditionsEditor.js +101 -0
- package/dist/app/site.d.ts +37 -0
- package/dist/app/site.js +18 -0
- package/dist/app/ui/MediaSlot.d.ts +10 -0
- package/dist/app/ui/MediaSlot.js +20 -0
- package/dist/app/ui/OpenCard.d.ts +18 -0
- package/dist/app/ui/OpenCard.js +20 -0
- package/dist/app/ui/ParagraphEditor.d.ts +8 -0
- package/dist/app/ui/ParagraphEditor.js +37 -0
- package/dist/app/ui/dnd.d.ts +1 -0
- package/dist/app/ui/dnd.js +11 -0
- package/dist/app/ui/icons.d.ts +26 -0
- package/dist/app/ui/icons.js +45 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +26 -0
- package/dist/form/MediaBrowser.d.ts +13 -0
- package/dist/form/MediaBrowser.js +24 -0
- package/dist/form/MediaPicker.d.ts +10 -0
- package/dist/form/MediaPicker.js +20 -0
- package/dist/form/SchemaForm.d.ts +21 -0
- package/dist/form/SchemaForm.js +67 -0
- package/dist/form/Trash.d.ts +1 -0
- package/dist/form/Trash.js +42 -0
- package/dist/form/fields/index.d.ts +1 -0
- package/dist/form/fields/index.js +110 -0
- package/dist/form/fields/objectList.d.ts +1 -0
- package/dist/form/fields/objectList.js +99 -0
- package/dist/form/index.d.ts +8 -0
- package/dist/form/index.js +8 -0
- package/dist/form/media/Lightbox.d.ts +4 -0
- package/dist/form/media/Lightbox.js +70 -0
- package/dist/form/media/MediaFolders.d.ts +20 -0
- package/dist/form/media/MediaFolders.js +116 -0
- package/dist/form/media/MediaGrid.d.ts +8 -0
- package/dist/form/media/MediaGrid.js +139 -0
- package/dist/form/media/UploadDialog.d.ts +4 -0
- package/dist/form/media/UploadDialog.js +37 -0
- package/dist/form/media/useMediaLibrary.d.ts +55 -0
- package/dist/form/media/useMediaLibrary.js +495 -0
- package/dist/form/prose/ProseBar.d.ts +21 -0
- package/dist/form/prose/ProseBar.js +111 -0
- package/dist/form/prose/ProseField.d.ts +11 -0
- package/dist/form/prose/ProseField.js +152 -0
- package/dist/form/prose/pages.d.ts +6 -0
- package/dist/form/prose/pages.js +13 -0
- package/dist/form/registry.d.ts +20 -0
- package/dist/form/registry.js +15 -0
- package/dist/frontmatter.d.ts +20 -0
- package/dist/frontmatter.js +202 -0
- package/dist/functions/_middleware.js +101 -0
- package/dist/functions/_shared/analytics-ga4.js +248 -0
- package/dist/functions/_shared/analytics-umami.js +119 -0
- package/dist/functions/_shared/assets.js +90 -0
- package/dist/functions/_shared/b2.js +300 -0
- package/dist/functions/_shared/delivery-crypto.js +99 -0
- package/dist/functions/_shared/delivery-purge.js +222 -0
- package/dist/functions/_shared/edge-cache.js +37 -0
- package/dist/functions/_shared/exif.js +53 -0
- package/dist/functions/_shared/github.js +138 -0
- package/dist/functions/_shared/imagesize.js +101 -0
- package/dist/functions/_shared/media-content.js +200 -0
- package/dist/functions/_shared/pending-media.js +98 -0
- package/dist/functions/_shared/refs.js +155 -0
- package/dist/functions/api/analytics.js +106 -0
- package/dist/functions/api/assets/delete.js +241 -0
- package/dist/functions/api/assets/folder.js +43 -0
- package/dist/functions/api/assets/folders.js +32 -0
- package/dist/functions/api/assets/move.js +253 -0
- package/dist/functions/api/assets/preview.js +17 -0
- package/dist/functions/api/assets/trash/delete.js +13 -0
- package/dist/functions/api/assets/trash/empty.js +16 -0
- package/dist/functions/api/assets/trash/restore.js +43 -0
- package/dist/functions/api/assets/trash.js +28 -0
- package/dist/functions/api/assets/upload.js +54 -0
- package/dist/functions/api/assets/usage.js +41 -0
- package/dist/functions/api/assets.js +94 -0
- package/dist/functions/api/deliveries.js +629 -0
- package/dist/functions/api/delivery/hero.js +21 -0
- package/dist/functions/api/delivery/image.js +35 -0
- package/dist/functions/api/delivery/multipart.js +261 -0
- package/dist/functions/api/pages/get.js +26 -0
- package/dist/functions/api/pages.js +354 -0
- package/dist/functions/api/publish.js +134 -0
- package/dist/functions/api/requests.js +57 -0
- package/dist/functions/api/settings.js +236 -0
- package/dist/functions/api/singleton.js +105 -0
- package/dist/functions/api/storage.js +125 -0
- package/dist/functions/index.js +124 -0
- package/dist/functions/site.js +38 -0
- package/dist/gallery.d.ts +28 -0
- package/dist/gallery.js +18 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +47 -0
- package/dist/normalize.d.ts +50 -0
- package/dist/normalize.js +120 -0
- package/dist/preview.d.ts +3 -0
- package/dist/preview.js +82 -0
- package/dist/prose.d.ts +17 -0
- package/dist/prose.js +205 -0
- package/dist/registries.d.ts +43 -0
- package/dist/registries.js +29 -0
- package/dist/serialize.d.ts +26 -0
- package/dist/serialize.js +136 -0
- package/dist/testing.d.ts +26 -0
- package/dist/testing.js +66 -0
- package/dist/theme.css +636 -0
- package/dist/yaml.d.ts +8 -0
- package/dist/yaml.js +90 -0
- package/package.json +97 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* L'UNICO widget strutturale: rende un oggetto o una lista annidata passando i figli per lo
|
|
3
|
+
* STESSO registro, in modo ricorsivo.
|
|
4
|
+
*
|
|
5
|
+
* È il punto in cui un po' di genericità entra per forza — le sezioni della home sono oggetti
|
|
6
|
+
* con dentro liste di oggetti — e va tenuta confinata. Regola: se ci si trova ad aggiungerne un
|
|
7
|
+
* secondo, fermarsi. È il segnale che sta ricrescendo il motore di form generico che si era
|
|
8
|
+
* deciso di non costruire, e la prossima domanda diventa "cosa dovrebbe dedurre il motore".
|
|
9
|
+
*/
|
|
10
|
+
import * as z from "zod";
|
|
11
|
+
import { ui } from "../../registries.js";
|
|
12
|
+
import { registerField, fieldFor } from "../registry.js";
|
|
13
|
+
/** Sotto-schema di un campo Zod, se è un oggetto: serve a scendere di un livello. */
|
|
14
|
+
function shapeOf(schema) {
|
|
15
|
+
const def = schema?._zod?.def ?? schema?._def;
|
|
16
|
+
if (def?.type === "object" || def?.typeName === "ZodObject") {
|
|
17
|
+
const s = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
18
|
+
return s ?? null;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/** Schema degli elementi, se è un array. */
|
|
23
|
+
function elementOf(schema) {
|
|
24
|
+
const def = schema?._zod?.def ?? schema?._def;
|
|
25
|
+
if (def?.type === "array" || def?.typeName === "ZodArray")
|
|
26
|
+
return def.element ?? def.type ?? null;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/* Voce vuota per un NUOVO elemento della lista, che rispetti la FORMA di ogni sotto-campo
|
|
30
|
+
* invece di azzerarli tutti a stringa. Finché ogni lista-di-oggetti di questo schema aveva
|
|
31
|
+
* solo campi piatti (stringhe/numeri), "tutto a stringa vuota" era sempre corretto per
|
|
32
|
+
* costruzione — non c'era nessun array o oggetto annidato da rispettare. Con un sotto-campo
|
|
33
|
+
* che è a sua volta un array (es. le card di un lavoro del portfolio, o le sue info), una
|
|
34
|
+
* stringa vuota lì è un valore di forma SBAGLIATA: `PageObject.safeParse` lo scarterebbe come
|
|
35
|
+
* non valido in CI, ma il salvataggio delle pagine (functions/admin/api/pages.js) non valida
|
|
36
|
+
* affatto e scrive dritto su `main` — quindi qui l'errore andrebbe in produzione prima che
|
|
37
|
+
* qualcuno se ne accorga. */
|
|
38
|
+
function blankFor(schema) {
|
|
39
|
+
const s = shapeOf(schema);
|
|
40
|
+
if (s)
|
|
41
|
+
return Object.fromEntries(Object.keys(s).map((k) => [k, blankFor(s[k])]));
|
|
42
|
+
if (elementOf(schema))
|
|
43
|
+
return [];
|
|
44
|
+
// Un `select` non ha una stringa vuota fra le sue opzioni: il tag <select> del browser
|
|
45
|
+
// mostra comunque la PRIMA opzione (nessun value la combacia, ma qualcosa va disegnato),
|
|
46
|
+
// quindi sembra già scelta finché non si tocca il menu — ma il valore resta "" finché non
|
|
47
|
+
// scatta un onChange. Prima card salvata così: `type: ""`, un valore che non corrisponde a
|
|
48
|
+
// nessun ramo del template (vedi PORTFOLIO.md). Il vuoto CORRETTO per un select è la sua
|
|
49
|
+
// prima opzione, non una stringa che nessuna opzione userà mai.
|
|
50
|
+
const meta = ui.get(schema);
|
|
51
|
+
if (meta?.widget === "select" && meta.options?.length)
|
|
52
|
+
return meta.options[0].value;
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
/** Widget che rendono da soli un campo-array: qui la ricorsione non deve scendere. */
|
|
56
|
+
const SELF_RENDERING_LISTS = new Set(["r2Gallery", "r2Curated", "paragraphs"]);
|
|
57
|
+
function Node({ schema, value, onChange, pickMedia, pickMediaFiles, label, }) {
|
|
58
|
+
const meta = ui.get(schema);
|
|
59
|
+
const shape = shapeOf(schema);
|
|
60
|
+
const element = elementOf(schema);
|
|
61
|
+
// Oggetto: un riquadro con dentro i suoi campi.
|
|
62
|
+
if (shape) {
|
|
63
|
+
const obj = (value ?? {});
|
|
64
|
+
return (_jsx("div", { className: "ol-obj", children: Object.entries(shape).map(([k, sub]) => (_jsx(Node, { schema: sub, label: ui.get(sub)?.label ?? k, value: obj[k], pickMedia: pickMedia, pickMediaFiles: pickMediaFiles, onChange: (v) => onChange({ ...obj, [k]: v }) }, k))) }));
|
|
65
|
+
}
|
|
66
|
+
/* Lista: elementi riordinabili e rimovibili — A MENO che il campo dichiari un widget che sa
|
|
67
|
+
* già rendere una lista per conto suo. La ricorsione si ferma lì e disegna il widget.
|
|
68
|
+
*
|
|
69
|
+
* Prima l'eccezione era una sola, scritta a mano (`!== "r2Gallery"`), e ha fatto sparire in
|
|
70
|
+
* silenzio il widget successivo che ne aveva bisogno: `paragraphs` è un array di stringhe, e
|
|
71
|
+
* dentro un objectList veniva reso come elenco di caselline invece che come textarea — la
|
|
72
|
+
* dichiarazione nello schema non aveva alcun effetto, senza un errore da nessuna parte.
|
|
73
|
+
* `r2Curated` è nella lista per lo stesso motivo: oggi sta solo al primo livello, dove questo
|
|
74
|
+
* ramo non passa, ma il giorno che finisce dentro un oggetto si romperebbe uguale. */
|
|
75
|
+
if (element && !SELF_RENDERING_LISTS.has(meta?.widget ?? "")) {
|
|
76
|
+
const list = Array.isArray(value) ? value : [];
|
|
77
|
+
const move = (from, to) => {
|
|
78
|
+
if (to < 0 || to >= list.length)
|
|
79
|
+
return;
|
|
80
|
+
const next = list.slice();
|
|
81
|
+
const [x] = next.splice(from, 1);
|
|
82
|
+
next.splice(to, 0, x);
|
|
83
|
+
onChange(next);
|
|
84
|
+
};
|
|
85
|
+
return (_jsxs("fieldset", { className: "ol-list", children: [_jsx("legend", { className: "ak-legend", children: label ?? meta?.label }), list.map((item, i) => (_jsxs("div", { className: "ol-item", children: [_jsxs("div", { className: "ol-item-bar", children: [_jsx("span", { className: "ak-hint", children: i + 1 }), _jsx("span", { className: "ak-spacer" }), _jsx("button", { type: "button", className: "ak-icon", title: "Su", onClick: () => move(i, i - 1), children: "\u2191" }), _jsx("button", { type: "button", className: "ak-icon", title: "Gi\u00F9", onClick: () => move(i, i + 1), children: "\u2193" }), _jsx("button", { type: "button", className: "ak-icon", title: "Rimuovi", onClick: () => onChange(list.filter((_, j) => j !== i)), children: "\u2715" })] }), _jsx(Node, { schema: element, value: item, pickMedia: pickMedia, pickMediaFiles: pickMediaFiles, onChange: (v) => onChange(list.map((x, j) => (j === i ? v : x))) })] }, i))), _jsx("button", { type: "button", className: "ak-btn", onClick: () => onChange([...list, blankFor(element)]), children: "+ Aggiungi" })] }));
|
|
86
|
+
}
|
|
87
|
+
// Foglia: la disegna il widget dichiarato, come qualsiasi altro campo.
|
|
88
|
+
const w = meta?.widget ?? "text";
|
|
89
|
+
const Field = fieldFor(w);
|
|
90
|
+
return (_jsx(Field, { name: label ?? "", meta: { ...(meta ?? { label: label ?? "", widget: w }), label: label ?? meta?.label ?? "" }, value: value, onChange: onChange, pickMedia: pickMedia, pickMediaFiles: pickMediaFiles }));
|
|
91
|
+
}
|
|
92
|
+
/* Il widget registrato riceve lo SCHEMA del campo attraverso `meta.schema`, che SchemaForm gli
|
|
93
|
+
* passa: senza, non potrebbe sapere che forma abbiano i figli. */
|
|
94
|
+
registerField("objectList", ({ meta, value, onChange, pickMedia, pickMediaFiles }) => {
|
|
95
|
+
const schema = meta.schema;
|
|
96
|
+
if (!schema)
|
|
97
|
+
return _jsx("p", { className: "ak-error", children: "objectList senza schema: va passato da SchemaForm." });
|
|
98
|
+
return (_jsxs("fieldset", { className: "ak-group ol", children: [_jsx("legend", { className: "ak-legend", children: meta.label }), meta.hint && _jsx("p", { className: "ak-hint", children: meta.hint }), _jsx(Node, { schema: schema, value: value, onChange: onChange, pickMedia: pickMedia, pickMediaFiles: pickMediaFiles })] }));
|
|
99
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./SchemaForm.js";
|
|
2
|
+
export * from "./registry.js";
|
|
3
|
+
export * from "./MediaBrowser.js";
|
|
4
|
+
export * from "./MediaPicker.js";
|
|
5
|
+
export * from "./Trash.js";
|
|
6
|
+
export * from "./prose/ProseField.js";
|
|
7
|
+
export * from "./prose/ProseBar.js";
|
|
8
|
+
export * from "./prose/pages.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./SchemaForm.js";
|
|
2
|
+
export * from "./registry.js";
|
|
3
|
+
export * from "./MediaBrowser.js";
|
|
4
|
+
export * from "./MediaPicker.js";
|
|
5
|
+
export * from "./Trash.js";
|
|
6
|
+
export * from "./prose/ProseField.js";
|
|
7
|
+
export * from "./prose/ProseBar.js";
|
|
8
|
+
export * from "./prose/pages.js";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* La foto a schermo intero.
|
|
3
|
+
*
|
|
4
|
+
* Serve a guardare, non a modificare: fondo nero, l'immagine intera senza tagli, e in alto le
|
|
5
|
+
* tre sole cose che si vogliono fare mentre la si guarda — scaricarla, sapere cos'è, buttarla.
|
|
6
|
+
*
|
|
7
|
+
* «Info» chiede al server DOVE è usata quella foto, e lo fa solo quando lo si apre: è la domanda
|
|
8
|
+
* che ci si pone un istante prima di cestinare, ed è l'unica risposta che il pannello non può
|
|
9
|
+
* dedurre da solo. (La stessa domanda la fa `remove` prima di cancellare — qui è per guardare,
|
|
10
|
+
* là per avvertire.)
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
|
+
import { api } from "../../api.js";
|
|
14
|
+
/* Tre icone, disegnate qui: una libreria di icone per tre tratti porterebbe un tema e un altro
|
|
15
|
+
* modo di dire "colore" che non passa dalle custom property. */
|
|
16
|
+
const S = { fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" };
|
|
17
|
+
const IcoDown = () => _jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", children: _jsx("path", { ...S, d: "M8 2.5v8m-3.5-3.5L8 10.5 11.5 7M3 13.5h10" }) });
|
|
18
|
+
const IcoInfo = () => _jsxs("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", children: [_jsx("circle", { ...S, cx: "8", cy: "8", r: "6" }), _jsx("path", { ...S, d: "M8 7.2v4M8 4.8v.6" })] });
|
|
19
|
+
const IcoTrash = () => _jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", children: _jsx("path", { ...S, d: "M3 4.5h10M6 4.5V3h4v1.5M4.5 4.5 5 13.5h6l.5-9" }) });
|
|
20
|
+
const fmtSize = (n) => (n < 1024 ? `${n} B` : n < 1048576 ? `${Math.round(n / 1024)} KB` : `${(n / 1048576).toFixed(1)} MB`);
|
|
21
|
+
const fmtDate = (s) => s ? new Date(s).toLocaleString("it-IT", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) : "—";
|
|
22
|
+
export function Lightbox({ lib }) {
|
|
23
|
+
const i = lib.viewing;
|
|
24
|
+
const file = i === null ? undefined : lib.shown[i];
|
|
25
|
+
const [info, setInfo] = useState(false);
|
|
26
|
+
const [usage, setUsage] = useState(null);
|
|
27
|
+
const [usageError, setUsageError] = useState(false);
|
|
28
|
+
// Tastiera: Esc chiude, le frecce scorrono. Su un visore a schermo intero è il modo in cui
|
|
29
|
+
// ci si aspetta di muoversi, e senza si è costretti a mirare due bottoni piccoli.
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (i === null)
|
|
32
|
+
return;
|
|
33
|
+
const on = (e) => {
|
|
34
|
+
if (e.key === "Escape")
|
|
35
|
+
lib.view(null);
|
|
36
|
+
else if (e.key === "ArrowRight")
|
|
37
|
+
lib.step(1);
|
|
38
|
+
else if (e.key === "ArrowLeft")
|
|
39
|
+
lib.step(-1);
|
|
40
|
+
};
|
|
41
|
+
addEventListener("keydown", on);
|
|
42
|
+
return () => removeEventListener("keydown", on);
|
|
43
|
+
}, [i, lib]);
|
|
44
|
+
// L'uso si richiede per la foto che si sta guardando, non una volta per tutte.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setUsage(null);
|
|
47
|
+
setUsageError(false);
|
|
48
|
+
if (!info || !file)
|
|
49
|
+
return;
|
|
50
|
+
let alive = true;
|
|
51
|
+
api.usage({ keys: [file.key] })
|
|
52
|
+
.then((u) => { if (alive)
|
|
53
|
+
setUsage(u.pages ?? []); })
|
|
54
|
+
.catch(() => {
|
|
55
|
+
if (alive) {
|
|
56
|
+
setUsage([]);
|
|
57
|
+
setUsageError(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return () => { alive = false; };
|
|
61
|
+
}, [info, file?.key]);
|
|
62
|
+
if (i === null || !file)
|
|
63
|
+
return null;
|
|
64
|
+
const folder = lib.prefix.replace(/\/$/, "") || "tutte le foto";
|
|
65
|
+
return (_jsxs("div", { className: "lb", onClick: (e) => { if (e.target === e.currentTarget)
|
|
66
|
+
lib.view(null); }, children: [_jsxs("header", { className: "lb-top", children: [_jsxs("button", { className: "lb-btn", onClick: () => lib.view(null), children: ["\u2039 Torna a ", folder] }), _jsxs("div", { className: "lb-id", children: [_jsx("span", { className: "lb-name", children: file.name }), _jsxs("span", { className: "lb-count", children: [i + 1, " / ", lib.shown.length] })] }), _jsxs("div", { className: "lb-tools", children: [_jsx("a", { className: "lb-btn", href: file.url, download: file.name, title: "Scarica", children: _jsx(IcoDown, {}) }), _jsx("button", { className: "lb-btn" + (info ? " is-on" : ""), onClick: () => setInfo((v) => !v), title: "Informazioni", children: _jsx(IcoInfo, {}) }), lib.mode === "manage" && (_jsx("button", { className: "lb-btn", onClick: () => void lib.remove([file.key]), title: "Cestina", children: _jsx(IcoTrash, {}) }))] })] }), _jsxs("div", { className: "lb-stage", onClick: (e) => { if (e.target === e.currentTarget)
|
|
67
|
+
lib.view(null); }, children: [_jsx("button", { className: "lb-nav lb-prev", disabled: i === 0, onClick: () => lib.step(-1), "aria-label": "Precedente", children: "\u2039" }), _jsx("img", { className: "lb-img", src: file.url, alt: file.name }), _jsx("button", { className: "lb-nav lb-next", disabled: i === lib.shown.length - 1, onClick: () => lib.step(1), "aria-label": "Successiva", children: "\u203A" })] }), info && (_jsx("aside", { className: "lb-info", children: _jsxs("dl", { children: [_jsx("dt", { children: "File" }), _jsx("dd", { children: file.name }), _jsx("dt", { children: "Cartella" }), _jsx("dd", { children: lib.prefix || "(radice)" }), _jsx("dt", { children: "Dimensioni" }), _jsx("dd", { children: file.w && file.h ? `${file.w} × ${file.h} px` : "—" }), _jsx("dt", { children: "Peso" }), _jsx("dd", { children: fmtSize(file.size) }), _jsx("dt", { children: "Scatto" }), _jsx("dd", { children: fmtDate(file.capturedAt) }), _jsx("dt", { children: "Caricata" }), _jsx("dd", { children: fmtDate(file.uploaded) }), _jsx("dt", { children: "Usata in" }), _jsx("dd", { children: usageError
|
|
68
|
+
? "controllo non disponibile"
|
|
69
|
+
: usage === null ? "…" : usage.length ? usage.join(", ") : "nessuna sorgente" })] }) }))] }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MediaLibrary } from "./useMediaLibrary.js";
|
|
2
|
+
export declare const DRAG_TYPE = "application/x-adminkit-keys";
|
|
3
|
+
export declare const DRAG_FOLDERS = "application/x-adminkit-prefixes";
|
|
4
|
+
/** Legge i due tipi e li rimette insieme: chi riceve tratta file e cartelle nello stesso modo. */
|
|
5
|
+
export declare function readDragged(dt: DataTransfer): string[];
|
|
6
|
+
export declare function FolderMenu({ lib, prefix, label, openTo }: {
|
|
7
|
+
lib: MediaLibrary;
|
|
8
|
+
prefix: string;
|
|
9
|
+
label: string;
|
|
10
|
+
openTo?: "left" | "auto";
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
export declare function MediaFolders({ lib, trash }: {
|
|
13
|
+
lib: MediaLibrary;
|
|
14
|
+
/** `leave` riporta ai media: dal cestino, cliccare una cartella deve anche USCIRE dal cestino. */
|
|
15
|
+
trash?: {
|
|
16
|
+
active: boolean;
|
|
17
|
+
open: () => void;
|
|
18
|
+
leave: () => void;
|
|
19
|
+
};
|
|
20
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* Le cartelle del bucket: nome e quante foto contengono.
|
|
3
|
+
*
|
|
4
|
+
* IL NOME CHE SI VEDE È QUELLO SCELTO. In R2 la cartella è un pezzo di chiave, e quella chiave
|
|
5
|
+
* finisce dentro gli URL pubblici delle immagini: dev'essere uno slug. Ma lo slug è un dettaglio
|
|
6
|
+
* del bucket, e chi ha scritto «Hospitality & Interiors» non deve ritrovarsi
|
|
7
|
+
* «hospitality-interiors» sullo schermo. L'etichetta vive nei metadati del `.keep` della
|
|
8
|
+
* cartella; chi non ce l'ha — le cartelle nate prima — mostra il suo slug, come sempre.
|
|
9
|
+
*
|
|
10
|
+
* Sono anche i BERSAGLI dello spostamento — si trascina una foto (o una selezione) sopra una
|
|
11
|
+
* cartella e il file si sposta davvero dentro R2, con i riferimenti nelle pagine riscritti nello
|
|
12
|
+
* stesso giro. Il bersaglio si illumina mentre ci passi sopra: senza, non si capisce se il drop
|
|
13
|
+
* sta per funzionare, e uno spostamento andato a vuoto sembra un bug.
|
|
14
|
+
*
|
|
15
|
+
* «Tutte» accetta solo CARTELLE: è così che una sottocartella torna al primo livello. Una foto
|
|
16
|
+
* no — la Function rifiuta una destinazione vuota per un file, e chi ce la lascia cadere se lo
|
|
17
|
+
* sente dire invece di veder sparire il gesto nel nulla.
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useState } from "react";
|
|
20
|
+
export const DRAG_TYPE = "application/x-adminkit-keys";
|
|
21
|
+
/* Le CARTELLE viaggiano con un tipo loro. Durante un trascinamento i VALORI non si possono
|
|
22
|
+
* leggere — il browser li protegge fino al rilascio — ma i TIPI sì: è l'unico modo di sapere
|
|
23
|
+
* *mentre* si trascina che quel che si ha in mano è una cartella, e quindi di mostrare il
|
|
24
|
+
* bersaglio «porta al primo livello» solo quando ha senso. */
|
|
25
|
+
export const DRAG_FOLDERS = "application/x-adminkit-prefixes";
|
|
26
|
+
/** Legge i due tipi e li rimette insieme: chi riceve tratta file e cartelle nello stesso modo. */
|
|
27
|
+
export function readDragged(dt) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const t of [DRAG_TYPE, DRAG_FOLDERS]) {
|
|
30
|
+
try {
|
|
31
|
+
out.push(...JSON.parse(dt.getData(t) || "[]"));
|
|
32
|
+
}
|
|
33
|
+
catch { /* niente */ }
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/* `openTo` decide da che parte si apre il menu, e non è un vezzo: i contenitori che lo ospitano
|
|
38
|
+
* hanno `overflow: auto`, quindi un menu che esce dal loro bordo viene TAGLIATO. Nella colonna
|
|
39
|
+
* delle cartelle c'è posto solo verso sinistra ("left"); su una cella della griglia dipende da
|
|
40
|
+
* dove sta la cella, e si guarda al momento di aprirlo ("auto"). */
|
|
41
|
+
export function FolderMenu({ lib, prefix, label, openTo = "left" }) {
|
|
42
|
+
const [open, setOpen] = useState(false);
|
|
43
|
+
const [renaming, setRenaming] = useState(false);
|
|
44
|
+
const [name, setName] = useState(label);
|
|
45
|
+
const [side, setSide] = useState("left");
|
|
46
|
+
const close = () => { setOpen(false); setRenaming(false); };
|
|
47
|
+
return (_jsxs("span", { className: "mf-actions", children: [_jsx("button", { className: "mf-more", title: "Altro", onClick: (e) => {
|
|
48
|
+
e.stopPropagation();
|
|
49
|
+
if (openTo === "auto") {
|
|
50
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
51
|
+
setSide(r.left < window.innerWidth / 2 ? "right" : "left");
|
|
52
|
+
}
|
|
53
|
+
setName(label);
|
|
54
|
+
setOpen((v) => !v);
|
|
55
|
+
setRenaming(false);
|
|
56
|
+
}, children: "\u2022\u2022\u2022" }), open && !renaming && (_jsxs("div", { className: `mf-menu is-${side}`, onMouseLeave: close, children: [_jsx("button", { onClick: () => setRenaming(true), children: "Rinomina\u2026" }), _jsx("button", { className: "mf-danger", onClick: () => { close(); void lib.removeFolder(prefix); }, children: "Elimina la cartella\u2026" })] })), open && renaming && (_jsxs("div", { className: `mf-menu mf-menu-form is-${side}`, children: [_jsx("input", { className: "ak-input", autoFocus: true, value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
|
|
57
|
+
if (e.key === "Escape")
|
|
58
|
+
close();
|
|
59
|
+
if (e.key === "Enter" && name.trim()) {
|
|
60
|
+
close();
|
|
61
|
+
void lib.renameFolder(prefix, name);
|
|
62
|
+
}
|
|
63
|
+
} }), _jsx("span", { className: "ak-hint", children: "Cambia anche l'indirizzo delle foto dentro. Le pagine del sito vengono aggiornate; i link scritti a mano altrove no." }), _jsxs("div", { className: "mf-menu-row", children: [_jsx("button", { className: "ak-btn ak-btn-primary", disabled: !name.trim(), onClick: () => { close(); void lib.renameFolder(prefix, name); }, children: "Rinomina" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: close, children: "Annulla" })] })] }))] }));
|
|
64
|
+
}
|
|
65
|
+
const IcoTrash = () => (_jsx("svg", { className: "mf-ico", viewBox: "0 0 16 16", width: "15", height: "15", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round", d: "M3 4.5h10M6 4.5V3h4v1.5M4.5 4.5 5 13.5h6l.5-9" }) }));
|
|
66
|
+
/* `trash` è facoltativo perché il cestino è una ROTTA, e le rotte le conosce l'applicazione, non
|
|
67
|
+
* questo componente. Sta qui in fondo, sotto «+ Cartella», perché è l'ultimo posto dove finiscono
|
|
68
|
+
* le foto: nella colonna che elenca dove stanno, non in una barra da un'altra parte. */
|
|
69
|
+
export function MediaFolders({ lib, trash }) {
|
|
70
|
+
const [over, setOver] = useState(null);
|
|
71
|
+
const [draggingFolder, setDraggingFolder] = useState(false);
|
|
72
|
+
const canDrop = lib.mode === "manage";
|
|
73
|
+
/* Nel cestino non si è in nessuna cartella: senza questo, «Tutte» resterebbe accesa insieme a
|
|
74
|
+
«Cestino» e la colonna indicherebbe due posti nello stesso momento. */
|
|
75
|
+
const here = (p) => !trash?.active && (p === "" ? lib.prefix === "" : lib.prefix.startsWith(p));
|
|
76
|
+
/* Dal cestino, aprire una cartella deve anche riportare ai media: cambiare la cartella e
|
|
77
|
+
restare a guardare il cestino è un click che sembra non fare niente. */
|
|
78
|
+
const goTo = (p) => { if (trash?.active)
|
|
79
|
+
trash.leave(); lib.go(p); };
|
|
80
|
+
const drop = (dest) => (e) => {
|
|
81
|
+
setOver(null);
|
|
82
|
+
if (!canDrop)
|
|
83
|
+
return;
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
const ids = readDragged(e.dataTransfer);
|
|
86
|
+
if (ids.length)
|
|
87
|
+
void lib.move(ids, dest);
|
|
88
|
+
};
|
|
89
|
+
/* Vero mentre si trascina una cartella, ovunque nella pagina. Serve a far comparire il
|
|
90
|
+
bersaglio del primo livello solo in quel momento: una riga fissa «porta qui» sarebbe rumore
|
|
91
|
+
per il 99% del tempo, e prima quel bersaglio non esisteva proprio — la sola via era «Tutte»,
|
|
92
|
+
che nessuno poteva indovinare. */
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!canDrop)
|
|
95
|
+
return;
|
|
96
|
+
const start = (e) => setDraggingFolder(!!e.dataTransfer?.types.includes(DRAG_FOLDERS));
|
|
97
|
+
const end = () => setDraggingFolder(false);
|
|
98
|
+
addEventListener("dragstart", start);
|
|
99
|
+
addEventListener("dragend", end);
|
|
100
|
+
addEventListener("drop", end);
|
|
101
|
+
return () => {
|
|
102
|
+
removeEventListener("dragstart", start);
|
|
103
|
+
removeEventListener("dragend", end);
|
|
104
|
+
removeEventListener("drop", end);
|
|
105
|
+
};
|
|
106
|
+
}, [canDrop]);
|
|
107
|
+
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", onClick: () => goTo(""), children: _jsx("span", { className: "mf-name", children: "Senza cartella" }) }) }) }), _jsx("ul", { className: "mf-list", children: lib.folders.map((f) => (_jsxs("li", {
|
|
108
|
+
/* Accesa anche quando si è DENTRO una sua sottocartella: la colonna risponde a
|
|
109
|
+
«dove sono», e stando in food/piatti-2024/ la risposta è ancora food. */
|
|
110
|
+
className: "mf-item"
|
|
111
|
+
+ (here(f.prefix) ? " is-on" : "")
|
|
112
|
+
+ (over === f.prefix ? " is-over" : ""), onDragOver: (e) => { if (canDrop) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
setOver(f.prefix);
|
|
115
|
+
} }, onDragLeave: () => setOver((p) => (p === f.prefix ? null : p)), onDrop: drop(f.prefix), children: [_jsxs("button", { className: "mf-open", 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", 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", onClick: trash.open, children: [_jsx(IcoTrash, {}), _jsx("span", { className: "mf-name", children: "Cestino" })] }) }) }))] }));
|
|
116
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AssetFile } from "../../api.js";
|
|
2
|
+
import { type MediaLibrary } from "./useMediaLibrary.js";
|
|
3
|
+
export declare function MediaGrid({ lib, onPick, onCancel }: {
|
|
4
|
+
lib: MediaLibrary;
|
|
5
|
+
/** Solo in modalità "pick": chi ha aperto il modale riceve i file scelti. */
|
|
6
|
+
onPick?: (files: AssetFile[]) => void;
|
|
7
|
+
onCancel?: () => void;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/* La griglia delle foto.
|
|
3
|
+
*
|
|
4
|
+
* Tre scelte che vale la pena spiegare, perché sono il motivo per cui questa griglia esiste:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Le foto non si tagliano.** Prima ogni miniatura era un quadrato con `object-fit: cover`:
|
|
7
|
+
* un verticale veniva mostrato come un quadrato, e per riconoscere una foto in mezzo a cento
|
|
8
|
+
* bisognava aprirla. Ogni cella ha un'altezza fissa e dentro ci sta l'immagine intera, con il
|
|
9
|
+
* suo rapporto — che è anche il modo in cui la si riconosce da lontano.
|
|
10
|
+
* 2. **Il click apre, la casella seleziona.** Sono due gesti diversi con due bersagli diversi.
|
|
11
|
+
* Nel modale di scelta invece il click seleziona: là aprire non serve a niente.
|
|
12
|
+
* 3. **La barra in alto cambia forma quando c'è una selezione.** Le azioni che valgono solo su
|
|
13
|
+
* una selezione non stanno sullo schermo quando non c'è niente di selezionato.
|
|
14
|
+
*
|
|
15
|
+
* VISORE E DIALOGO STANNO FUORI DA `.mg-wrap`, e non è pignoleria: `.mg-wrap` è una zona di
|
|
16
|
+
* rilascio che carica i file lasciati cadere sopra, e gli eventi React risalgono l'albero dei
|
|
17
|
+
* COMPONENTI, non quello visivo. Con il dialogo dentro, un drop sulla sua area finiva anche a
|
|
18
|
+
* `.mg-wrap`: stessi file caricati due volte, e `uniqueKey` li salvava come `foto.jpg` e
|
|
19
|
+
* `foto-2.jpg`. Una sovrapposizione non deve mai essere figlia di una zona di rilascio.
|
|
20
|
+
*/
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { SORT_LABELS } from "./useMediaLibrary.js";
|
|
23
|
+
import { DRAG_TYPE, DRAG_FOLDERS, readDragged, FolderMenu } from "./MediaFolders.js";
|
|
24
|
+
import { Lightbox } from "./Lightbox.js";
|
|
25
|
+
import { UploadDialog } from "./UploadDialog.js";
|
|
26
|
+
/* Scaricare più file significa più download: il browser ne consente una raffica breve dallo
|
|
27
|
+
* stesso gesto dell'utente, e non c'è modo di fare uno zip senza un endpoint che lo costruisca. */
|
|
28
|
+
function download(urls) {
|
|
29
|
+
urls.forEach(({ url, name }, i) => {
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
const a = document.createElement("a");
|
|
32
|
+
a.href = url;
|
|
33
|
+
a.download = name;
|
|
34
|
+
a.style.display = "none";
|
|
35
|
+
document.body.appendChild(a);
|
|
36
|
+
a.click();
|
|
37
|
+
a.remove();
|
|
38
|
+
}, i * 250);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function MoveMenu({ lib, keys }) {
|
|
42
|
+
const [open, setOpen] = useState(false);
|
|
43
|
+
const dest = lib.folders.filter((f) => f.prefix !== lib.prefix);
|
|
44
|
+
return (_jsxs("span", { className: "mg-menu-wrap", children: [_jsx("button", { className: "mg-act", onClick: () => setOpen((v) => !v), disabled: !dest.length, children: "Sposta in\u2026" }), open && (_jsx("div", { className: "mg-menu", onMouseLeave: () => setOpen(false), children: dest.map((f) => (_jsxs("button", { onClick: () => { setOpen(false); void lib.move(keys, f.prefix); }, children: [f.label || f.name, " ", _jsx("span", { className: "mf-count", children: f.count })] }, f.prefix))) }))] }));
|
|
45
|
+
}
|
|
46
|
+
/* Una cartella dentro la griglia. Fa tre cose che un chip non poteva fare: si apre, si
|
|
47
|
+
* seleziona insieme alle foto, e si trascina — dentro un'altra cartella qui, o sulla colonna di
|
|
48
|
+
* sinistra per riportarla al primo livello. Ed è anche un BERSAGLIO: ci si lasciano cadere foto
|
|
49
|
+
* o altre cartelle. */
|
|
50
|
+
function FolderCell({ lib, folder, manage }) {
|
|
51
|
+
const [over, setOver] = useState(false);
|
|
52
|
+
const on = lib.selected.has(folder.prefix);
|
|
53
|
+
const name = folder.label || folder.name;
|
|
54
|
+
return (_jsxs("figure", { className: "mg-item mg-folder" + (on ? " is-on" : "") + (over ? " is-over" : ""), draggable: manage, onDragStart: (e) => {
|
|
55
|
+
const ids = on && lib.selected.size ? [...lib.selected] : [folder.prefix];
|
|
56
|
+
// Due tipi, non uno: durante il trascinamento i valori non si leggono, i tipi sì —
|
|
57
|
+
// ed è così che la colonna sa di avere in mano una cartella e offre il primo livello.
|
|
58
|
+
const files = ids.filter((i) => !i.endsWith("/"));
|
|
59
|
+
const dirs = ids.filter((i) => i.endsWith("/"));
|
|
60
|
+
if (files.length)
|
|
61
|
+
e.dataTransfer.setData(DRAG_TYPE, JSON.stringify(files));
|
|
62
|
+
if (dirs.length)
|
|
63
|
+
e.dataTransfer.setData(DRAG_FOLDERS, JSON.stringify(dirs));
|
|
64
|
+
e.dataTransfer.effectAllowed = "move";
|
|
65
|
+
},
|
|
66
|
+
/* Si accende per tutto TRANNE i file trascinati dal Finder: quelli devono continuare a
|
|
67
|
+
salire fino alla griglia, che li carica. Il controllo è "non sono file di sistema" e non
|
|
68
|
+
"è roba nostra": il tipo dei nostri dati non è leggibile in ogni passaggio del trascinamento,
|
|
69
|
+
e un bersaglio che non si accende sembra un bersaglio che non funziona. */
|
|
70
|
+
onDragOver: (e) => {
|
|
71
|
+
if (!manage || e.dataTransfer.types.includes("Files"))
|
|
72
|
+
return;
|
|
73
|
+
/* Si accende anche quando la cartella trascinata è questa stessa: durante il
|
|
74
|
+
trascinamento i valori non sono leggibili, quindi non si può sapere. Il rifiuto
|
|
75
|
+
arriva al rilascio, con un messaggio che dice perché. */
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
setOver(true);
|
|
78
|
+
}, onDragLeave: () => setOver(false), onDrop: (e) => {
|
|
79
|
+
setOver(false);
|
|
80
|
+
if (!manage)
|
|
81
|
+
return;
|
|
82
|
+
const ids = readDragged(e.dataTransfer);
|
|
83
|
+
if (!ids.length)
|
|
84
|
+
return; // non è roba nostra: lascia fare a chi sta sopra
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
void lib.move(ids, folder.prefix);
|
|
87
|
+
}, children: [_jsxs("div", { className: "mg-frame", onClick: () => 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", 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" }) }))] }));
|
|
88
|
+
}
|
|
89
|
+
export function MediaGrid({ lib, onPick, onCancel }) {
|
|
90
|
+
const manage = lib.mode === "manage";
|
|
91
|
+
const sel = lib.selected;
|
|
92
|
+
const allOn = lib.shown.length > 0 && lib.shown.every((f) => sel.has(f.key));
|
|
93
|
+
/* `dragDepth` e non un booleano: `dragenter`/`dragleave` scattano anche passando da un figlio
|
|
94
|
+
all'altro, quindi con un booleano la zona si spegne mentre ci si muove sopra. Si conta. */
|
|
95
|
+
const [dragDepth, setDragDepth] = useState(0);
|
|
96
|
+
/* Nostro o del Finder? Se il trascinamento porta il nostro tipo è uno spostamento interno e la
|
|
97
|
+
zona di caricamento non c'entra. Il controllo è "NON è roba nostra" invece di "è un file":
|
|
98
|
+
`types` non contiene "Files" allo stesso modo in ogni browser, e con quel controllo il
|
|
99
|
+
caricamento non partiva affatto dove il nome era diverso. */
|
|
100
|
+
const external = (e) => manage && !e.dataTransfer.types.includes(DRAG_TYPE) && !e.dataTransfer.types.includes(DRAG_FOLDERS);
|
|
101
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mg-wrap" + (dragDepth > 0 ? " is-dropping" : ""),
|
|
102
|
+
/* Trascinare file dal Finder sulla griglia carica, senza passare dal dialogo: è il gesto
|
|
103
|
+
più corto e va lasciato funzionare. Il dialogo serve a chi cerca un bottone.
|
|
104
|
+
La zona è ALTA QUANTO IL PANNELLO (`.mg-wrap { min-height: 100% }`): prima era alta
|
|
105
|
+
quanto il suo contenuto, quindi in una cartella vuota — cioè proprio quando si carica —
|
|
106
|
+
erano quaranta pixel in cima allo schermo e il resto della pagina non riceveva niente. */
|
|
107
|
+
onDragEnter: (e) => { if (external(e)) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
setDragDepth((n) => n + 1);
|
|
110
|
+
} }, onDragOver: (e) => { if (external(e))
|
|
111
|
+
e.preventDefault(); }, onDragLeave: (e) => { if (external(e))
|
|
112
|
+
setDragDepth((n) => Math.max(0, n - 1)); }, onDrop: (e) => {
|
|
113
|
+
setDragDepth(0);
|
|
114
|
+
if (!manage || !e.dataTransfer.files.length)
|
|
115
|
+
return;
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
void lib.upload(e.dataTransfer.files);
|
|
118
|
+
}, children: [dragDepth > 0 && (_jsx("div", { className: "mg-dropzone", children: _jsxs("span", { children: ["Rilascia per caricare in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella" })] }) })), _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, ref: (el) => { if (el)
|
|
119
|
+
el.indeterminate = false; }, onChange: () => lib.setAll(true) }), _jsx("span", { children: "Seleziona tutto" })] })), _jsx("span", { className: "ak-spacer" }), _jsxs("label", { className: "ak-hint", children: ["ordina per", " ", _jsx("select", { className: "mg-sort", value: lib.sortKey, 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", 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, ref: (el) => { if (el)
|
|
120
|
+
el.indeterminate = !allOn; }, onChange: () => lib.setAll(!allOn) }), _jsxs("span", { children: [sel.size, " selezionate"] })] }), !allOn && lib.shown.length > sel.size && (_jsxs("button", { className: "mg-act is-link", onClick: () => lib.setAll(true), children: ["Seleziona tutte le ", lib.shown.length] })), manage && (_jsxs(_Fragment, { children: [lib.shown.some((f) => sel.has(f.key)) && (_jsx("button", { className: "mg-act", onClick: () => download(lib.shown.filter((f) => sel.has(f.key)).map((f) => ({ url: f.url, name: f.name }))), children: "Scarica" })), _jsx(MoveMenu, { lib: lib, keys: [...sel] }), _jsx("button", { className: "mg-act is-danger", onClick: () => void lib.remove([...sel]), children: "Elimina" })] })), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "mg-act", onClick: () => lib.setAll(false), children: "Deseleziona" })] })) }), lib.error && _jsx("p", { className: "ak-error", onClick: lib.clearError, children: lib.error }), lib.progress && (_jsxs("p", { className: "mg-progress", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026 \u2014 non chiudere la pagina"] })), 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", 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 }, 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) => {
|
|
121
|
+
const on = sel.has(f.key);
|
|
122
|
+
return (_jsxs("figure", { className: "mg-item" + (on ? " is-on" : ""), draggable: manage, onDragStart: (e) => {
|
|
123
|
+
// Si trascina la SELEZIONE se la foto ne fa parte, altrimenti solo quella.
|
|
124
|
+
const ids = on && sel.size ? [...sel] : [f.key];
|
|
125
|
+
const files = ids.filter((i) => !i.endsWith("/"));
|
|
126
|
+
const dirs = ids.filter((i) => i.endsWith("/"));
|
|
127
|
+
if (files.length)
|
|
128
|
+
e.dataTransfer.setData(DRAG_TYPE, JSON.stringify(files));
|
|
129
|
+
if (dirs.length)
|
|
130
|
+
e.dataTransfer.setData(DRAG_FOLDERS, JSON.stringify(dirs));
|
|
131
|
+
e.dataTransfer.effectAllowed = "move";
|
|
132
|
+
}, children: [_jsxs("div", { className: "mg-frame", onClick: () => (manage ? lib.view(i) : lib.toggle(f.key)), children: [_jsx("img", { className: "mg-img", src: f.url, alt: "", loading: "lazy",
|
|
133
|
+
/* Il rapporto è noto prima che l'immagine arrivi (lo dà l'elenco): la cella
|
|
134
|
+
riserva subito l'ingombro giusto e la griglia non salta mentre si carica. */
|
|
135
|
+
style: { aspectRatio: f.w && f.h ? `${f.w} / ${f.h}` : "3 / 2" } }), _jsx("button", { className: "mg-check", "aria-label": on ? "Deseleziona" : "Seleziona", onClick: (e) => { e.stopPropagation(); lib.toggle(f.key); }, children: on ? "✓" : "" })] }), _jsx("figcaption", { className: "mg-cap", title: f.name, children: f.name })] }, f.key));
|
|
136
|
+
}) }), !lib.busy && lib.shown.length === 0 && (_jsx("p", { className: "ak-hint", children: lib.prefix === "" && lib.folders.length
|
|
137
|
+
? "Nessuna foto fuori dalle cartelle. Scegline una qui a fianco."
|
|
138
|
+
: "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", onClick: () => onCancel?.(), children: "Annulla" }), _jsxs("button", { className: "ak-btn ak-btn-primary", disabled: !sel.size, onClick: () => onPick?.(lib.shown.filter((f) => sel.has(f.key))), children: ["Usa ", sel.size > 0 ? `(${sel.size})` : ""] })] }))] }), _jsx(Lightbox, { lib: lib }), manage && _jsx(UploadDialog, { lib: lib })] }));
|
|
139
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* Il dialogo di caricamento.
|
|
3
|
+
*
|
|
4
|
+
* Dice sempre DOVE finiranno le foto. Il caricamento va nella cartella che si sta guardando, e
|
|
5
|
+
* senza scriverlo è una scommessa: si carica dalla vista «Tutte» e le foto atterrano nella
|
|
6
|
+
* radice, dove nessuno le cerca più.
|
|
7
|
+
*
|
|
8
|
+
* Si chiude da solo quando ha finito, ma solo se è andato tutto bene: se qualcosa non è passato
|
|
9
|
+
* il messaggio resta sullo schermo invece di sparire con la finestra.
|
|
10
|
+
*/
|
|
11
|
+
import { useRef, useState } from "react";
|
|
12
|
+
export function UploadDialog({ lib }) {
|
|
13
|
+
const input = useRef(null);
|
|
14
|
+
const [over, setOver] = useState(false);
|
|
15
|
+
if (!lib.uploadOpen)
|
|
16
|
+
return null;
|
|
17
|
+
const send = async (list) => {
|
|
18
|
+
if (!list || !list.length)
|
|
19
|
+
return;
|
|
20
|
+
// `upload` dice se sono passati tutti. Guardare `lib.error` da qui non funzionerebbe: è il
|
|
21
|
+
// valore catturato prima del render, quindi sarebbe sempre uguale a sé stesso.
|
|
22
|
+
if (await lib.upload(list))
|
|
23
|
+
lib.setUploadOpen(false);
|
|
24
|
+
};
|
|
25
|
+
return (_jsx("div", { className: "up", onClick: (e) => { if (e.target === e.currentTarget)
|
|
26
|
+
lib.setUploadOpen(false); }, children: _jsxs("div", { className: "up-panel", children: [_jsx("button", { className: "up-close", onClick: () => lib.setUploadOpen(false), "aria-label": "Chiudi", children: "\u00D7" }), _jsxs("div", { className: "up-drop" + (over ? " is-over" : ""),
|
|
27
|
+
/* `stopPropagation` oltre a `preventDefault`: il dialogo è una sovrapposizione, ma
|
|
28
|
+
per React resta un nodo dell'albero e l'evento continuerebbe a salire. Oggi sopra
|
|
29
|
+
non c'è più nessuna zona di rilascio (il dialogo è fuori da `.mg-wrap`), e questa
|
|
30
|
+
riga serve a che non torni a essercene una senza che nessuno se ne accorga. */
|
|
31
|
+
onDragOver: (e) => { e.preventDefault(); e.stopPropagation(); setOver(true); }, onDragLeave: () => setOver(false), onDrop: (e) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
setOver(false);
|
|
35
|
+
void send(e.dataTransfer.files);
|
|
36
|
+
}, 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: "Trascina qui le foto da caricare" }), _jsx("input", { ref: input, type: "file", multiple: true, accept: "image/*,video/*", hidden: true, onChange: (e) => void send(e.target.files) }), _jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => input.current?.click(), children: "Scegli i file" })] }), _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 })] }) }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type AssetFile, type AssetFolder } from "../../api.js";
|
|
2
|
+
export type SortKey = "uploaded" | "capturedAt" | "name" | "size";
|
|
3
|
+
export declare const SORT_LABELS: Record<SortKey, string>;
|
|
4
|
+
export interface MediaLibrary {
|
|
5
|
+
mode: "manage" | "pick";
|
|
6
|
+
multiple: boolean;
|
|
7
|
+
prefix: string;
|
|
8
|
+
folders: AssetFolder[];
|
|
9
|
+
subfolders: AssetFolder[];
|
|
10
|
+
shown: AssetFile[];
|
|
11
|
+
selected: Set<string>;
|
|
12
|
+
busy: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
/** Avanzamento di un'operazione lunga. `verb` è quello che si sta facendo ("Carico",
|
|
15
|
+
* "Cestino", "Sposto"): senza, un'attesa di trenta secondi non dice cosa sta succedendo e
|
|
16
|
+
* chi guarda ricarica la pagina — interrompendo l'operazione a metà. */
|
|
17
|
+
progress: {
|
|
18
|
+
done: number;
|
|
19
|
+
total: number;
|
|
20
|
+
verb: string;
|
|
21
|
+
} | null;
|
|
22
|
+
sortKey: SortKey;
|
|
23
|
+
sortDir: "asc" | "desc";
|
|
24
|
+
uploadOpen: boolean;
|
|
25
|
+
viewing: number | null;
|
|
26
|
+
go(prefix: string): void;
|
|
27
|
+
reload(): void;
|
|
28
|
+
toggle(key: string): void;
|
|
29
|
+
setAll(on: boolean): void;
|
|
30
|
+
setSort(k: SortKey): void;
|
|
31
|
+
flipDir(): void;
|
|
32
|
+
/** Vero se sono passati tutti: chi ha aperto un dialogo lo chiude solo in quel caso. */
|
|
33
|
+
upload(files: File[] | FileList): Promise<boolean>;
|
|
34
|
+
setUploadOpen(open: boolean): void;
|
|
35
|
+
view(i: number | null): void;
|
|
36
|
+
step(delta: number): void;
|
|
37
|
+
/** File e cartelle insieme: una cartella si riconosce dalla chiave, che finisce con "/". */
|
|
38
|
+
move(ids: string[], dest: string): Promise<void>;
|
|
39
|
+
remove(ids: string[]): Promise<void>;
|
|
40
|
+
/** `parent` esplicito: chi preme il pulsante sa dove sta creando, il pulsante non lo indovina. */
|
|
41
|
+
newFolder(parent: string): Promise<void>;
|
|
42
|
+
/** Cambia il nome per esteso di una cartella — e con lui lo slug, se il nuovo è diverso. */
|
|
43
|
+
renameFolder(prefix: string, label: string): Promise<void>;
|
|
44
|
+
/** Cestina la cartella e tutto ciò che contiene. */
|
|
45
|
+
removeFolder(prefix: string): Promise<void>;
|
|
46
|
+
/** Il nome da mostrare per un prefisso: l'etichetta se c'è, altrimenti lo slug. */
|
|
47
|
+
labelOf(prefix: string): string;
|
|
48
|
+
clearError(): void;
|
|
49
|
+
}
|
|
50
|
+
export declare function useMediaLibrary({ mode, multiple, active, remember }: {
|
|
51
|
+
mode: "manage" | "pick";
|
|
52
|
+
multiple?: boolean;
|
|
53
|
+
active?: boolean;
|
|
54
|
+
remember?: boolean;
|
|
55
|
+
}): MediaLibrary;
|