@cardinali-studio/adminkit 2.2.20 → 2.2.22
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 +38 -2
- package/dist/api.js +13 -0
- package/dist/app/App.d.ts +1 -0
- package/dist/app/App.js +36 -15
- package/dist/app/NextAdminApp.d.ts +8 -0
- package/dist/app/NextAdminApp.js +3 -0
- package/dist/app/lib/admin-interface.d.ts +11 -0
- package/dist/app/lib/admin-interface.js +45 -0
- package/dist/app/next.css +826 -0
- package/dist/app/routes/AdminInterfaceSettings.d.ts +12 -0
- package/dist/app/routes/AdminInterfaceSettings.js +14 -0
- package/dist/app/routes/Deliveries.js +1 -1
- package/dist/app/routes/GallerySectionsEditor.d.ts +1 -1
- package/dist/app/routes/GallerySectionsEditor.js +14 -2
- package/dist/app/routes/HomeEditor.js +2 -2
- package/dist/app/routes/Overview.js +4 -4
- package/dist/app/routes/PageEditor.js +12 -5
- package/dist/app/routes/PortfolioWorksEditor.d.ts +1 -1
- package/dist/app/routes/PortfolioWorksEditor.js +11 -3
- package/dist/app/routes/Requests.js +2 -2
- package/dist/app/routes/SiteSettingsEditor.d.ts +4 -1
- package/dist/app/routes/SiteSettingsEditor.js +3 -2
- package/dist/app/ui/MediaSlot.js +1 -1
- package/dist/app/ui/OpenCard.js +22 -1
- package/dist/app/ui/icons.js +5 -2
- package/dist/form/fields/index.js +2 -1
- package/dist/form/media/Lightbox.js +33 -3
- package/dist/form/media/MediaGrid.js +9 -6
- package/dist/form/media/media-model.d.ts +2 -0
- package/dist/form/media/media-model.js +8 -0
- package/dist/form/media/useMediaLibrary.d.ts +11 -2
- package/dist/form/media/useMediaLibrary.js +89 -24
- package/dist/functions/_shared/d1-media.js +105 -91
- package/dist/functions/_shared/media-alt.js +61 -0
- package/dist/functions/api/assets/delete.js +20 -3
- package/dist/functions/api/assets/media.js +57 -2
- package/dist/functions/api/assets/move.js +5 -1
- package/dist/functions/api/assets/upload.js +3 -1
- package/dist/functions/api/assets.js +5 -2
- package/dist/gallery.d.ts +2 -0
- package/dist/media.d.ts +19 -0
- package/dist/media.js +132 -0
- package/dist/normalize.d.ts +1 -0
- package/dist/normalize.js +2 -1
- package/dist/persistence.d.ts +8 -0
- package/dist/persistence.js +78 -6
- package/dist/theme.css +42 -0
- package/package.json +2 -2
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AdminInterface } from "../lib/admin-interface.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
adminInterface: AdminInterface;
|
|
4
|
+
onAdminInterfaceChange: (next: AdminInterface) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function SiteSettingsIntro(): import("react").JSX.Element;
|
|
7
|
+
export declare function AdminInterfaceSetting({ adminInterface, onAdminInterfaceChange }: Props): import("react").JSX.Element;
|
|
8
|
+
export declare function SiteSettingsUnavailable({ error, onRetry, adminInterface, onAdminInterfaceChange, }: Props & {
|
|
9
|
+
error: string;
|
|
10
|
+
onRetry: () => void;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function SiteSettingsIntro() {
|
|
3
|
+
return (_jsxs("header", { className: "st-intro", children: [_jsx("span", { children: "Impostazioni generali" }), _jsx("h2", { children: "Sito" }), _jsx("p", { children: "Identit\u00E0, descrizione e accesso al sito pubblico." })] }));
|
|
4
|
+
}
|
|
5
|
+
export function AdminInterfaceSetting({ adminInterface, onAdminInterfaceChange }) {
|
|
6
|
+
const next = adminInterface === "next";
|
|
7
|
+
return (_jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Pannello di amministrazione" }), _jsxs("div", { className: "st-row", children: [_jsxs("div", { className: "st-copy", children: [_jsx("h3", { children: "Anteprima della nuova interfaccia" }), _jsx("p", { children: "Attiva nel solo browser in uso l\u2019interfaccia su cui stiamo lavorando. Il sito pubblico e gli altri amministratori non cambiano." })] }), _jsx("div", { className: "st-control", children: _jsxs("div", { className: "st-stack", children: [_jsxs("label", { className: "st-toggle", children: [_jsx("input", { type: "checkbox", checked: next, onChange: (event) => onAdminInterfaceChange(event.target.checked ? "next" : "current") }), _jsx("span", { className: "st-toggle-track", "aria-hidden": "true", children: _jsx("span", {}) }), _jsx("span", { children: next ? "On · Nuova interfaccia" : "Off · Interfaccia attuale" })] }), _jsx("p", { className: "st-inline-help", children: "La scelta \u00E8 immediata e non richiede di salvare o pubblicare il sito." })] }) })] })] }));
|
|
8
|
+
}
|
|
9
|
+
/* Il controllo dell'interfaccia non dipende dalla persistenza editoriale. In locale D1 può
|
|
10
|
+
* essere intenzionalmente assente: l'errore resta visibile, ma non deve bloccare il toggle che
|
|
11
|
+
* serve proprio a provare il pannello in quel contesto. */
|
|
12
|
+
export function SiteSettingsUnavailable({ error, onRetry, adminInterface, onAdminInterfaceChange, }) {
|
|
13
|
+
return (_jsxs("section", { className: "settings-editor", children: [_jsx(SiteSettingsIntro, {}), _jsx(AdminInterfaceSetting, { adminInterface: adminInterface, onAdminInterfaceChange: onAdminInterfaceChange }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Impostazioni del sito non disponibili" }), _jsx("p", { className: "ak-error", children: error }), _jsx("button", { className: "ak-btn", type: "button", onClick: onRetry, children: "Riprova" })] })] }));
|
|
14
|
+
}
|
|
@@ -340,7 +340,7 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
340
340
|
? "Il link vale già mentre l'upload continua: chi lo apre vede che le foto stanno arrivando."
|
|
341
341
|
: "Aggiungi i file: il caricamento parte subito e il resto si compila mentre va." })] }))] }), _jsxs("div", { className: "dl-side", children: [_jsxs(Group, { title: "Chi pu\u00F2 aprirlo", children: [_jsxs("label", { className: "dl-radio", children: [_jsx("input", { type: "radio", name: "dl-access", checked: !draft.pin, onChange: () => change({ pin: "" }) }), _jsxs("span", { children: [_jsx("strong", { children: "Chiunque abbia il link" }), _jsx("small", { children: "Il link \u00E8 lungo e casuale: non si indovina." })] })] }), _jsxs("label", { className: "dl-radio", children: [_jsx("input", { type: "radio", name: "dl-access", checked: !!draft.pin, onChange: () => change({ pin: draft.pin || makeCode() }) }), _jsxs("span", { children: [_jsx("strong", { children: "Serve anche un codice" }), _jsx("small", { children: "Da mandare insieme al link. Dopo dieci errori si blocca per un'ora." })] })] }), !!draft.pin && (_jsxs("div", { className: "dl-code", children: [_jsx("input", { value: draft.pin, spellCheck: false, onChange: (e) => change({ pin: e.target.value.toUpperCase() }) }), _jsx("button", { className: "ak-btn", onClick: () => change({ pin: makeCode() }), children: "Altro" })] }))] }), _jsxs(Group, { title: "Cosa riceve", note: packages.length
|
|
342
342
|
? "Si decide prima di caricare: per cambiarle bisognerebbe ricaricare i file."
|
|
343
|
-
: undefined, children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.gallery, disabled: !!packages.length, onChange: (e) => change({ gallery: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Vede le foto sulla pagina" }), _jsx("small", { children: "Anteprime e ingrandimento, senza scaricare niente. Da telefono \u00E8 la differenza fra un link e
|
|
343
|
+
: undefined, children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.gallery, disabled: !!packages.length, onChange: (e) => change({ gallery: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Vede le foto sulla pagina" }), _jsx("small", { children: "Anteprime e ingrandimento, senza scaricare niente. Da telefono \u00E8 la differenza fra un link e un Transfer." })] })] }), _jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.light, disabled: !!packages.length, onChange: (e) => change({ light: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Pu\u00F2 scaricare una versione leggera" }), _jsx("small", { children: "Un secondo download, adatto a social e web. Gli originali ci sono sempre." })] })] })] }), _jsxs(Group, { title: "Per quanto resta scaricabile", children: [_jsxs("select", { className: "dl-select", value: draft.duration, onChange: (e) => change({ duration: e.target.value }), children: [delivery && _jsx("option", { value: "keep", children: delivery.filesExpiresAt
|
|
344
344
|
? `Fino al ${formatDate(delivery.filesExpiresAt)}`
|
|
345
345
|
: "Per sempre" }), _jsx("option", { value: "7", children: "7 giorni" }), _jsx("option", { value: "30", children: "30 giorni" }), _jsx("option", { value: "180", children: "6 mesi" }), _jsx("option", { value: "365", children: "1 anno" }), _jsx("option", { value: "forever", children: "Per sempre" })] }), _jsx("p", { className: "dl-note", children: expiry
|
|
346
346
|
? `Dopo il ${formatDate(expiry)} il link continua ad aprirsi, ma i file non si scaricano più.`
|
|
@@ -31,10 +31,12 @@ import { useEffect, useRef, useState } from "react";
|
|
|
31
31
|
import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from "@dnd-kit/core";
|
|
32
32
|
import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
|
|
33
33
|
import { CSS } from "@dnd-kit/utilities";
|
|
34
|
+
import { assetAltText } from "../../index.js";
|
|
34
35
|
import { GALLERY_BLOCK_ID, emptySection } from "../../index.js";
|
|
35
36
|
import { normalizeSections, withGalleryBlock } from "../../normalize.js";
|
|
36
37
|
import { ParagraphEditor } from "../ui/ParagraphEditor";
|
|
37
38
|
import { Icon } from "../ui/icons";
|
|
39
|
+
import { site } from "../site";
|
|
38
40
|
function sectionId() {
|
|
39
41
|
return `section-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
|
40
42
|
}
|
|
@@ -58,8 +60,18 @@ function SortableSection({ section, index, onPatch, onDuplicate, onRemove, rende
|
|
|
58
60
|
const chooseImage = async () => {
|
|
59
61
|
const selected = await pickMediaFiles?.({ multiple: false });
|
|
60
62
|
const file = selected?.[0];
|
|
61
|
-
if (file)
|
|
62
|
-
|
|
63
|
+
if (file) {
|
|
64
|
+
const locale = site().languages?.default ?? "default";
|
|
65
|
+
onPatch({
|
|
66
|
+
image: file.url,
|
|
67
|
+
w: file.w ?? null,
|
|
68
|
+
h: file.h ?? null,
|
|
69
|
+
/* Il valore del Media è il default iniziale; questo campo resta modificabile e quindi
|
|
70
|
+
* costituisce l'override del singolo utilizzo. Una decorativa produce deliberatamente
|
|
71
|
+
* alt="", non il nome del file. */
|
|
72
|
+
alt: assetAltText(file, locale),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
63
75
|
};
|
|
64
76
|
return (_jsxs("article", { ref: setNodeRef, className: ["gs-card", `is-${section.type}`, isDragging ? "is-dragging" : ""]
|
|
65
77
|
.filter(Boolean).join(" "), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("header", { className: "gs-card-head", children: [_jsx("button", { type: "button", className: "gs-drag", title: "Trascina per riordinare", "aria-label": `Riordina sezione ${labelOf(section.type)}`, ...attributes, ...listeners, children: _jsx(Icon.grip, {}) }), _jsx("span", { className: "gs-card-icon", children: _jsx(SectionIcon, { type: section.type }) }), _jsx("strong", { children: labelOf(section.type) }), _jsx("span", { className: "gs-position", children: index + 1 }), !isGallery && (_jsx("button", { type: "button", className: "gs-head-action", onClick: onDuplicate, children: "Duplica" })), (!isGallery || section.images.length > 0) && (_jsx("button", { type: "button", className: "gs-head-action is-danger", onClick: onRemove, children: "Elimina" }))] }), isGallery && (_jsx("div", { className: "gs-gallery", children: renderGallery?.(section.images, (images) => onPatch({ images })) })), section.type === "header" && (_jsxs("div", { className: "gs-fields", children: [_jsxs("label", { children: [_jsxs("span", { children: ["Occhiello ", _jsx("small", { children: "facoltativo" })] }), _jsx("input", { value: section.eyebrow, maxLength: 120, placeholder: "Es. Fotografia gastronomica", onChange: (event) => onPatch({ eyebrow: event.target.value }) })] }), _jsxs("label", { children: [_jsx("span", { children: "Titolo" }), _jsx("input", { value: section.heading, maxLength: 200, placeholder: "Titolo della sezione", onChange: (event) => onPatch({ heading: event.target.value }) })] }), _jsxs("label", { children: [_jsxs("span", { children: ["Introduzione ", _jsx("small", { children: "facoltativa" })] }), _jsx("textarea", { rows: 4, value: section.lead, maxLength: 800, placeholder: "Una breve apertura sotto il titolo.", onChange: (event) => onPatch({ lead: event.target.value }) })] })] })), section.type === "text" && (_jsx("div", { className: "gs-fields", children: _jsxs("label", { children: [_jsx("span", { children: "Testo" }), _jsx(ParagraphEditor, { value: section.paragraphs, onChange: (paragraphs) => onPatch({ paragraphs }) }), _jsx("small", { children: "Seleziona una parola per metterla in grassetto, in corsivo o per collegarla." })] }) })), section.type === "image" && (_jsxs("div", { className: "gs-image-editor", children: [_jsx("button", { type: "button", className: "gs-image-preview", onClick: () => void chooseImage(), children: section.image ? _jsx("img", { src: section.image, alt: "" }) : (_jsxs("span", { children: [_jsx(Icon.media, {}), " Scegli un\u2019immagine"] })) }), _jsxs("div", { className: "gs-image-fields", children: [_jsxs("div", { className: "gs-image-actions", children: [_jsx("button", { type: "button", className: "btn-soft", onClick: () => void chooseImage(), children: section.image ? "Cambia immagine" : "Scegli immagine" }), section.image && (_jsx("button", { type: "button", className: "btn-soft is-quiet", onClick: () => onPatch({ image: "", w: null, h: null }), children: "Rimuovi" }))] }), _jsxs("label", { children: [_jsx("span", { children: "Testo alternativo" }), _jsx("input", { value: section.alt, maxLength: 300, placeholder: "Descrive l\u2019immagine a chi non pu\u00F2 vederla", onChange: (event) => onPatch({ alt: event.target.value }) })] }), _jsxs("label", { children: [_jsxs("span", { children: ["Didascalia ", _jsx("small", { children: "facoltativa" })] }), _jsx("input", { value: section.caption, maxLength: 300, onChange: (event) => onPatch({ caption: event.target.value }) })] }), _jsxs("div", { className: "gs-layout", children: [_jsx("span", { children: "Dimensione" }), _jsxs("div", { className: "seg", role: "group", "aria-label": "Dimensione dell\u2019immagine", children: [_jsx("button", { type: "button", className: "seg-btn" + (section.layout === "wide" ? " is-on" : ""), onClick: () => onPatch({ layout: "wide" }), children: "Larga" }), _jsx("button", { type: "button", className: "seg-btn" + (section.layout === "contained" ? " is-on" : ""), onClick: () => onPatch({ layout: "contained" }), children: "Contenuta" })] })] })] })] }))] }));
|
|
@@ -84,7 +84,7 @@ function HeroScreen({ value, onChange, pick }) {
|
|
|
84
84
|
const patch = (next) => onChange({ ...value, hero: { ...hero, ...next } });
|
|
85
85
|
const title = strings(hero.title);
|
|
86
86
|
const narrow = strings(hero.narrow);
|
|
87
|
-
return (_jsx(Screen, { hint: "La prima schermata: la foto a tutta pagina e il titolo sopra.", children: _jsxs("div", { className: "hm-split", children: [_jsxs("div", { className: "hm-photos", children: [_jsx(Photo, { value: str(hero.photo), label: "
|
|
87
|
+
return (_jsx(Screen, { hint: "La prima schermata: la foto a tutta pagina e il titolo sopra.", children: _jsxs("div", { className: "hm-split", children: [_jsxs("div", { className: "hm-photos hm-stacked-photos", children: [_jsx(Photo, { value: str(hero.photo), label: "Sfondo desktop", onPick: () => pick((photo) => patch({ photo })), onClear: () => patch({ photo: "" }) }), _jsx(Photo, { value: str(hero.mobile), label: "Sfondo mobile", ratio: "portrait", onPick: () => pick((mobile) => patch({ mobile })), onClear: () => patch({ mobile: "" }) })] }), _jsxs("div", { className: "hm-side", children: [_jsxs(Block, { title: "Titolo", children: [_jsx("div", { className: "hm-title-preview", "aria-hidden": "true", children: (title.length ? title : ["Il titolo della home"]).map((line, index) => (_jsx("span", { className: title.length ? "" : "is-empty", children: line }, index))) }), _jsx(Rows, { items: title, ids: title.map((_, i) => `riga-${i}`), onChange: (next) => patch({ title: next }), onAdd: () => patch({ title: [...title, ""] }), addLabel: "Aggiungi riga", empty: "Nessuna riga: il titolo non comparirebbe.", render: (line, index, set) => (_jsx("input", { className: "pw-field", value: line, maxLength: 30, placeholder: `Riga ${index + 1}`, "aria-label": `Riga ${index + 1} del titolo`, onChange: (event) => set(event.target.value) })) })] }), _jsxs(Block, { title: "Titolo su schermi stretti", children: [_jsx("p", { className: "pw-empty-note", children: "Su un telefono le righe larghe rimpicciolirebbero tutto il titolo, quindi qui si spezza pi\u00F9 corto. Lasciandolo vuoto si usano comunque le righe qui sopra." }), _jsx(Rows, { items: narrow, ids: narrow.map((_, i) => `stretta-${i}`), onChange: (next) => patch({ narrow: next }), onAdd: () => patch({ narrow: [...narrow, ""] }), addLabel: "Aggiungi riga", render: (line, index, set) => (_jsx("input", { className: "pw-field", value: line, maxLength: 20, placeholder: `Riga ${index + 1}`, "aria-label": `Riga ${index + 1} del titolo su schermi stretti`, onChange: (event) => set(event.target.value) })) })] }), _jsx(Block, { children: _jsx(Field, { label: "Invito allo scroll", note: "in fondo alla schermata", children: _jsx(Text, { value: str(hero.scroll), maxLength: 20, placeholder: "Scroll", onChange: (scroll) => patch({ scroll }) }) }) })] })] }) }));
|
|
88
88
|
}
|
|
89
89
|
/* ---- Statistiche ----------------------------------------------------------------------------- */
|
|
90
90
|
function StatsScreen({ value, onChange, pick }) {
|
|
@@ -92,7 +92,7 @@ function StatsScreen({ value, onChange, pick }) {
|
|
|
92
92
|
const bg = obj(stats.bg);
|
|
93
93
|
const items = list(stats.items).map(obj);
|
|
94
94
|
const patch = (next) => onChange({ ...value, stats: { ...stats, ...next } });
|
|
95
|
-
return (
|
|
95
|
+
return (_jsx(Screen, { hint: "I numeri ruotano uno alla volta, sopra la foto di sfondo.", children: _jsxs("div", { className: "hm-split", children: [_jsxs("div", { className: "hm-photos hm-stacked-photos", children: [_jsx(Photo, { value: str(bg.desktop), label: "Sfondo desktop", onPick: () => pick((desktop) => patch({ bg: { ...bg, desktop } })), onClear: () => patch({ bg: { ...bg, desktop: "" } }) }), _jsx(Photo, { value: str(bg.mobile), label: "Sfondo mobile", ratio: "portrait", onPick: () => pick((mobile) => patch({ bg: { ...bg, mobile } })), onClear: () => patch({ bg: { ...bg, mobile: "" } }) })] }), _jsx("div", { className: "hm-side", children: _jsx(Block, { title: "Numeri", children: _jsx(Rows, { items: items, ids: items.map((_, i) => `numero-${i}`), onChange: (next) => patch({ items: next }), onAdd: () => patch({ items: [...items, { num: "", label: "" }] }), addLabel: "Aggiungi numero", empty: "Nessun numero: la schermata resterebbe con il solo sfondo.", render: (item, _index, set) => (_jsxs(_Fragment, { children: [_jsx("input", { className: "pw-field hm-num", value: str(item.num), maxLength: 12, placeholder: "15+", "aria-label": "Numero", onChange: (event) => set({ ...item, num: event.target.value }) }), _jsx("input", { className: "pw-field", value: str(item.label), maxLength: 60, placeholder: "Years of Experience", "aria-label": "Etichetta", onChange: (event) => set({ ...item, label: event.target.value }) })] })) }) }) })] }) }));
|
|
96
96
|
}
|
|
97
97
|
/* ---- Chi sono ------------------------------------------------------------------------------- */
|
|
98
98
|
function AboutScreen({ value, onChange, pick }) {
|
|
@@ -65,15 +65,15 @@ export function Overview({ deliveries, expiring, requests, pending, features, bo
|
|
|
65
65
|
const hasBookings = features?.appointments === true;
|
|
66
66
|
// Senza «Visitatori» la riga in cima ha tre riquadri, che devono coprire le dodici colonne.
|
|
67
67
|
const statSpan = hasAnalytics ? 3 : 4;
|
|
68
|
-
return (_jsxs("div", { className: "ov", children: [_jsxs("div", { className: "ov-bar", children: [_jsx("p", { className: "ov-bar-hi", children: "Tutto quello che \u00E8 successo mentre non guardavi." }), _jsx("button", { className: "ak-btn ak-btn-quiet", disabled: storage.busy || analytics.busy, onClick: () => { void storage.load(true); analytics.reload(); }, children: storage.busy || analytics.busy ? "Aggiorno…" : "Aggiorna" })] }), _jsxs("div", { className: "ov-grid", children: [hasAnalytics && (_jsx(Stat, { span: 3, label: "Visitatori", icon: Icon.eye, tone: "blue", hint: RANGE_LABEL[analytics.range], value: totals ? int(totals.visitors.value) : null, quiet: analytics.data && !analytics.data.configured ? "Statistiche non collegate" : undefined, delta: totals?.visitors })), _jsx(Stat, { span: statSpan, label: "Transfer attivi", icon: Icon.delivery, tone: "accent", hint: uploading.length ? `+${uploading.length} in caricamento` : `${downloads} download`, value: deliveries ? String(active.length) : null, onClick: () => go("/deliveries") }), _jsx(Stat, { span: statSpan, label: "Spazio in uso", icon: Icon.archive, tone: "quiet", hint: storage.data ? `${int(storage.data.r2.objects)} foto · ${int(storage.data.b2.files)} file
|
|
68
|
+
return (_jsxs("div", { className: "ov", children: [_jsxs("div", { className: "ov-bar", children: [_jsx("p", { className: "ov-bar-hi", children: "Tutto quello che \u00E8 successo mentre non guardavi." }), _jsx("button", { className: "ak-btn ak-btn-quiet", disabled: storage.busy || analytics.busy, onClick: () => { void storage.load(true); analytics.reload(); }, children: storage.busy || analytics.busy ? "Aggiorno…" : "Aggiorna" })] }), _jsxs("div", { className: "ov-grid", children: [hasAnalytics && (_jsx(Stat, { span: 3, label: "Visitatori", icon: Icon.eye, tone: "blue", hint: RANGE_LABEL[analytics.range], value: totals ? int(totals.visitors.value) : null, quiet: analytics.data && !analytics.data.configured ? "Statistiche non collegate" : undefined, delta: totals?.visitors })), _jsx(Stat, { span: statSpan, label: "Transfer attivi", icon: Icon.delivery, tone: "accent", hint: uploading.length ? `+${uploading.length} in caricamento` : `${downloads} download`, value: deliveries ? String(active.length) : null, onClick: () => go("/deliveries") }), _jsx(Stat, { span: statSpan, label: "Spazio in uso", icon: Icon.archive, tone: "quiet", hint: storage.data ? `${int(storage.data.r2.objects)} foto · ${int(storage.data.b2.files)} file trasferiti` : "R2 + B2", value: storage.data ? size(bytes) : null, quiet: storage.error ?? undefined }), _jsxs("button", { className: "ov-tile ov-cta", "data-span": statSpan, onClick: () => go("/deliveries"), children: [_jsxs("span", { className: "ov-cta-copy", children: [_jsx("strong", { children: "Transfer" }), _jsx("span", { children: "Manda le foto a un cliente" })] }), _jsx("span", { className: "ov-cta-go", "aria-hidden": "true", children: _jsx(Icon.delivery, {}) }), _jsx("span", { className: "ov-cta-bars", "aria-hidden": "true", children: BARS.map((h, i) => _jsx("i", { style: { height: `${h}%` } }, i)) }), expiring > 0 && _jsxs("span", { className: "ov-cta-badge", children: [expiring, " in scadenza"] })] }), !hasAnalytics ? (known && _jsx(LockedCard, { feature: "analytics", go: go })) : (_jsxs("section", { className: "ov-tile ov-chart", "data-span": "8", children: [_jsxs("header", { className: "ov-tile-head", children: [_jsxs("div", { children: [_jsx("h2", { children: "Visite al sito" }), _jsxs("p", { children: ["Pagine viste e sessioni", analytics.data?.providerLabel ? `, da ${analytics.data.providerLabel}` : ""] })] }), _jsx("div", { className: "ov-range", role: "group", "aria-label": "Periodo", children: Object.keys(RANGE_LABEL).map((r) => (_jsx("button", { className: "ov-range-btn" + (analytics.range === r ? " is-on" : ""), onClick: () => analytics.setRange(r), children: RANGE_LABEL[r] }, r))) })] }), !analytics.data ? (_jsx("p", { className: "ak-hint ov-pad", children: "Carico\u2026" })) : !analytics.data.configured ? (_jsx(NotConnected, { providers: analytics.data.providers ?? [] })) : (_jsxs(_Fragment, { children: [analytics.error && _jsx("p", { className: "ak-error ov-pad", children: analytics.error }), _jsxs("div", { className: "ov-chart-figures", children: [_jsx(Figure, { label: "Pagine viste", metric: analytics.data.totals?.pageviews }), _jsx(Figure, { label: "Sessioni", metric: analytics.data.totals?.visits }), _jsx(Figure, { label: "Visitatori", metric: analytics.data.totals?.visitors }), _jsx(Figure, { label: "Tempo medio", metric: averageTime(analytics.data.totals), format: seconds })] }), _jsx(Chart, { points: analytics.data.series ?? [], unit: analytics.data.unit ?? "day" })] }))] })), _jsxs("section", { className: "ov-tile", "data-span": "4", children: [_jsx("header", { className: "ov-tile-head", children: _jsxs("div", { children: [_jsx("h2", { children: "Archivi" }), _jsx("p", { children: "Quanto occupano le due librerie" })] }) }), storage.error ? _jsx("p", { className: "ak-error ov-pad", children: storage.error })
|
|
69
69
|
: !storage.data ? _jsx("p", { className: "ak-hint ov-pad", children: "Conto gli oggetti\u2026" })
|
|
70
|
-
: (_jsxs("div", { className: "ov-store", children: [_jsxs("div", { className: "ov-store-rows", children: [_jsx(StoreRow, { name: "Media del sito", where: "R2", bytes: storage.data.r2.bytes, total: bytes, detail: `${int(storage.data.r2.objects)} file`, onClick: () => go("/media") }), _jsx(StoreRow, { name: "
|
|
70
|
+
: (_jsxs("div", { className: "ov-store", children: [_jsxs("div", { className: "ov-store-rows", children: [_jsx(StoreRow, { name: "Media del sito", where: "R2", bytes: storage.data.r2.bytes, total: bytes, detail: `${int(storage.data.r2.objects)} file`, onClick: () => go("/media") }), _jsx(StoreRow, { name: "Transfer", where: "B2", bytes: storage.data.b2.bytes, total: bytes, detail: `${int(storage.data.b2.packages)} pacchetti · ${int(storage.data.b2.files)} file`, onClick: () => go("/deliveries") }), _jsx(StoreRow, { name: "Cestino", where: "R2", bytes: storage.data.r2.trashBytes, total: bytes, detail: storage.data.r2.trashObjects
|
|
71
71
|
? `${int(storage.data.r2.trashObjects)} file recuperabili — svuotalo per liberare spazio`
|
|
72
72
|
: "vuoto", onClick: () => go("/trash") })] }), _jsxs("dl", { className: "ov-store-notes", children: [storage.data.b2.pendingBytes > 0 && (_jsxs(_Fragment, { children: [_jsx("dt", { children: "In cancellazione" }), _jsxs("dd", { children: [size(storage.data.b2.pendingBytes), ", spariscono da soli entro sette giorni"] })] })), storage.data.r2.partial && (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Attenzione" }), _jsx("dd", { children: "Il conteggio dei media si \u00E8 fermato ai primi 20.000 oggetti: il totale \u00E8 per difetto." })] }))] })] }))] }), !hasBookings
|
|
73
73
|
? known && _jsx(LockedCard, { feature: "appointments", go: go })
|
|
74
74
|
: _jsx(BookingsCard, { rows: bookings, pending: bookingsPending, error: bookingsError, go: go }), _jsxs("section", { className: "ov-tile", "data-span": "8", children: [_jsxs("header", { className: "ov-tile-head", children: [_jsxs("div", { children: [_jsx("h2", { children: "Ultimi transfer" }), _jsx("p", { children: "Quante volte \u00E8 stato scaricato ognuno" })] }), _jsx("button", { className: "ov-more", onClick: () => go("/deliveries"), children: "Tutti" })] }), !deliveries ? _jsx("p", { className: "ak-hint ov-pad", children: "Carico\u2026" })
|
|
75
75
|
: recent.length === 0 ? _jsx("p", { className: "ak-hint ov-pad", children: "Nessun transfer spedito, per ora." })
|
|
76
|
-
: (_jsxs("table", { className: "ov-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Transfer" }), _jsx("th", { children: "Spedito" }), _jsx("th", { children: "Peso" }), _jsx("th", { className: "ov-num", children: "Download" }), _jsx("th", {})] }) }), _jsx("tbody", { children: recent.map((d) => (_jsxs("tr", { onClick: () => go(`/deliveries/${d.id}`), children: [_jsxs("td", { children: [_jsx("span", { className: "ov-row-main", children: d.title || "Senza nome" }), _jsx("span", { className: "ov-row-sub", children: d.clientName || "—" })] }), _jsx("td", { className: "ov-when", children: day(d.activatedAt || d.updatedAt) }), _jsx("td", { className: "ov-when", children: size(d.storedBytes) }), _jsx("td", { className: "ov-num" + (d.downloadCount ? "" : " is-zero"), children: d.downloadCount || "—" }), _jsx("td", { className: "ov-state", children: _jsx(StatusPill, { delivery: d }) })] }, d.id))) })] }))] }), _jsxs("section", { className: "ov-tile", "data-span": "4", children: [_jsxs("header", { className: "ov-tile-head", children: [_jsxs("div", { children: [_jsx("h2", { children: "Ultima richiesta" }), _jsx("p", { children: pending > 0 ? `${pending}
|
|
76
|
+
: (_jsxs("table", { className: "ov-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Transfer" }), _jsx("th", { children: "Spedito" }), _jsx("th", { children: "Peso" }), _jsx("th", { className: "ov-num", children: "Download" }), _jsx("th", {})] }) }), _jsx("tbody", { children: recent.map((d) => (_jsxs("tr", { onClick: () => go(`/deliveries/${d.id}`), children: [_jsxs("td", { children: [_jsx("span", { className: "ov-row-main", children: d.title || "Senza nome" }), _jsx("span", { className: "ov-row-sub", children: d.clientName || "—" })] }), _jsx("td", { className: "ov-when", children: day(d.activatedAt || d.updatedAt) }), _jsx("td", { className: "ov-when", children: size(d.storedBytes) }), _jsx("td", { className: "ov-num" + (d.downloadCount ? "" : " is-zero"), children: d.downloadCount || "—" }), _jsx("td", { className: "ov-state", children: _jsx(StatusPill, { delivery: d }) })] }, d.id))) })] }))] }), _jsxs("section", { className: "ov-tile", "data-span": "4", children: [_jsxs("header", { className: "ov-tile-head", children: [_jsxs("div", { children: [_jsx("h2", { children: "Ultima richiesta" }), _jsx("p", { children: pending > 0 ? `${pending} ${pending === 1 ? "non letta" : "non lette"}` : "Dal form del sito" })] }), _jsx("button", { className: "ov-more", onClick: () => go("/requests"), children: "Tutte" })] }), !requests ? _jsx("p", { className: "ak-hint ov-pad", children: "Carico\u2026" })
|
|
77
77
|
: requests.length === 0 ? _jsx("p", { className: "ak-hint ov-pad", children: "Nessuna richiesta, per ora." })
|
|
78
78
|
: _jsx(LastRequest, { row: requests[0], go: go })] }), hasAnalytics && _jsx(Top, { span: 4, title: "Pagine pi\u00F9 viste", note: RANGE_LABEL[analytics.range], rows: analytics.data?.pages ?? [], ready: !!analytics.data?.configured, label: (name) => name === "/" ? "/ (home)" : name }), hasAnalytics && _jsx(Top, { span: 4, title: "Da dove arrivano", note: "Siti che portano al tuo", rows: analytics.data?.referrers ?? [], ready: !!analytics.data?.configured, label: (name) => name.replace(/^https?:\/\//, "") })] })] }));
|
|
79
79
|
}
|
|
@@ -187,7 +187,7 @@ function StoreRow({ name, where, bytes, total, detail, onClick }) {
|
|
|
187
187
|
return (_jsxs("button", { className: "ov-store-row", onClick: onClick, children: [_jsxs("span", { className: "ov-store-top", children: [_jsxs("span", { className: "ov-store-name", children: [name, _jsx("em", { children: where })] }), _jsx("strong", { children: size(bytes) })] }), _jsx("span", { className: "ov-store-track", children: _jsx("i", { style: { width: `${share}%`, minWidth: share > 0 ? undefined : 0 } }) }), _jsxs("span", { className: "ov-store-detail", children: [_jsx("span", { children: detail }), _jsx("em", { children: percent(share) })] })] }));
|
|
188
188
|
}
|
|
189
189
|
function LastRequest({ row, go }) {
|
|
190
|
-
return (_jsxs("div", { className: "ov-req" + (row.handled_at ? " is-handled" : ""), children: [_jsxs("div", { className: "ov-req-head", children: [_jsx("span", { className: "rq-avatar", children: initials(row.name) }), _jsxs("span", { className: "ov-req-who", children: [_jsx("strong", { children: row.name }), _jsx("a", { href: `mailto:${row.email}`, onClick: (e) => e.stopPropagation(), children: row.email })] }), _jsx("span", { className: "ov-req-when", children: when(row.created_at) })] }), _jsx("p", { className: "ov-req-msg", children: row.message }), _jsxs("div", { className: "ov-req-actions", children: [_jsx("button", { className: "ak-btn", onClick: () => go(`/requests/${row.id}`), children: "Apri" }), _jsx("a", { className: "ak-btn ak-btn-primary", href: `mailto:${row.email}`, children: "Rispondi" }), row.handled_at && _jsx("span", { className: "ak-hint", children: "Gi\u00E0
|
|
190
|
+
return (_jsxs("div", { className: "ov-req" + (row.handled_at ? " is-handled" : ""), children: [_jsxs("div", { className: "ov-req-head", children: [_jsx("span", { className: "rq-avatar", children: initials(row.name) }), _jsxs("span", { className: "ov-req-who", children: [_jsx("strong", { children: row.name }), _jsx("a", { href: `mailto:${row.email}`, onClick: (e) => e.stopPropagation(), children: row.email })] }), _jsx("span", { className: "ov-req-when", children: when(row.created_at) })] }), _jsx("p", { className: "ov-req-msg", children: row.message }), _jsxs("div", { className: "ov-req-actions", children: [_jsx("button", { className: "ak-btn", onClick: () => go(`/requests/${row.id}`), children: "Apri" }), _jsx("a", { className: "ak-btn ak-btn-primary", href: `mailto:${row.email}`, children: "Rispondi" }), row.handled_at && _jsx("span", { className: "ak-hint", children: "Gi\u00E0 letta" })] })] }));
|
|
191
191
|
}
|
|
192
192
|
function Top({ span: cols, title, note, rows, ready, label }) {
|
|
193
193
|
const top = Math.max(1, ...rows.map((r) => r.count));
|
|
@@ -13,8 +13,8 @@ import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter,
|
|
|
13
13
|
import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable";
|
|
14
14
|
import { CSS } from "@dnd-kit/utilities";
|
|
15
15
|
import { ProseField, SchemaForm } from "../../form/index.js";
|
|
16
|
-
import { api } from "../../index.js";
|
|
17
|
-
import { pageType } from "../site.js";
|
|
16
|
+
import { api, assetAltText } from "../../index.js";
|
|
17
|
+
import { pageType, site } from "../site.js";
|
|
18
18
|
import { normalizeGalleryLayout } from "../lib/gallery-layout";
|
|
19
19
|
import { uniqueSlug } from "../lib/slug";
|
|
20
20
|
import { proseStats } from "../lib/text";
|
|
@@ -165,9 +165,15 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
|
165
165
|
if (!picked?.length)
|
|
166
166
|
return;
|
|
167
167
|
const have = new Set(list.map((item) => item.src));
|
|
168
|
+
const locale = site().languages?.default ?? "default";
|
|
168
169
|
const additions = picked
|
|
169
170
|
.filter((file) => !have.has(file.url))
|
|
170
|
-
.map((file) => ({
|
|
171
|
+
.map((file) => ({
|
|
172
|
+
src: file.url,
|
|
173
|
+
w: file.w ?? null,
|
|
174
|
+
h: file.h ?? null,
|
|
175
|
+
alt: assetAltText(file, locale),
|
|
176
|
+
}));
|
|
171
177
|
if (additions.length)
|
|
172
178
|
onChange(position === "start" ? [...additions, ...list] : [...list, ...additions]);
|
|
173
179
|
};
|
|
@@ -180,8 +186,9 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
|
180
186
|
alert("Questa foto è già presente nella gallery.");
|
|
181
187
|
return;
|
|
182
188
|
}
|
|
189
|
+
const locale = site().languages?.default ?? "default";
|
|
183
190
|
onChange(list.map((item, itemIndex) => itemIndex === index
|
|
184
|
-
? { ...item, src: file.url, w: file.w ?? null, h: file.h ?? null }
|
|
191
|
+
? { ...item, src: file.url, w: file.w ?? null, h: file.h ?? null, alt: assetAltText(file, locale) }
|
|
185
192
|
: item));
|
|
186
193
|
};
|
|
187
194
|
const finishDrag = ({ active, over }) => {
|
|
@@ -208,7 +215,7 @@ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
|
|
|
208
215
|
const remove = (src) => onChange(list.filter((item) => item.src !== src));
|
|
209
216
|
return (_jsxs("section", { className: `pe-gallery${masonry ? ` is-${layout}` : ""}`, "aria-label": "Foto della gallery", children: [_jsxs("div", { className: "pe-gallery-bar", children: [_jsxs("div", { className: "pe-gallery-summary", children: [_jsxs("strong", { children: [list.length, " ", list.length === 1 ? "foto" : "foto"] }), _jsx("span", { children: list.length === 0
|
|
210
217
|
? "Scegli le immagini da mostrare nella pagina."
|
|
211
|
-
: `${visible} online${visible < list.length ? ` · ${list.length - visible} nascoste` : ""}` }), layout === "masonry-vertical" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry verticale \u00B7 3 colonne \u00B7 ordine dall\u2019alto verso il basso" })), layout === "masonry-horizontal" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry orizzontale \u00B7 6 colonne \u00B7 ordine da sinistra a destra" }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] })) : (_jsxs("div", { className: "pe-add-group", ref: addPositionRef, children: [_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi
|
|
218
|
+
: `${visible} online${visible < list.length ? ` · ${list.length - visible} nascoste` : ""}` }), layout === "masonry-vertical" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry verticale \u00B7 3 colonne \u00B7 ordine dall\u2019alto verso il basso" })), layout === "masonry-horizontal" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry orizzontale \u00B7 6 colonne \u00B7 ordine da sinistra a destra" }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] })) : (_jsxs("div", { className: "pe-add-group", ref: addPositionRef, children: [_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] }), _jsx("button", { type: "button", className: "pe-add-position", "aria-label": "Scegli dove aggiungere le foto", "aria-expanded": addPositionOpen, "aria-controls": "gallery-add-position", onClick: () => setAddPositionOpen((open) => !open), children: "\u2304" }), addPositionOpen && (_jsx("div", { className: "pe-add-menu", id: "gallery-add-position", children: _jsx("button", { type: "button", onClick: () => void add("end"), children: "Aggiungi in fondo" }) }))] }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-gallery-empty", onClick: () => void add("start"), children: [_jsx(Icon.gallery, {}), _jsx("strong", { children: "La gallery \u00E8 vuota" }), _jsx("span", { children: "Aggiungi le prime fotografie" })] })) : (_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: ({ active }) => setActiveId(String(active.id)), onDragOver: ({ over }) => setOverId(over ? String(over.id) : null), onDragCancel: () => { setActiveId(null); setOverId(null); }, onDragEnd: finishDrag, children: [_jsx(SortableContext, { items: shownItems.map((item) => item.src), strategy: rectSortingStrategy, children: _jsx("div", { className: "pe-photo-grid", children: shownItems.map((item) => (_jsx(GalleryCard, { item: item, verticalMasonry: verticalMasonry, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) }) }), _jsx(DragOverlay, { children: activeItem && (_jsx("div", { className: "pe-photo-overlay", children: _jsx("img", { src: activeItem.src, alt: "" }) })) }), masonry && hiddenItems.length > 0 && (_jsxs("section", { className: "pe-hidden-gallery", "aria-label": "Foto nascoste", children: [_jsxs("div", { className: "pe-hidden-gallery-head", children: [_jsx("strong", { children: "Nascoste" }), _jsx("span", { children: "Non occupano spazio nella gallery pubblica." })] }), _jsx("div", { className: "pe-hidden-grid", children: hiddenItems.map((item) => (_jsx(GalleryCard, { item: item, sortable: false, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) })] }))] }))] }));
|
|
212
219
|
}
|
|
213
220
|
export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, activeLanguage, defaultLanguage, }) {
|
|
214
221
|
const [bodyState, setBodyState] = useState("idle");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AssetFile } from "../../index.js";
|
|
2
2
|
import { normalizeWorks, type Work, type WorkCard, type WorkCardType, type WorkInfo } from "../../normalize.js";
|
|
3
3
|
export type { WorkCardType, WorkInfo, WorkCard, Work };
|
|
4
4
|
export { normalizeWorks };
|
|
@@ -31,6 +31,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
31
31
|
import { DndContext, closestCenter } from "@dnd-kit/core";
|
|
32
32
|
import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
|
|
33
33
|
import { CSS } from "@dnd-kit/utilities";
|
|
34
|
+
import { assetAltText } from "../../index.js";
|
|
34
35
|
import { ParagraphEditor } from "../ui/ParagraphEditor";
|
|
35
36
|
/* La casella immagine sta in `ui/`: la usano copertina e card qui, e il ritratto della pagina
|
|
36
37
|
"chi sono". Vedi il commento in cima a quel file. */
|
|
@@ -41,6 +42,7 @@ import { OpenCard } from "../ui/OpenCard";
|
|
|
41
42
|
import { useDragSensors } from "../ui/dnd";
|
|
42
43
|
import { movedOpenIndex, removedOpenIndex } from "../lib/reorder";
|
|
43
44
|
import { Icon } from "../ui/icons";
|
|
45
|
+
import { site } from "../site.js";
|
|
44
46
|
import { normalizeWorks, emptyWork, emptyCard, CARD_KINDS, } from "../../normalize.js";
|
|
45
47
|
export { normalizeWorks };
|
|
46
48
|
const str = (value) => (typeof value === "string" ? value : "");
|
|
@@ -69,7 +71,10 @@ function SortableCard({ id, card, index, onPatch, onRemove, pickMediaFiles }) {
|
|
|
69
71
|
const chooseImage = async () => {
|
|
70
72
|
const file = (await pickMediaFiles?.({ multiple: false }))?.[0];
|
|
71
73
|
if (file)
|
|
72
|
-
onPatch({
|
|
74
|
+
onPatch({
|
|
75
|
+
image: file.url,
|
|
76
|
+
alt: assetAltText(file, site().languages?.default ?? "default"),
|
|
77
|
+
});
|
|
73
78
|
};
|
|
74
79
|
return (_jsxs("article", { ref: setNodeRef, className: "pw-card" + (isDragging ? " is-dragging" : ""), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("header", { className: "pw-card-head", children: [_jsx("button", { type: "button", className: "pw-grip", "aria-label": `Riordina card ${index + 1}`, ...attributes, ...listeners, children: _jsx(Icon.grip, {}) }), _jsx("span", { className: "pw-card-kind", children: kind.label }), _jsx("div", { className: "seg", role: "group", "aria-label": "Tipo di card", children: CARD_KINDS.map((option) => (_jsx("button", { type: "button", className: "seg-btn" + (card.type === option.type ? " is-on" : ""), onClick: () => onPatch({ type: option.type }), title: option.hint, children: option.label }, option.type))) }), _jsx("button", { type: "button", className: "pw-icon-btn is-danger", "aria-label": "Elimina card", onClick: onRemove, children: _jsx(Icon.trash, {}) })] }), _jsxs("div", { className: "pw-card-body" + (wantsImage ? " is-split" : ""), children: [wantsImage && (_jsx(MediaSlot, { value: card.image, ratio: "wide", empty: "Scegli un\u2019immagine", onPick: () => void chooseImage(), onClear: () => onPatch({ image: "" }) })), _jsxs("div", { className: "pw-card-text", children: [wantsImage && (_jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Testo alternativo" }), _jsx("input", { className: "pw-field", value: card.alt, maxLength: 200, placeholder: "Descrive l\u2019immagine a chi non pu\u00F2 vederla", onChange: (event) => onPatch({ alt: event.target.value }) })] })), wantsText && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "pw-label", children: [_jsxs("span", { children: ["Occhiello ", _jsx("small", { children: "facoltativo" })] }), _jsx("input", { className: "pw-field", value: card.kicker, maxLength: 60, onChange: (event) => onPatch({ kicker: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Titolo" }), _jsx("input", { className: "pw-field", value: card.heading, maxLength: 120, onChange: (event) => onPatch({ heading: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Paragrafi" }), _jsx(ParagraphEditor, { className: "pw-field pw-textarea", rows: 5, value: card.body, placeholder: "Scrivi il testo della card.\nVai a capo per iniziare un paragrafo nuovo.", onChange: (body) => onPatch({ body }) })] })] }))] })] })] }));
|
|
75
80
|
}
|
|
@@ -100,7 +105,10 @@ function SortableWork({ id, work, index, total, open, onToggle, onPatch, onDupli
|
|
|
100
105
|
const chooseCover = async () => {
|
|
101
106
|
const file = (await pickMediaFiles?.({ multiple: false }))?.[0];
|
|
102
107
|
if (file)
|
|
103
|
-
onPatch({
|
|
108
|
+
onPatch({
|
|
109
|
+
cover: file.url,
|
|
110
|
+
coverAlt: assetAltText(file, site().languages?.default ?? "default"),
|
|
111
|
+
});
|
|
104
112
|
};
|
|
105
113
|
const pieces = [
|
|
106
114
|
work.cards.length ? `${work.cards.length} ${work.cards.length === 1 ? "card" : "card"}` : "",
|
|
@@ -109,7 +117,7 @@ function SortableWork({ id, work, index, total, open, onToggle, onPatch, onDupli
|
|
|
109
117
|
return (
|
|
110
118
|
/* Il guscio (maniglia, miniatura, titolo, apri/chiudi) sta in `ui/OpenCard`: è lo stesso
|
|
111
119
|
delle edizioni dei workshop, e tenerlo qui avrebbe voluto dire ricopiarlo là. */
|
|
112
|
-
_jsxs(OpenCard, { id: id, index: index, total: total, open: open, thumb: work.cover, title: work.title, empty: "Lavoro senza titolo", meta: pieces.join(" · "), kind: "questo lavoro", onToggle: onToggle, actions: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "btn-soft is-quiet", onClick: onDuplicate, children: "Duplica" }), _jsx("button", { type: "button", className: "btn-soft is-danger", onClick: onRemove, children: "Elimina" })] }), children: [_jsxs("div", { className: "pw-identity", children: [_jsx(MediaSlot, { value: work.cover, ratio: "cover", empty: "Scegli la copertina", onPick: () => void chooseCover(), onClear: () => onPatch({ cover: "" }) }), _jsxs("div", { className: "pw-identity-fields", children: [_jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Titolo" }), _jsx("input", { className: "pw-field pw-field-title", value: work.title, maxLength: 120, placeholder: "Il nome del lavoro", onChange: (event) => onPatch({ title: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Descrizione" }), _jsx(ParagraphEditor, { className: "pw-field pw-textarea", rows: 7, value: work.description, placeholder: "Racconta il lavoro.\nVai a capo per iniziare un paragrafo nuovo.", onChange: (description) => onPatch({ description }) }), _jsx("small", { children: "Seleziona una parola per metterla in grassetto, in corsivo o per collegarla." })] })] })] }), _jsx(InfoList, { items: work.info, onChange: (info) => onPatch({ info }) }), _jsx(CardList, { items: work.cards, onChange: (cards) => onPatch({ cards }), pickMediaFiles: pickMediaFiles })] }));
|
|
120
|
+
_jsxs(OpenCard, { id: id, index: index, total: total, open: open, thumb: work.cover, title: work.title, empty: "Lavoro senza titolo", meta: pieces.join(" · "), kind: "questo lavoro", onToggle: onToggle, actions: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "btn-soft is-quiet", onClick: onDuplicate, children: "Duplica" }), _jsx("button", { type: "button", className: "btn-soft is-danger", onClick: onRemove, children: "Elimina" })] }), children: [_jsxs("div", { className: "pw-identity", children: [_jsx(MediaSlot, { value: work.cover, ratio: "cover", empty: "Scegli la copertina", onPick: () => void chooseCover(), onClear: () => onPatch({ cover: "" }) }), _jsxs("div", { className: "pw-identity-fields", children: [_jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Titolo" }), _jsx("input", { className: "pw-field pw-field-title", value: work.title, maxLength: 120, placeholder: "Il nome del lavoro", onChange: (event) => onPatch({ title: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Testo alternativo della copertina" }), _jsx("input", { className: "pw-field", value: work.coverAlt, maxLength: 200, placeholder: "Descrive la copertina a chi non pu\u00F2 vederla", onChange: (event) => onPatch({ coverAlt: event.target.value }) })] }), _jsxs("label", { className: "pw-label", children: [_jsx("span", { children: "Descrizione" }), _jsx(ParagraphEditor, { className: "pw-field pw-textarea", rows: 7, value: work.description, placeholder: "Racconta il lavoro.\nVai a capo per iniziare un paragrafo nuovo.", onChange: (description) => onPatch({ description }) }), _jsx("small", { children: "Seleziona una parola per metterla in grassetto, in corsivo o per collegarla." })] })] })] }), _jsx(InfoList, { items: work.info, onChange: (info) => onPatch({ info }) }), _jsx(CardList, { items: work.cards, onChange: (cards) => onPatch({ cards }), pickMediaFiles: pickMediaFiles })] }));
|
|
113
121
|
}
|
|
114
122
|
/* ---- L'elenco ------------------------------------------------------------------------------ */
|
|
115
123
|
export function PortfolioWorksEditor({ value, onChange, pickMediaFiles }) {
|
|
@@ -18,7 +18,7 @@ const initials = (name) => name.trim().split(/\s+/).slice(0, 2).map((w) => w[0]
|
|
|
18
18
|
export function RequestsList({ rows, current, picked, onOpen, onPick, }) {
|
|
19
19
|
return (_jsx("ul", { className: "rq-list", children: rows.map((r) => (_jsxs("li", { className: "rq-row"
|
|
20
20
|
+ (r.id === current ? " is-on" : "")
|
|
21
|
-
+ (r.handled_at ? " is-handled" : ""), children: [_jsx("input", { className: "rq-pick", type: "checkbox", "aria-label": `Scegli la richiesta di ${r.name}`, checked: picked.has(r.id), onChange: () => onPick(r.id) }), _jsxs("button", { className: "rq-open", onClick: () => onOpen(r.id), children: [_jsxs("span", { className: "rq-line", children: [_jsx("span", { className: "rq-who", children: r.name }), !r.handled_at && _jsx("span", { className: "rq-new", title: "
|
|
21
|
+
+ (r.handled_at ? " is-handled" : ""), children: [_jsx("input", { className: "rq-pick", type: "checkbox", "aria-label": `Scegli la richiesta di ${r.name}`, checked: picked.has(r.id), onChange: () => onPick(r.id) }), _jsxs("button", { className: "rq-open", onClick: () => onOpen(r.id), children: [_jsxs("span", { className: "rq-line", children: [_jsx("span", { className: "rq-who", children: r.name }), !r.handled_at && _jsx("span", { className: "rq-new", title: "non letta" })] }), _jsx("span", { className: "rq-when", children: fmtShort(r.created_at) }), _jsx("span", { className: "rq-snip", children: r.message })] })] }, r.id))) }));
|
|
22
22
|
}
|
|
23
23
|
export function RequestDetail({ row, onMark, onRemove, }) {
|
|
24
24
|
const [menu, setMenu] = useState(false);
|
|
@@ -26,5 +26,5 @@ export function RequestDetail({ row, onMark, onRemove, }) {
|
|
|
26
26
|
// `mailto` con l'oggetto già pronto: rispondere è il gesto normale dopo aver letto, e non
|
|
27
27
|
// deve costare un copia-incolla.
|
|
28
28
|
const reply = `mailto:${row.email}?subject=${encodeURIComponent("Re: la tua richiesta")}`;
|
|
29
|
-
return (_jsxs("article", { className: "rq-detail" + (handled ? " is-handled" : ""), children: [_jsxs("header", { className: "rq-head", children: [_jsx("span", { className: "rq-avatar", "aria-hidden": "true", children: initials(row.name) }), _jsx("span", { className: "rq-name", children: row.name }), handled && _jsx("span", { className: "rq-tag", children: "
|
|
29
|
+
return (_jsxs("article", { className: "rq-detail" + (handled ? " is-handled" : ""), children: [_jsxs("header", { className: "rq-head", children: [_jsx("span", { className: "rq-avatar", "aria-hidden": "true", children: initials(row.name) }), _jsx("span", { className: "rq-name", children: row.name }), handled && _jsx("span", { className: "rq-tag", children: "letta" }), _jsx("span", { className: "ak-spacer" }), _jsx("time", { className: "rq-date", dateTime: row.created_at, children: fmtLong(row.created_at) }), _jsxs("span", { className: "rq-menu-wrap", children: [_jsx("button", { className: "ak-icon", onClick: () => setMenu((v) => !v), title: "Altro", children: "\u2022\u2022\u2022" }), menu && (_jsxs("div", { className: "pa-menu", onMouseLeave: () => setMenu(false), children: [_jsx("button", { onClick: () => { onMark(!handled); setMenu(false); }, children: handled ? "Segna come non letto" : "Segna come letto" }), _jsx("button", { className: "pa-danger", onClick: () => { onRemove(); setMenu(false); }, children: "Elimina\u2026" })] }))] })] }), _jsxs("dl", { className: "rq-fields", children: [_jsx("dt", { children: "Nome" }), _jsx("dd", { children: row.name }), _jsx("dt", { children: "Email" }), _jsx("dd", { children: _jsx("a", { className: "rq-mail", href: reply, children: row.email }) }), _jsx("dt", { children: "Arrivata" }), _jsx("dd", { children: fmtLong(row.created_at) }), _jsx("dt", { children: "Messaggio" }), _jsx("dd", { className: "rq-msg", children: row.message })] }), _jsxs("footer", { className: "rq-actions", children: [_jsx("a", { className: "ak-btn ak-btn-primary", href: reply, children: "Rispondi" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => onMark(!handled), children: handled ? "Segna come non letto" : "Segna come letto" })] })] }));
|
|
30
30
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssetFile } from "../../index.js";
|
|
2
2
|
import type { SiteFeatures, SiteSettings } from "../lib/settings";
|
|
3
|
+
import type { AdminInterface } from "../lib/admin-interface";
|
|
3
4
|
export declare const EXTRAS: {
|
|
4
5
|
key: keyof SiteFeatures;
|
|
5
6
|
title: string;
|
|
@@ -7,12 +8,14 @@ export declare const EXTRAS: {
|
|
|
7
8
|
card: string;
|
|
8
9
|
short: string;
|
|
9
10
|
}[];
|
|
10
|
-
export declare function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, passwordDraftPending, onDiscardPasswordDraft, }: {
|
|
11
|
+
export declare function SiteSettingsEditor({ value, onChange, pickMediaFiles, adminInterface, onAdminInterfaceChange, focus, passwordDraftPending, onDiscardPasswordDraft, }: {
|
|
11
12
|
value: SiteSettings;
|
|
12
13
|
onChange: (next: SiteSettings) => void;
|
|
13
14
|
pickMediaFiles: (o: {
|
|
14
15
|
multiple: boolean;
|
|
15
16
|
}) => Promise<AssetFile[] | null>;
|
|
17
|
+
adminInterface: AdminInterface;
|
|
18
|
+
onAdminInterfaceChange: (next: AdminInterface) => void;
|
|
16
19
|
focus?: string;
|
|
17
20
|
passwordDraftPending?: boolean;
|
|
18
21
|
onDiscardPasswordDraft?: () => void;
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { contentPersistenceMode, site } from "../site.js";
|
|
4
4
|
import { Icon } from "../ui/icons";
|
|
5
|
+
import { AdminInterfaceSetting, SiteSettingsIntro } from "./AdminInterfaceSettings.js";
|
|
5
6
|
function Toggle({ checked, onChange, label, disabled = false, }) {
|
|
6
7
|
return (
|
|
7
8
|
/* `disabled` sull'`input`, non solo una classe: così non si preme, non prende il focus da
|
|
@@ -35,7 +36,7 @@ function SettingRow({ title, help, children, }) {
|
|
|
35
36
|
function Field({ value, onChange, maxLength, placeholder, type = "text", disabled = false, }) {
|
|
36
37
|
return (_jsx("input", { className: "st-input", type: type, value: value, maxLength: maxLength, placeholder: placeholder, disabled: disabled, onChange: (event) => onChange(event.target.value) }));
|
|
37
38
|
}
|
|
38
|
-
export function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, passwordDraftPending = false, onDiscardPasswordDraft, }) {
|
|
39
|
+
export function SiteSettingsEditor({ value, onChange, pickMediaFiles, adminInterface, onAdminInterfaceChange, focus, passwordDraftPending = false, onDiscardPasswordDraft, }) {
|
|
39
40
|
const d1Content = contentPersistenceMode() === "d1";
|
|
40
41
|
const managedSiteAccess = d1Content && Boolean(site().siteAccess);
|
|
41
42
|
const update = (key, next) => onChange({ ...value, [key]: next });
|
|
@@ -59,7 +60,7 @@ export function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, pas
|
|
|
59
60
|
if (selected?.[0])
|
|
60
61
|
update("favicon", selected[0].url);
|
|
61
62
|
};
|
|
62
|
-
return (_jsxs("section", { className: "settings-editor", children: [
|
|
63
|
+
return (_jsxs("section", { className: "settings-editor", children: [_jsx(SiteSettingsIntro, {}), _jsx(AdminInterfaceSetting, { adminInterface: adminInterface, onAdminInterfaceChange: onAdminInterfaceChange }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Identit\u00E0 del sito" }), _jsx(SettingRow, { title: "Titolo del sito", help: "Il nome generale del sito, usato anche come nome del publisher nei metadati.", children: _jsx(Field, { value: value.siteTitle, maxLength: 140, placeholder: site().name, onChange: (next) => update("siteTitle", next) }) }), _jsxs(SettingRow, { title: "Descrizione breve", help: "Una frase sintetica usata come descrizione predefinita quando una pagina non ne possiede una specifica.", children: [_jsx("textarea", { className: "st-input st-textarea", rows: 3, value: value.shortDescription, maxLength: 255, onChange: (event) => update("shortDescription", event.target.value) }), _jsxs("span", { className: "st-count", children: [value.shortDescription.length, "/255"] })] }), _jsx(SettingRow, { title: "Favicon", help: "L\u2019icona mostrata nella scheda del browser. \u00C8 preferibile un\u2019immagine quadrata.", children: _jsxs("div", { className: "st-favicon", children: [_jsx("span", { className: "st-favicon-preview", children: value.favicon ? _jsx("img", { src: value.favicon, alt: "Anteprima favicon" }) : _jsx(Icon.pages, {}) }), _jsx("button", { className: "ak-btn", type: "button", onClick: () => void chooseFavicon(), children: value.favicon ? "Cambia favicon" : "Carica favicon" }), value.favicon && (_jsx("button", { className: "ak-btn ak-btn-quiet", type: "button", onClick: () => update("favicon", ""), children: "Rimuovi" }))] }) })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Accesso al sito" }), _jsx(SettingRow, { title: "Password del sito", help: "Quando \u00E8 attiva, ogni pagina e ogni immagine pubblica richiedono la stessa password. L\u2019admin resta raggiungibile tramite il suo accesso protetto.", children: _jsxs("div", { className: "st-stack", children: [_jsx(Field, { type: "password", value: value.password, maxLength: 200, placeholder: value.passwordEnabled ? "Password già impostata · inseriscine una per cambiarla" : "Almeno 8 caratteri", disabled: d1Content && !managedSiteAccess, onChange: (next) => update("password", next) }), _jsx(Toggle, { checked: value.passwordEnabled, label: "Protezione con password", disabled: d1Content && !managedSiteAccess, onChange: (next) => update("passwordEnabled", next) }), d1Content && !managedSiteAccess && (_jsx("p", { className: "st-inline-help", children: "La protezione esistente resta attiva e viene gestita dall\u2019operatore durante la migrazione D1." })), managedSiteAccess && (_jsx("p", { className: "st-inline-help", children: "La modifica resta in bozza e diventa effettiva solo dopo una pubblicazione completata." })), managedSiteAccess && passwordDraftPending && onDiscardPasswordDraft && (_jsx("button", { className: "ak-btn ak-btn-quiet", type: "button", onClick: onDiscardPasswordDraft, children: "Annulla la modifica salvata" })), value.passwordEnabled && (_jsx("p", { className: "st-inline-help", children: "Lascia il campo vuoto per mantenere la password attuale." }))] }) })] }), _jsxs("section", { className: "st-section" + (focus === "funzioni" ? " is-focused" : ""), ref: extras, children: [_jsx("h2", { children: "Funzioni aggiuntive" }), EXTRAS.map((extra) => (_jsx(SettingRow, { title: extra.title, help: extra.help, children: _jsx(Toggle, { checked: value.features[extra.key], disabled: true, label: value.features[extra.key] ? "Attiva su questo sito" : "Non attiva", onChange: () => { } }) }, extra.key))), _jsxs("p", { className: "st-inline-help st-extras-note", children: ["Queste funzioni si attivano separatamente. Per accenderle su questo sito scrivi a", " ", _jsx("strong", { children: site().support.name }), ":", " ", _jsx("a", { href: `mailto:${site().support.email}`, children: site().support.email }), "."] })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Statistiche" }), _jsx(SettingRow, { title: "Tag Analytics", help: _jsxs(_Fragment, { children: ["Il codice fornito dal servizio di statistiche (Umami, Google Analytics, altro). Si incolla qui cos\u00EC com\u2019\u00E8: finisce dentro ", _jsx("code", { children: "<head>" }), " di ogni pagina del sito alla prossima pubblicazione. Le pagine Transfer dei clienti restano escluse. Per svuotarlo, cancella tutto e salva."] }), children: _jsxs("div", { className: "st-stack", children: [_jsx("textarea", { className: "st-input st-textarea st-code", rows: 6, value: value.analyticsTag, maxLength: 4000, spellCheck: false, autoCapitalize: "off", autoCorrect: "off", placeholder: '<script defer src="https://cloud.umami.is/script.js" data-website-id="…"></script>', onChange: (event) => update("analyticsTag", event.target.value) }), _jsxs("span", { className: "st-count", children: [value.analyticsTag.length, "/4000"] }), _jsxs("p", { className: "st-inline-help", children: ["\u00C8 codice eseguito su ogni pagina: incolla solo quello che ti ha dato il servizio. Un tag incompleto (per esempio senza ", _jsx("code", { children: "</script>" }), ") pu\u00F2 rompere il sito."] })] }) })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Profilo" }), _jsx(SettingRow, { title: "Nome", help: "Il nome della persona proprietaria del sito. Sar\u00E0 disponibile per future funzioni del pannello.", children: _jsx(Field, { value: value.ownerName, maxLength: 120, placeholder: site().name, onChange: (next) => update("ownerName", next) }) }), _jsxs(SettingRow, { title: "Descrizione del sito", help: "Una descrizione generale pi\u00F9 estesa, conservata come informazione del profilo del sito.", children: [_jsx("textarea", { className: "st-input st-textarea", rows: 5, value: value.siteDescription, maxLength: 1200, onChange: (event) => update("siteDescription", event.target.value) }), _jsxs("span", { className: "st-count", children: [value.siteDescription.length, "/1200"] })] })] })] }));
|
|
63
64
|
}
|
|
64
65
|
export function WebsiteSeoSettingsEditor({ value, onChange, }) {
|
|
65
66
|
const update = (key, next) => onChange({ ...value, [key]: next });
|
package/dist/app/ui/MediaSlot.js
CHANGED
|
@@ -16,5 +16,5 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
16
16
|
*/
|
|
17
17
|
import { Icon } from "./icons";
|
|
18
18
|
export function MediaSlot({ value, ratio, empty, onPick, onClear }) {
|
|
19
|
-
return (_jsxs("div", { className: "pw-media" + (value ? " has-image" : ""), children: [_jsx("button", { type: "button", className: "pw-thumb " + (ratio === "cover" ? "is-cover" : ratio === "portrait" ? "is-portrait" : "is-large"), onClick: onPick, "aria-label": value ? "Cambia immagine" : empty, children: value ? _jsx("img", { src: value, alt: "" }) : _jsxs("span", { className: "pw-thumb-empty", children: [_jsx(Icon.media, {}), " ", empty] }) }), value && (_jsxs(_Fragment, { children: [_jsx("span", { className: "pw-media-hint", "aria-hidden": "true", children: "
|
|
19
|
+
return (_jsxs("div", { className: "pw-media" + (value ? " has-image" : ""), children: [_jsx("button", { type: "button", className: "pw-thumb " + (ratio === "cover" ? "is-cover" : ratio === "portrait" ? "is-portrait" : "is-large"), onClick: onPick, "aria-label": value ? "Cambia immagine" : empty, children: value ? _jsx("img", { src: value, alt: "" }) : _jsxs("span", { className: "pw-thumb-empty", children: [_jsx(Icon.media, {}), " ", empty] }) }), value && (_jsxs(_Fragment, { children: [_jsx("span", { className: "pw-media-hint", "aria-hidden": "true", children: "Clicca per cambiare" }), _jsx("button", { type: "button", className: "pw-media-clear", "aria-label": "Rimuovi immagine", onClick: onClear, children: _jsx(Icon.trash, {}) })] }))] }));
|
|
20
20
|
}
|
package/dist/app/ui/OpenCard.js
CHANGED
|
@@ -11,10 +11,31 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
* l'elenco perde la sua funzione, che è far vedere l'ordine — e l'ordine è il contenuto della
|
|
12
12
|
* pagina, non un dettaglio.
|
|
13
13
|
*/
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
14
15
|
import { useSortable } from "@dnd-kit/sortable";
|
|
15
16
|
import { CSS } from "@dnd-kit/utilities";
|
|
16
17
|
import { Icon } from "./icons";
|
|
17
18
|
export function OpenCard({ id, index, total, open, thumb, title, empty, meta, kind, onToggle, actions, children, }) {
|
|
18
19
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
|
19
|
-
|
|
20
|
+
/* Il contenuto resta montato durante la chiusura, altrimenti il browser non avrebbe
|
|
21
|
+
un'altezza da animare. Alla prima apertura viene montato ancora chiuso e si espande nel
|
|
22
|
+
frame seguente: così anche l'apertura iniziale parte davvero da zero, senza un salto. */
|
|
23
|
+
const [bodyMounted, setBodyMounted] = useState(open);
|
|
24
|
+
const [bodyExpanded, setBodyExpanded] = useState(open);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let frame = 0;
|
|
27
|
+
if (open) {
|
|
28
|
+
setBodyMounted(true);
|
|
29
|
+
frame = requestAnimationFrame(() => setBodyExpanded(true));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
setBodyExpanded(false);
|
|
33
|
+
}
|
|
34
|
+
return () => cancelAnimationFrame(frame);
|
|
35
|
+
}, [open]);
|
|
36
|
+
return (_jsxs("article", { ref: setNodeRef, className: ["pw-work", open ? "is-open" : "", isDragging ? "is-dragging" : ""].filter(Boolean).join(" "), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("div", { className: "pw-work-head", children: [_jsx("button", { type: "button", className: "pw-grip", "aria-label": `Riordina ${title || empty.toLowerCase()}, posizione ${index + 1} di ${total}`, ...attributes, ...listeners, children: _jsx(Icon.grip, {}) }), _jsxs("button", { type: "button", className: "pw-work-open", "aria-expanded": open, onClick: onToggle, children: [_jsx("span", { className: "pw-thumb", children: thumb ? _jsx("img", { src: thumb, alt: "" }) : _jsx("span", { className: "pw-thumb-empty", children: _jsx(Icon.media, {}) }) }), _jsxs("span", { className: "pw-work-title", children: [_jsx("strong", { className: title ? "" : "is-empty", children: title || empty }), !!meta && _jsx("small", { children: meta })] }), _jsx("span", { className: "pw-chevron", "aria-hidden": "true", children: _jsx(Icon.chevron, {}) })] }), _jsx("div", { className: "pw-work-actions", "aria-label": `Azioni su ${kind}`, children: actions })] }), _jsx("div", { className: "pw-work-reveal" + (bodyExpanded ? " is-expanded" : ""), "aria-hidden": !open, inert: open ? undefined : true, onTransitionEnd: (event) => {
|
|
37
|
+
if (event.target === event.currentTarget && event.propertyName === "grid-template-rows" && !open) {
|
|
38
|
+
setBodyMounted(false);
|
|
39
|
+
}
|
|
40
|
+
}, children: _jsx("div", { className: "pw-work-reveal-inner", children: bodyMounted && _jsx("div", { className: "pw-work-body", children: children }) }) })] }));
|
|
20
41
|
}
|
package/dist/app/ui/icons.js
CHANGED
|
@@ -16,9 +16,12 @@ export const Icon = {
|
|
|
16
16
|
home: () => _jsx(Svg, { children: _jsx("path", { ...P, d: "M2.5 6.8 8 2.5l5.5 4.3V13a.5.5 0 0 1-.5.5h-3v-4h-4v4H3a.5.5 0 0 1-.5-.5V6.8Z" }) }),
|
|
17
17
|
pages: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M3.5 2.5h6l3 3v8h-9v-11Z" }), _jsx("path", { ...P, d: "M9.5 2.5v3h3" })] }),
|
|
18
18
|
media: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2.5", y: "3.5", width: "11", height: "9", rx: "1" }), _jsx("path", { ...P, d: "m2.5 10 3-2.5 3 2.5 2-1.5 3 2.5" }), _jsx("circle", { ...P, cx: "6", cy: "6", r: "1" })] }),
|
|
19
|
-
|
|
19
|
+
/* Un invio verso l'esterno, non una freccia che entra in una inbox. Il secondo tratto
|
|
20
|
+
distingue chiaramente la piega dell'aeroplanino anche quando l'icona è a 16 px. */
|
|
21
|
+
delivery: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "m2.5 7 11-4.5-3.5 11-2.5-4L2.5 7Z" }), _jsx("path", { ...P, d: "m7.5 9.5 6-7" })] }),
|
|
20
22
|
calendar: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2.5", y: "3.5", width: "11", height: "10", rx: "1" }), _jsx("path", { ...P, d: "M5 2.5v2M11 2.5v2M2.5 6.5h11" }), _jsx("path", { ...P, d: "M5.2 9h.1M8 9h.1M10.8 9h.1M5.2 11.3h.1M8 11.3h.1" })] }),
|
|
21
|
-
|
|
23
|
+
/* Tre regolazioni, riconoscibili come configurazione anche accanto al sole/luna del tema. */
|
|
24
|
+
settings: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M2.5 4h3M8.5 4h5M2.5 8h6M11.5 8h2M2.5 12h2M7.5 12h6" }), _jsx("circle", { ...P, cx: "7", cy: "4", r: "1.5" }), _jsx("circle", { ...P, cx: "10", cy: "8", r: "1.5" }), _jsx("circle", { ...P, cx: "6", cy: "12", r: "1.5" })] }),
|
|
22
25
|
mail: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2.5", y: "3.5", width: "11", height: "9", rx: "1" }), _jsx("path", { ...P, d: "m2.5 5 5.5 3.5L13.5 5" })] }),
|
|
23
26
|
play: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2", y: "3", width: "12", height: "8.5", rx: "1" }), _jsx("path", { ...P, d: "M5.5 14h5" }), _jsx("path", { ...P, d: "M7 6.2 9.6 7.7 7 9.2v-3Z" })] }),
|
|
24
27
|
out: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M7 3.5H3.5v9h9V9" }), _jsx("path", { ...P, d: "M9.5 2.5h4v4" }), _jsx("path", { ...P, d: "m13 3-5 5" })] }),
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
/* I widget. Piccoli di proposito: il valore del sistema sta nello schema, non qui.
|
|
3
3
|
* Nessuno di questi conosce il sito — etichette, opzioni e aiuti arrivano da `ui`. */
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
|
+
import { assetAltText } from "../../api.js";
|
|
5
6
|
import { registerField } from "../registry.js";
|
|
6
7
|
import { ProseField } from "../prose/ProseField.js";
|
|
7
8
|
function Wrap({ meta, error, children }) {
|
|
@@ -100,7 +101,7 @@ registerField("r2Curated", ({ meta, value, onChange, error, pickMediaFiles }) =>
|
|
|
100
101
|
const have = new Set(list.map((it) => it.src));
|
|
101
102
|
const add = picked
|
|
102
103
|
.filter((f) => !have.has(f.url))
|
|
103
|
-
.map((f) => ({ src: f.url, w: f.w ?? null, h: f.h ?? null }));
|
|
104
|
+
.map((f) => ({ src: f.url, w: f.w ?? null, h: f.h ?? null, alt: assetAltText(f) }));
|
|
104
105
|
if (add.length)
|
|
105
106
|
onChange([...list, ...add]);
|
|
106
107
|
}, children: "+ Aggiungi foto" })] }));
|