@cardinali-studio/adminkit 2.2.11 → 2.2.12
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 +43 -2
- package/dist/api.js +13 -3
- package/dist/app/App.js +10 -4
- package/dist/app/app.css +5 -0
- package/dist/app/lib/publish.js +4 -1
- package/dist/app/lib/settings.d.ts +2 -0
- package/dist/app/lib/settings.js +54 -8
- package/dist/app/routes/MediaSettingsEditor.d.ts +4 -0
- package/dist/app/routes/MediaSettingsEditor.js +4 -0
- package/dist/app/routes/SiteSettingsEditor.d.ts +3 -1
- package/dist/app/routes/SiteSettingsEditor.js +3 -2
- package/dist/app/routes/UploadDock.js +31 -9
- package/dist/app/site.d.ts +4 -0
- package/dist/deployment.js +14 -1
- package/dist/form/media/MediaFolders.d.ts +5 -1
- package/dist/form/media/MediaFolders.js +3 -2
- package/dist/form/media/image-conversion.d.ts +14 -0
- package/dist/form/media/image-conversion.js +163 -0
- package/dist/form/media/media-upload-queue.d.ts +31 -0
- package/dist/form/media/media-upload-queue.js +230 -0
- package/dist/form/media/useMediaLibrary.d.ts +3 -0
- package/dist/form/media/useMediaLibrary.js +72 -18
- package/dist/functions/_shared/assets.js +3 -2
- package/dist/functions/api/assets/folders.js +2 -1
- package/dist/functions/api/assets/settings.js +40 -0
- package/dist/functions/api/assets/upload.js +4 -1
- package/dist/functions/api/assets.js +4 -3
- package/dist/functions/api/site-access.js +78 -0
- package/dist/functions/index.js +4 -0
- package/dist/functions/site.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/migrations/0004_site_access.sql +41 -0
- package/dist/persistence.d.ts +1 -0
- package/dist/persistence.js +22 -4
- package/dist/release.d.ts +3 -0
- package/dist/release.js +7 -0
- package/dist/site-access.d.ts +48 -0
- package/dist/site-access.js +180 -0
- package/dist/theme.css +1 -0
- package/package.json +4 -1
package/dist/api.d.ts
CHANGED
|
@@ -76,6 +76,16 @@ export type ContentDocument = {
|
|
|
76
76
|
publishedPayload: string | null;
|
|
77
77
|
publishedTombstone: boolean | null;
|
|
78
78
|
};
|
|
79
|
+
export type SiteAccessPublicVersion = {
|
|
80
|
+
versionId: string;
|
|
81
|
+
enabled: boolean;
|
|
82
|
+
formatVersion: 1;
|
|
83
|
+
};
|
|
84
|
+
export type SiteAccessPublicState = {
|
|
85
|
+
active: SiteAccessPublicVersion | null;
|
|
86
|
+
draft: SiteAccessPublicVersion | null;
|
|
87
|
+
effective: SiteAccessPublicVersion | null;
|
|
88
|
+
};
|
|
79
89
|
/** Builds the only D1 Save wire shape. The Worker recomputes these hashes too,
|
|
80
90
|
* so retries are deterministic rather than client-trusted. */
|
|
81
91
|
export declare function contentSaveRequest(command: ContentSaveCommand): Promise<{
|
|
@@ -133,6 +143,20 @@ export declare const api: {
|
|
|
133
143
|
ok: true;
|
|
134
144
|
discarded: true;
|
|
135
145
|
}>;
|
|
146
|
+
siteAccessState: () => Promise<{
|
|
147
|
+
ok: true;
|
|
148
|
+
state: SiteAccessPublicState;
|
|
149
|
+
}>;
|
|
150
|
+
saveSiteAccess: (enabled: boolean, password: string, expectedDraftVersionId: string | null) => Promise<{
|
|
151
|
+
ok: true;
|
|
152
|
+
saved: true;
|
|
153
|
+
state: SiteAccessPublicState;
|
|
154
|
+
}>;
|
|
155
|
+
discardSiteAccess: (expectedDraftVersionId: string) => Promise<{
|
|
156
|
+
ok: true;
|
|
157
|
+
discarded: true;
|
|
158
|
+
state: SiteAccessPublicState;
|
|
159
|
+
}>;
|
|
136
160
|
/** D1 publication is site-owned: the site chooses which documents it can
|
|
137
161
|
* materialize, while the panel only displays the pending immutable inputs. */
|
|
138
162
|
contentPublishState: () => Promise<{
|
|
@@ -145,6 +169,9 @@ export declare const api: {
|
|
|
145
169
|
updatedAt: number;
|
|
146
170
|
}[];
|
|
147
171
|
mediaOperations?: MediaOperationStatus[];
|
|
172
|
+
siteAccess?: {
|
|
173
|
+
enabled: boolean;
|
|
174
|
+
} | null;
|
|
148
175
|
activeReleaseId?: string | null;
|
|
149
176
|
}>;
|
|
150
177
|
contentPublish: () => Promise<{
|
|
@@ -273,7 +300,21 @@ export declare const api: {
|
|
|
273
300
|
ok: true;
|
|
274
301
|
}>;
|
|
275
302
|
/** L'upload NON è JSON: corpo grezzo e metadati negli header (vedi assets/upload.js). */
|
|
276
|
-
upload: (prefix: string, file: File
|
|
303
|
+
upload: (prefix: string, file: File, metadata?: {
|
|
304
|
+
capturedAt?: string | null;
|
|
305
|
+
}) => Promise<AssetFile>;
|
|
306
|
+
mediaSettings: () => Promise<{
|
|
307
|
+
ok: true;
|
|
308
|
+
settings: {
|
|
309
|
+
autoWebp: boolean;
|
|
310
|
+
};
|
|
311
|
+
}>;
|
|
312
|
+
saveMediaSettings: (autoWebp: boolean) => Promise<{
|
|
313
|
+
ok: true;
|
|
314
|
+
settings: {
|
|
315
|
+
autoWebp: boolean;
|
|
316
|
+
};
|
|
317
|
+
}>;
|
|
277
318
|
trash: () => Promise<{
|
|
278
319
|
ok: true;
|
|
279
320
|
items: TrashItem[];
|
|
@@ -295,4 +336,4 @@ export declare const api: {
|
|
|
295
336
|
removed: number;
|
|
296
337
|
}>;
|
|
297
338
|
};
|
|
298
|
-
export declare function uploadQueue(prefix: string, files: File[], onProgress: (done: number, total: number, last?: AssetFile, error?: string) => void, concurrency?: number): Promise<AssetFile[]>;
|
|
339
|
+
export declare function uploadQueue(prefix: string, files: File[], onProgress: (done: number, total: number, last?: AssetFile, error?: string) => void, concurrency?: number, prepare?: (file: File) => Promise<File>): Promise<AssetFile[]>;
|
package/dist/api.js
CHANGED
|
@@ -60,6 +60,9 @@ export const api = {
|
|
|
60
60
|
contentDocument: (documentId) => call(`/admin/api/content/document?id=${encodeURIComponent(documentId)}`),
|
|
61
61
|
saveContent: async (command) => post("/admin/api/content/save", await contentSaveRequest(command)),
|
|
62
62
|
discardContent: (documentId, expectedDraftRevisionId, baselineRevisionId) => post("/admin/api/content/discard", { documentId, expectedDraftRevisionId, baselineRevisionId }),
|
|
63
|
+
siteAccessState: () => call("/admin/api/content/site-access"),
|
|
64
|
+
saveSiteAccess: (enabled, password, expectedDraftVersionId) => post("/admin/api/content/site-access", { action: "save", enabled, password, expectedDraftVersionId }),
|
|
65
|
+
discardSiteAccess: (expectedDraftVersionId) => post("/admin/api/content/site-access", { action: "discard", expectedDraftVersionId }),
|
|
63
66
|
/** D1 publication is site-owned: the site chooses which documents it can
|
|
64
67
|
* materialize, while the panel only displays the pending immutable inputs. */
|
|
65
68
|
contentPublishState: () => call("/admin/api/content/publish"),
|
|
@@ -123,15 +126,18 @@ export const api = {
|
|
|
123
126
|
body: JSON.stringify({ id }),
|
|
124
127
|
}),
|
|
125
128
|
/** L'upload NON è JSON: corpo grezzo e metadati negli header (vedi assets/upload.js). */
|
|
126
|
-
upload: (prefix, file) => call("/admin/api/assets/upload", {
|
|
129
|
+
upload: (prefix, file, metadata = {}) => call("/admin/api/assets/upload", {
|
|
127
130
|
method: "POST",
|
|
128
131
|
headers: {
|
|
129
132
|
"Content-Type": file.type || "application/octet-stream",
|
|
130
133
|
"X-Prefix": prefix,
|
|
131
134
|
"X-Filename": encodeURIComponent(file.name),
|
|
135
|
+
...(metadata.capturedAt ? { "X-Captured-At": metadata.capturedAt } : {}),
|
|
132
136
|
},
|
|
133
137
|
body: file,
|
|
134
138
|
}),
|
|
139
|
+
mediaSettings: () => call("/admin/api/assets/settings"),
|
|
140
|
+
saveMediaSettings: (autoWebp) => post("/admin/api/assets/settings", { autoWebp }),
|
|
135
141
|
// ---- cestino
|
|
136
142
|
trash: () => call("/admin/api/assets/trash"),
|
|
137
143
|
trashRestore: (trashKeys) => post("/admin/api/assets/trash/restore", { trashKeys }),
|
|
@@ -142,14 +148,18 @@ export const api = {
|
|
|
142
148
|
* (app.js:922), il che su quaranta foto è tedioso senza motivo; tutte insieme invece saturano
|
|
143
149
|
* la connessione e rendono il progresso illeggibile. Tre è il compromesso.
|
|
144
150
|
* Un file che fallisce non ferma gli altri: si segnala e si prosegue. */
|
|
145
|
-
export async function uploadQueue(prefix, files, onProgress, concurrency = 3) {
|
|
151
|
+
export async function uploadQueue(prefix, files, onProgress, concurrency = 3, prepare = async (file) => file) {
|
|
146
152
|
const out = [];
|
|
147
153
|
let done = 0, next = 0;
|
|
148
154
|
const worker = async () => {
|
|
149
155
|
while (next < files.length) {
|
|
150
156
|
const file = files[next++];
|
|
151
157
|
try {
|
|
152
|
-
|
|
158
|
+
/* Preparazione e rete stanno nello STESSO worker: tre immagini vengono convertite in
|
|
159
|
+
* parallelo, e appena una è pronta parte il suo upload mentre le altre continuano. Non
|
|
160
|
+
* teniamo in memoria quaranta WebP aspettando che l'intero batch sia convertito. */
|
|
161
|
+
const ready = await prepare(file);
|
|
162
|
+
const r = await api.upload(prefix, ready);
|
|
153
163
|
out.push(r);
|
|
154
164
|
onProgress(++done, files.length, r);
|
|
155
165
|
}
|
package/dist/app/App.js
CHANGED
|
@@ -32,6 +32,7 @@ import { PagesList } from "./routes/PagesList";
|
|
|
32
32
|
import { PageEditor, PageHeader } from "./routes/PageEditor";
|
|
33
33
|
import { PageSeoEditor } from "./routes/PageSeoEditor";
|
|
34
34
|
import { SiteSettingsEditor, WebsiteSeoSettingsEditor } from "./routes/SiteSettingsEditor";
|
|
35
|
+
import { MediaSettingsEditor } from "./routes/MediaSettingsEditor";
|
|
35
36
|
import { HomeEditor } from "./routes/HomeEditor";
|
|
36
37
|
import { RequestsList, RequestDetail } from "./routes/Requests";
|
|
37
38
|
import { useRequests } from "./lib/requests";
|
|
@@ -67,6 +68,7 @@ export function App() {
|
|
|
67
68
|
colonna e la griglia nella terza, e sono due punti dello schermo lontani. Uno stato per
|
|
68
69
|
ciascuno vorrebbe dire due idee diverse di quale cartella si sta guardando. */
|
|
69
70
|
const media = useMediaLibrary({ mode: "manage", active: section === "media" });
|
|
71
|
+
const mediaSettingsOpen = view === "media" && route[1] === "settings";
|
|
70
72
|
/* Le richieste si caricano SUBITO, non entrando nella sezione: il numero «da gestire» nella
|
|
71
73
|
prima colonna è il motivo per cui uno ci entra, e un badge che compare solo dopo essere
|
|
72
74
|
andati a guardare non serve a niente. */
|
|
@@ -195,7 +197,9 @@ export function App() {
|
|
|
195
197
|
: section === "pages" ? (editing ? String(editing.title ?? editing.slug) : "Pagine")
|
|
196
198
|
: section === "home" ? (homeGroup || "Home")
|
|
197
199
|
: view === "trash" ? "Cestino"
|
|
198
|
-
: section === "media" ? (
|
|
200
|
+
: section === "media" ? (mediaSettingsOpen
|
|
201
|
+
? "Impostazioni Media"
|
|
202
|
+
: media.prefix ? media.labelOf(media.prefix) : "Senza cartella")
|
|
199
203
|
/* Consegne non ripete il titolo qui: il nome del transfer sta nella sua schermata, dove
|
|
200
204
|
si modifica, e si aggiorna mentre lo si scrive. Averlo in due posti significa averne
|
|
201
205
|
uno dei due sempre vecchio. */
|
|
@@ -220,7 +224,7 @@ export function App() {
|
|
|
220
224
|
: _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
|
|
221
225
|
if (editing?.slug === from)
|
|
222
226
|
go(`/pages/${to}${seoEditing ? "/seo" : ""}`);
|
|
223
|
-
} }) })] })), section === "media" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Cartelle" }) }), _jsx("div", { className: "col-body", children: _jsx(MediaFolders, { lib: media, trash: { active: view === "trash", open: () => go("/trash"), leave: () => go("/media") } }) })] })), section === "requests" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: pickedReq.size === 0 ? (_jsx("span", { className: "col-title", children: requests.rows ? `${requests.rows.length} richieste` : "Richieste" })) : (_jsxs("span", { className: "rq-bulk", children: [_jsxs("span", { className: "col-title", children: [pickedReq.size, " scelte"] }), _jsx("button", { className: "mg-act", onClick: () => { void requests.mark([...pickedReq], true); setPickedReq(new Set()); }, children: "Gestite" }), _jsx("button", { className: "mg-act is-danger", onClick: () => { void requests.remove([...pickedReq]); setPickedReq(new Set()); }, children: "Elimina" }), _jsx("button", { className: "mg-act", onClick: () => setPickedReq(new Set()), children: "\u00D7" })] })) }), _jsx("div", { className: "col-body", children: requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
227
|
+
} }) })] })), section === "media" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Cartelle" }) }), _jsx("div", { className: "col-body", children: _jsx(MediaFolders, { lib: media, trash: { active: view === "trash", open: () => go("/trash"), leave: () => go("/media") }, settings: { active: mediaSettingsOpen, open: () => go("/media/settings") } }) })] })), section === "requests" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: pickedReq.size === 0 ? (_jsx("span", { className: "col-title", children: requests.rows ? `${requests.rows.length} richieste` : "Richieste" })) : (_jsxs("span", { className: "rq-bulk", children: [_jsxs("span", { className: "col-title", children: [pickedReq.size, " scelte"] }), _jsx("button", { className: "mg-act", onClick: () => { void requests.mark([...pickedReq], true); setPickedReq(new Set()); }, children: "Gestite" }), _jsx("button", { className: "mg-act is-danger", onClick: () => { void requests.remove([...pickedReq]); setPickedReq(new Set()); }, children: "Elimina" }), _jsx("button", { className: "mg-act", onClick: () => setPickedReq(new Set()), children: "\u00D7" })] })) }), _jsx("div", { className: "col-body", children: requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
224
228
|
: !requests.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
225
229
|
: requests.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessuna richiesta, per ora." })
|
|
226
230
|
: _jsx(RequestsList, { rows: requests.rows, current: openReq?.id, picked: pickedReq, onOpen: (id) => go(`/requests/${id}`), onPick: (id) => setPickedReq((prev) => {
|
|
@@ -240,7 +244,7 @@ export function App() {
|
|
|
240
244
|
: deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
|
|
241
245
|
: _jsx(DeliveriesList, { rows: deliveries.rows, current: activeDeliveryId || quickSelectedId, onOpen: (id) => { setQuickSelectedId(undefined); go(`/deliveries/${id}`); } }) }), _jsxs("div", { className: "dl-col-foot", children: [deliveries.rows && _jsx(DeliveriesTotal, { rows: deliveries.rows }), _jsxs("button", { className: "col-item" + (deliverySettingsOpen ? " is-on" : ""), onClick: () => { setQuickSelectedId(undefined); go("/deliveries/settings"); }, children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] })), section === "bookings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: bookings.rows ? `${bookings.rows.length} appuntamenti` : "Prenotazioni" }) }), _jsx("div", { className: "col-body", children: bookings.error ? _jsx("p", { className: "ak-error", children: bookings.error })
|
|
242
246
|
: !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
243
|
-
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && (_jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => media.setUploadOpen(true), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
|
|
247
|
+
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && (_jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => media.setUploadOpen(true), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
|
|
244
248
|
/* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
|
|
245
249
|
rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
|
|
246
250
|
il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
|
|
@@ -261,7 +265,9 @@ export function App() {
|
|
|
261
265
|
: home.data ? _jsx(HomeEditorView, { data: home.data, group: homeGroup, onChange: home.change, pickMedia: pickMedia })
|
|
262
266
|
: _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: settings.error }), _jsx("button", { className: "ak-btn", onClick: () => void settings.load(), children: "Riprova" })] })) : settings.data ? (settingsView === "seo"
|
|
263
267
|
? _jsx(WebsiteSeoSettingsEditor, { value: settings.data, onChange: settings.change })
|
|
264
|
-
: _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, focus: route[1] })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" &&
|
|
268
|
+
: _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, focus: route[1], passwordDraftPending: settings.accessDraftPending, onDiscardPasswordDraft: () => void settings.discardSiteAccess().then(() => publish.reload()) })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && (mediaSettingsOpen
|
|
269
|
+
? _jsx(MediaSettingsEditor, { lib: media })
|
|
270
|
+
: _jsx(MediaGrid, { lib: media })), view === "trash" && _jsx(Trash, {}), section === "requests" && (requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
265
271
|
: openReq ? (_jsx(RequestDetail, { row: openReq, onMark: (h) => void requests.mark([openReq.id], h), onRemove: () => { void requests.remove([openReq.id]); go("/requests"); } }))
|
|
266
272
|
: _jsx("p", { className: "pane-empty ak-hint", children: requests.rows?.length ? "Scegli una richiesta dall'elenco." : "" })), section === "deliveries" && (deliveries.error ? _jsx("p", { className: "ak-error", children: deliveries.error })
|
|
267
273
|
: deliverySettingsOpen ? (_jsx(DeliverySettings, { settings: deliveries.settings, onAction: deliveries.action, pickMedia: pickMedia })) : activeDeliveryId && !deliveries.detail ? (_jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })) : (
|
package/dist/app/app.css
CHANGED
|
@@ -235,6 +235,7 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
235
235
|
.st-inline-help {
|
|
236
236
|
margin: -3px 0 0; color: var(--ak-ink-quiet); font: 400 11px/1.45 var(--ak-font);
|
|
237
237
|
}
|
|
238
|
+
.media-settings-note { margin-top: 13px; max-width: 520px; }
|
|
238
239
|
.st-favicon { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
239
240
|
.st-favicon-preview {
|
|
240
241
|
width: 48px; height: 48px; display: grid; place-items: center; flex: none;
|
|
@@ -1165,6 +1166,10 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
1165
1166
|
.uk-job.is-failed .uk-job-top span { color: #a33; }
|
|
1166
1167
|
.uk-track { height: 4px; overflow: hidden; border-radius: 3px; background: var(--ak-line); }
|
|
1167
1168
|
.uk-track i { display: block; height: 100%; background: var(--ak-accent); transition: width .25s; }
|
|
1169
|
+
.uk-phase { display: grid; gap: 4px; }
|
|
1170
|
+
.uk-phase > span { color: var(--ak-ink-quiet); font: 500 10px/1.2 var(--ak-font); }
|
|
1171
|
+
.uk-media-job .uk-phase:nth-of-type(3) .uk-track i { opacity: .65; }
|
|
1172
|
+
.uk-warning { margin: 0; color: #8a5a12; font-size: 11px; line-height: 1.45; }
|
|
1168
1173
|
.uk-job.is-paused .uk-track i { background: var(--ak-ink-quiet); }
|
|
1169
1174
|
.uk-error { margin: 0; color: #a33; font-size: 11px; line-height: 1.45; }
|
|
1170
1175
|
.uk-job-actions { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
|
package/dist/app/lib/publish.js
CHANGED
|
@@ -27,7 +27,10 @@ export function usePublish() {
|
|
|
27
27
|
.map((operation) => ({
|
|
28
28
|
message: operation.type === "move" ? "Spostamento Media pronto" : "Eliminazione Media pronta",
|
|
29
29
|
date: operation.updatedAt ? new Date(operation.updatedAt).toISOString() : null,
|
|
30
|
-
})))
|
|
30
|
+
}))).concat(d.siteAccess ? [{
|
|
31
|
+
message: d.siteAccess.enabled ? "Attivazione o cambio password del sito" : "Disattivazione password del sito",
|
|
32
|
+
date: null,
|
|
33
|
+
}] : []));
|
|
31
34
|
setConfigured(true);
|
|
32
35
|
setError(null);
|
|
33
36
|
}
|
|
@@ -26,4 +26,6 @@ export declare function useSiteSettings(active: boolean): {
|
|
|
26
26
|
change: (next: SiteSettings) => void;
|
|
27
27
|
save: () => Promise<void>;
|
|
28
28
|
setNotice: import("react").Dispatch<import("react").SetStateAction<string | null>>;
|
|
29
|
+
accessDraftPending: boolean;
|
|
30
|
+
discardSiteAccess: () => Promise<void>;
|
|
29
31
|
};
|
package/dist/app/lib/settings.js
CHANGED
|
@@ -37,6 +37,12 @@ export function useSiteSettings(active) {
|
|
|
37
37
|
const [saving, setSaving] = useState(false);
|
|
38
38
|
const [notice, setNotice] = useState(null);
|
|
39
39
|
const [revision, setRevision] = useState(null);
|
|
40
|
+
const [accessState, setAccessState] = useState(null);
|
|
41
|
+
const [editorialBaseline, setEditorialBaseline] = useState("");
|
|
42
|
+
const editorialSnapshot = (settings) => {
|
|
43
|
+
const { password: _password, passwordEnabled: _passwordEnabled, ...editorial } = settings;
|
|
44
|
+
return JSON.stringify(editorial);
|
|
45
|
+
};
|
|
40
46
|
const load = useCallback(async () => {
|
|
41
47
|
try {
|
|
42
48
|
const mapped = site().siteSettingsDocument;
|
|
@@ -49,12 +55,18 @@ export function useSiteSettings(active) {
|
|
|
49
55
|
const settings = mapped.fromPayload ? mapped.fromPayload(stored) : stored;
|
|
50
56
|
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
51
57
|
throw new Error("content_unavailable");
|
|
52
|
-
|
|
58
|
+
const access = site().siteAccess ? (await api.siteAccessState()).state : null;
|
|
59
|
+
const loaded = { ...empty, ...settings, passwordEnabled: access?.effective?.enabled ?? false, password: "" };
|
|
60
|
+
setData(loaded);
|
|
61
|
+
setAccessState(access);
|
|
62
|
+
setEditorialBaseline(editorialSnapshot(loaded));
|
|
53
63
|
setRevision({ base: document.draftRevisionId ?? document.publishedRevisionId, expected: document.draftRevisionId });
|
|
54
64
|
}
|
|
55
65
|
else {
|
|
56
66
|
const result = await call("/admin/api/settings");
|
|
57
|
-
|
|
67
|
+
const loaded = { ...empty, ...result.settings, password: "" };
|
|
68
|
+
setData(loaded);
|
|
69
|
+
setEditorialBaseline(editorialSnapshot(loaded));
|
|
58
70
|
}
|
|
59
71
|
setError(null);
|
|
60
72
|
setDirty(false);
|
|
@@ -75,10 +87,19 @@ export function useSiteSettings(active) {
|
|
|
75
87
|
try {
|
|
76
88
|
const mapped = site().siteSettingsDocument;
|
|
77
89
|
if (contentPersistenceMode() === "d1" && mapped && revision) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
if (editorialSnapshot(data) !== editorialBaseline) {
|
|
91
|
+
const revisionId = crypto.randomUUID();
|
|
92
|
+
const payload = mapped.toPayload ? mapped.toPayload(data) : data;
|
|
93
|
+
await api.saveContent({ revisionId, documentId: mapped.documentId, baseRevisionId: revision.base, expectedDraftRevisionId: revision.expected, kind: mapped.kind, locale: mapped.locale, schemaVersion: mapped.schemaVersion, payload: payload, tombstone: false });
|
|
94
|
+
setRevision({ base: revisionId, expected: revisionId });
|
|
95
|
+
setEditorialBaseline(editorialSnapshot(data));
|
|
96
|
+
}
|
|
97
|
+
const effectiveEnabled = accessState?.effective?.enabled ?? false;
|
|
98
|
+
if (site().siteAccess && (data.password !== "" || data.passwordEnabled !== effectiveEnabled)) {
|
|
99
|
+
const result = await api.saveSiteAccess(data.passwordEnabled, data.password, accessState?.draft?.versionId ?? null);
|
|
100
|
+
setAccessState(result.state);
|
|
101
|
+
setData({ ...data, password: "", passwordEnabled: result.state.effective?.enabled ?? false });
|
|
102
|
+
}
|
|
82
103
|
setDirty(false);
|
|
83
104
|
setNotice("Bozza D1 salvata");
|
|
84
105
|
return;
|
|
@@ -101,10 +122,35 @@ export function useSiteSettings(active) {
|
|
|
101
122
|
finally {
|
|
102
123
|
setSaving(false);
|
|
103
124
|
}
|
|
104
|
-
}, [data, revision]);
|
|
125
|
+
}, [data, revision, accessState, editorialBaseline]);
|
|
105
126
|
const change = useCallback((next) => {
|
|
106
127
|
setData(next);
|
|
107
128
|
setDirty(true);
|
|
108
129
|
}, []);
|
|
109
|
-
|
|
130
|
+
const discardSiteAccess = useCallback(async () => {
|
|
131
|
+
if (!accessState?.draft)
|
|
132
|
+
return;
|
|
133
|
+
setSaving(true);
|
|
134
|
+
setNotice(null);
|
|
135
|
+
try {
|
|
136
|
+
const result = await api.discardSiteAccess(accessState.draft.versionId);
|
|
137
|
+
setAccessState(result.state);
|
|
138
|
+
setData((current) => {
|
|
139
|
+
if (!current)
|
|
140
|
+
return current;
|
|
141
|
+
const next = { ...current, password: "", passwordEnabled: result.state.active?.enabled ?? false };
|
|
142
|
+
setDirty(editorialSnapshot(next) !== editorialBaseline);
|
|
143
|
+
return next;
|
|
144
|
+
});
|
|
145
|
+
setNotice("Modifica della password annullata");
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
setNotice(error.message);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
setSaving(false);
|
|
152
|
+
}
|
|
153
|
+
}, [accessState, editorialBaseline]);
|
|
154
|
+
return { data, error, dirty, saving, notice, load, change, save, setNotice,
|
|
155
|
+
accessDraftPending: Boolean(accessState?.draft), discardSiteAccess };
|
|
110
156
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function MediaSettingsEditor({ lib }) {
|
|
3
|
+
return (_jsxs("section", { className: "settings-editor", children: [_jsxs("header", { className: "st-intro", children: [_jsx("span", { children: "Impostazioni Media" }), _jsx("h2", { children: "Caricamento immagini" }), _jsx("p", { children: "Ottimizza le immagini prima che vengano inviate alla libreria del sito." })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Ottimizzazione automatica" }), _jsxs("div", { className: "st-row", children: [_jsxs("div", { className: "st-copy", children: [_jsx("h3", { children: "Converti le immagini in WebP" }), _jsx("p", { children: "JPG, JPEG, PNG, WebP, HEIC e gli altri formati immagine compatibili vengono convertiti in WebP, qualit\u00E0 80, con lato lungo massimo di 2500 px. I file pi\u00F9 piccoli non vengono ingranditi. Video e file non immagine restano invariati." })] }), _jsxs("div", { className: "st-control", children: [_jsxs("label", { className: "st-toggle" + (lib.settingsBusy ? " is-locked" : ""), children: [_jsx("input", { type: "checkbox", checked: lib.autoWebp, disabled: lib.settingsBusy, onChange: (event) => void lib.setAutoWebp(event.target.checked) }), _jsx("span", { className: "st-toggle-track", "aria-hidden": "true", children: _jsx("span", {}) }), _jsxs("span", { children: [lib.autoWebp ? "On" : "Off", " \u00B7 Conversione automatica"] })] }), _jsx("p", { className: "st-inline-help media-settings-note", children: "La conversione (da una a tre immagini) e l\u2019upload (da due a quattro file) usano pool adattivi e procedono in parallelo in base al dispositivo e alla connessione. L\u2019impostazione vale per questo sito e per tutti gli utenti del pannello." })] })] })] }), lib.error && _jsx("p", { className: "ak-error", onClick: lib.clearError, children: lib.error })] }));
|
|
4
|
+
}
|
|
@@ -7,13 +7,15 @@ export declare const EXTRAS: {
|
|
|
7
7
|
card: string;
|
|
8
8
|
short: string;
|
|
9
9
|
}[];
|
|
10
|
-
export declare function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, }: {
|
|
10
|
+
export declare function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, passwordDraftPending, onDiscardPasswordDraft, }: {
|
|
11
11
|
value: SiteSettings;
|
|
12
12
|
onChange: (next: SiteSettings) => void;
|
|
13
13
|
pickMediaFiles: (o: {
|
|
14
14
|
multiple: boolean;
|
|
15
15
|
}) => Promise<AssetFile[] | null>;
|
|
16
16
|
focus?: string;
|
|
17
|
+
passwordDraftPending?: boolean;
|
|
18
|
+
onDiscardPasswordDraft?: () => void;
|
|
17
19
|
}): import("react").JSX.Element;
|
|
18
20
|
export declare function WebsiteSeoSettingsEditor({ value, onChange, }: {
|
|
19
21
|
value: SiteSettings;
|
|
@@ -35,8 +35,9 @@ function SettingRow({ title, help, children, }) {
|
|
|
35
35
|
function Field({ value, onChange, maxLength, placeholder, type = "text", disabled = false, }) {
|
|
36
36
|
return (_jsx("input", { className: "st-input", type: type, value: value, maxLength: maxLength, placeholder: placeholder, disabled: disabled, onChange: (event) => onChange(event.target.value) }));
|
|
37
37
|
}
|
|
38
|
-
export function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, }) {
|
|
38
|
+
export function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, passwordDraftPending = false, onDiscardPasswordDraft, }) {
|
|
39
39
|
const d1Content = contentPersistenceMode() === "d1";
|
|
40
|
+
const managedSiteAccess = d1Content && Boolean(site().siteAccess);
|
|
40
41
|
const update = (key, next) => onChange({ ...value, [key]: next });
|
|
41
42
|
const extras = useRef(null);
|
|
42
43
|
/* Salto SECCO (`behavior: "auto"`), non fluido, e al frame successivo.
|
|
@@ -58,7 +59,7 @@ export function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, })
|
|
|
58
59
|
if (selected?.[0])
|
|
59
60
|
update("favicon", selected[0].url);
|
|
60
61
|
};
|
|
61
|
-
return (_jsxs("section", { className: "settings-editor", children: [_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." })] }), _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, onChange: (next) => update("password", next) }), _jsx(Toggle, { checked: value.passwordEnabled, label: "Protezione con password", disabled: d1Content, onChange: (next) => update("passwordEnabled", next) }), d1Content && (_jsx("p", { className: "st-inline-help", children: "La protezione esistente resta attiva e viene gestita dall\u2019operatore durante la migrazione D1." })), 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 di consegna ai 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"] })] })] })] }));
|
|
62
|
+
return (_jsxs("section", { className: "settings-editor", children: [_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." })] }), _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 di consegna ai 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"] })] })] })] }));
|
|
62
63
|
}
|
|
63
64
|
export function WebsiteSeoSettingsEditor({ value, onChange, }) {
|
|
64
65
|
const update = (key, next) => onChange({ ...value, [key]: next });
|
|
@@ -12,11 +12,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
12
12
|
*/
|
|
13
13
|
import { useEffect, useState } from "react";
|
|
14
14
|
import { cancelUpload, dismissUpload, isBusy, pauseUpload, resumeUpload, useUploads, } from "../lib/upload-queue";
|
|
15
|
+
import { dismissMediaUpload, useMediaUploads, } from "../../form/media/media-upload-queue.js";
|
|
15
16
|
export function UploadDock() {
|
|
16
17
|
const jobs = useUploads();
|
|
18
|
+
const mediaJobs = useMediaUploads();
|
|
17
19
|
const [open, setOpen] = useState(true);
|
|
18
20
|
const [confirmStop, setConfirmStop] = useState(null);
|
|
19
|
-
const busy = jobs.some((job) => job.state === "running");
|
|
21
|
+
const busy = jobs.some(isBusy) || mediaJobs.some((job) => job.state === "running");
|
|
20
22
|
useEffect(() => {
|
|
21
23
|
if (!busy)
|
|
22
24
|
return;
|
|
@@ -24,21 +26,41 @@ export function UploadDock() {
|
|
|
24
26
|
window.addEventListener("beforeunload", warn);
|
|
25
27
|
return () => window.removeEventListener("beforeunload", warn);
|
|
26
28
|
}, [busy]);
|
|
27
|
-
if (!jobs.length)
|
|
29
|
+
if (!jobs.length && !mediaJobs.length)
|
|
28
30
|
return null;
|
|
29
|
-
return (_jsxs("aside", { className: "uk" + (open ? "" : " is-closed"), "aria-label": "Caricamenti", children: [_jsxs("button", { className: "uk-head", onClick: () => setOpen((value) => !value), "aria-expanded": open, children: [_jsx("span", { className: "uk-head-text", children: summary(jobs) }), _jsx("span", { className: "uk-chevron", "aria-hidden": "true", children: open ? "▾" : "▴" })] }), open && (
|
|
31
|
+
return (_jsxs("aside", { className: "uk" + (open ? "" : " is-closed"), "aria-label": "Caricamenti", children: [_jsxs("button", { className: "uk-head", onClick: () => setOpen((value) => !value), "aria-expanded": open, children: [_jsx("span", { className: "uk-head-text", children: summary(jobs, mediaJobs) }), _jsx("span", { className: "uk-chevron", "aria-hidden": "true", children: open ? "▾" : "▴" })] }), open && (_jsxs("ul", { className: "uk-list", children: [mediaJobs.map((job) => _jsx(MediaJob, { job: job }, job.id)), jobs.map((job) => (_jsxs("li", { className: "uk-job is-" + job.state, children: [_jsxs("div", { className: "uk-job-top", children: [_jsx("strong", { children: job.title }), _jsx("span", { children: line(job) })] }), isBusy(job) && (_jsx("div", { className: "uk-track", children: _jsx("i", { style: { width: `${percent(job)}%` } }) })), job.error && _jsx("p", { className: "uk-error", children: job.error }), _jsxs("div", { className: "uk-job-actions", children: [job.state === "running" && (_jsx("button", { className: "ak-btn", onClick: () => pauseUpload(job.id), children: "Pausa" })), job.state === "paused" && (_jsx("button", { className: "ak-btn", onClick: () => resumeUpload(job.id), children: "Riprendi" })), isBusy(job) && (confirmStop === job.id ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "uk-confirm", children: "I file gi\u00E0 caricati verranno buttati." }), _jsx("button", { className: "ak-btn", onClick: () => setConfirmStop(null), children: "No" }), _jsx("button", { className: "ak-btn dl-danger-btn", onClick: () => { cancelUpload(job.id); setConfirmStop(null); }, children: "Annulla il caricamento" })] })) : (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setConfirmStop(job.id), children: "Annulla" }))), !isBusy(job) && (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => dismissUpload(job.id), children: "Togli dall'elenco" }))] })] }, job.id)))] }))] }));
|
|
30
32
|
}
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
+
function MediaJob({ job }) {
|
|
34
|
+
const conversionPercent = job.total ? (job.conversionDone / job.total) * 100 : 0;
|
|
35
|
+
const uploadPercent = job.total ? (job.uploadDone / job.total) * 100 : 0;
|
|
36
|
+
return (_jsxs("li", { className: "uk-job uk-media-job is-" + job.state, children: [_jsxs("div", { className: "uk-job-top", children: [_jsx("strong", { children: job.title }), _jsx("span", { children: mediaLine(job) })] }), _jsxs("div", { className: "uk-phase", children: [_jsxs("span", { children: [job.autoWebp ? "Conversione" : "Preparazione", " \u00B7 ", job.conversionDone, "/", job.total] }), _jsx("div", { className: "uk-track", children: _jsx("i", { style: { width: `${Math.max(3, conversionPercent)}%` } }) })] }), _jsxs("div", { className: "uk-phase", children: [_jsxs("span", { children: ["Upload \u00B7 ", job.uploadDone, "/", job.total] }), _jsx("div", { className: "uk-track", children: _jsx("i", { style: { width: `${Math.max(3, uploadPercent)}%` } }) })] }), job.originals > 0 && (_jsx("p", { className: "uk-warning", children: job.originals === 1 ? "1 originale caricato dopo tre tentativi di conversione." :
|
|
37
|
+
`${job.originals} originali caricati dopo tre tentativi di conversione.` })), job.errors.map((error, index) => _jsx("p", { className: "uk-error", children: error }, `${index}-${error}`)), job.state !== "running" && (_jsx("div", { className: "uk-job-actions", children: _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => dismissMediaUpload(job.id), children: "Togli dall\u2019elenco" }) }))] }));
|
|
38
|
+
}
|
|
39
|
+
function mediaLine(job) {
|
|
40
|
+
if (job.state === "failed")
|
|
41
|
+
return `Completato con ${job.failed} ${job.failed === 1 ? "errore" : "errori"}`;
|
|
42
|
+
if (job.state === "done")
|
|
43
|
+
return job.originals ? "Completato con originali" : "Completato";
|
|
44
|
+
if (job.uploadDone < job.conversionDone)
|
|
45
|
+
return "Conversione e upload in parallelo";
|
|
46
|
+
return job.autoWebp ? "Converto le immagini" : "Preparo i file";
|
|
47
|
+
}
|
|
48
|
+
function summary(jobs, mediaJobs) {
|
|
49
|
+
const running = jobs.filter((job) => job.state === "running").length +
|
|
50
|
+
mediaJobs.filter((job) => job.state === "running").length;
|
|
33
51
|
const paused = jobs.filter((job) => job.state === "paused").length;
|
|
34
|
-
const
|
|
52
|
+
const interrupted = jobs.filter((job) => job.state === "failed").length;
|
|
53
|
+
const partial = mediaJobs.filter((job) => job.state === "failed").length;
|
|
54
|
+
const total = jobs.length + mediaJobs.length;
|
|
35
55
|
if (running)
|
|
36
56
|
return running === 1 ? "Caricamento in corso" : `${running} caricamenti in corso`;
|
|
37
57
|
if (paused)
|
|
38
58
|
return paused === 1 ? "Caricamento in pausa" : `${paused} caricamenti in pausa`;
|
|
39
|
-
if (
|
|
40
|
-
return
|
|
41
|
-
|
|
59
|
+
if (interrupted)
|
|
60
|
+
return interrupted === 1 ? "Caricamento interrotto" : `${interrupted} caricamenti interrotti`;
|
|
61
|
+
if (partial)
|
|
62
|
+
return partial === 1 ? "Caricamento completato con errori" : `${partial} caricamenti con errori`;
|
|
63
|
+
return total === 1 ? "Caricamento completato" : "Caricamenti completati";
|
|
42
64
|
}
|
|
43
65
|
function line(job) {
|
|
44
66
|
if (job.state === "done") {
|
package/dist/app/site.d.ts
CHANGED
|
@@ -82,6 +82,10 @@ export interface SiteAdminConfig {
|
|
|
82
82
|
fromPayload?: (payload: unknown) => Record<string, unknown>;
|
|
83
83
|
toPayload?: (settings: Record<string, unknown>) => unknown;
|
|
84
84
|
};
|
|
85
|
+
/** Separate versioned D1 access control. Passwords never enter an editorial document. */
|
|
86
|
+
siteAccess?: {
|
|
87
|
+
siteId: string;
|
|
88
|
+
};
|
|
85
89
|
}
|
|
86
90
|
/** Chiamata da `mountAdmin` prima del primo render. */
|
|
87
91
|
export declare function setSiteConfig(config: SiteAdminConfig): void;
|
package/dist/deployment.js
CHANGED
|
@@ -191,13 +191,26 @@ export class D1DeploymentAttemptRepository {
|
|
|
191
191
|
WHERE draft_revision_id IN (SELECT revision_id FROM content_release_entries WHERE release_id = ?)
|
|
192
192
|
AND document_id IN (SELECT document_id FROM content_release_entries WHERE release_id = ?) AND ${guard}`)
|
|
193
193
|
.bind(input.now, input.releaseId, input.releaseId, input.attemptId, input.releaseId, input.tokenChecksum, input.now),
|
|
194
|
+
this.db.prepare(`UPDATE site_access_versions SET state = 'superseded', superseded_at = ?, rollback_expires_at = ?
|
|
195
|
+
WHERE state = 'active' AND version_id IS NOT (SELECT version_id FROM content_release_site_access WHERE release_id = ?)
|
|
196
|
+
AND EXISTS (SELECT 1 FROM content_release_site_access WHERE release_id = ?) AND ${guard}`)
|
|
197
|
+
.bind(input.now, input.now + 7 * 24 * 60 * 60 * 1000, input.releaseId, input.releaseId, input.attemptId, input.releaseId, input.tokenChecksum, input.now),
|
|
198
|
+
this.db.prepare(`UPDATE site_access_versions SET state = 'active', activated_at = ?
|
|
199
|
+
WHERE version_id = (SELECT version_id FROM content_release_site_access WHERE release_id = ?) AND state = 'draft' AND ${guard}`)
|
|
200
|
+
.bind(input.now, input.releaseId, input.attemptId, input.releaseId, input.tokenChecksum, input.now),
|
|
201
|
+
this.db.prepare(`UPDATE site_access_state SET
|
|
202
|
+
active_version_id = COALESCE((SELECT version_id FROM content_release_site_access WHERE release_id = ?), active_version_id),
|
|
203
|
+
draft_version_id = CASE WHEN draft_version_id = (SELECT version_id FROM content_release_site_access WHERE release_id = ?) THEN NULL ELSE draft_version_id END,
|
|
204
|
+
updated_at = ?
|
|
205
|
+
WHERE singleton = 1 AND EXISTS (SELECT 1 FROM content_release_site_access WHERE release_id = ?) AND ${guard}`)
|
|
206
|
+
.bind(input.releaseId, input.releaseId, input.now, input.releaseId, input.attemptId, input.releaseId, input.tokenChecksum, input.now),
|
|
194
207
|
this.db.prepare(`UPDATE production_deployment_attempts SET phase = 'succeeded', active_slot = NULL, updated_at = ?, finished_at = ?
|
|
195
208
|
WHERE attempt_id = ? AND release_id = ? AND phase = 'verifying_canonical' AND ci_claim_token_checksum = ?
|
|
196
209
|
AND ci_claim_expires_at > ? AND EXISTS (SELECT 1 FROM content_site_state WHERE singleton = 1 AND current_release_id = ?)`)
|
|
197
210
|
.bind(input.now, input.now, input.attemptId, input.releaseId, input.tokenChecksum, input.now, input.releaseId),
|
|
198
211
|
];
|
|
199
212
|
const results = await this.db.batch(statements);
|
|
200
|
-
if (results[0]?.meta?.changes !== 1 || results[1]?.meta?.changes !== 1 || results[
|
|
213
|
+
if (results[0]?.meta?.changes !== 1 || results[1]?.meta?.changes !== 1 || results[6]?.meta?.changes !== 1) {
|
|
201
214
|
throw new Error("attempt_unavailable");
|
|
202
215
|
}
|
|
203
216
|
}
|
|
@@ -9,7 +9,7 @@ export declare function FolderMenu({ lib, prefix, label, openTo }: {
|
|
|
9
9
|
label: string;
|
|
10
10
|
openTo?: "left" | "auto";
|
|
11
11
|
}): import("react").JSX.Element;
|
|
12
|
-
export declare function MediaFolders({ lib, trash }: {
|
|
12
|
+
export declare function MediaFolders({ lib, trash, settings }: {
|
|
13
13
|
lib: MediaLibrary;
|
|
14
14
|
/** `leave` riporta ai media: dal cestino, cliccare una cartella deve anche USCIRE dal cestino. */
|
|
15
15
|
trash?: {
|
|
@@ -17,4 +17,8 @@ export declare function MediaFolders({ lib, trash }: {
|
|
|
17
17
|
open: () => void;
|
|
18
18
|
leave: () => void;
|
|
19
19
|
};
|
|
20
|
+
settings?: {
|
|
21
|
+
active: boolean;
|
|
22
|
+
open: () => void;
|
|
23
|
+
};
|
|
20
24
|
}): import("react").JSX.Element;
|
|
@@ -63,10 +63,11 @@ export function FolderMenu({ lib, prefix, label, openTo = "left" }) {
|
|
|
63
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
64
|
}
|
|
65
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
|
+
const IcoSettings = () => (_jsxs("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.35", strokeLinecap: "round", strokeLinejoin: "round", d: "M6.6 2.2h2.8l.4 1.5c.4.2.8.4 1.1.7l1.5-.4 1.4 2.4-1.1 1.1v1l1.1 1.1-1.4 2.4-1.5-.4c-.3.3-.7.5-1.1.7l-.4 1.5H6.6l-.4-1.5a5 5 0 0 1-1.1-.7l-1.5.4-1.4-2.4 1.1-1.1v-1L2.2 6.4 3.6 4l1.5.4c.3-.3.7-.5 1.1-.7l.4-1.5Z" }), _jsx("circle", { cx: "8", cy: "8", r: "2", fill: "none", stroke: "currentColor", strokeWidth: "1.35" })] }));
|
|
66
67
|
/* `trash` è facoltativo perché il cestino è una ROTTA, e le rotte le conosce l'applicazione, non
|
|
67
68
|
* questo componente. Sta qui in fondo, sotto «+ Cartella», perché è l'ultimo posto dove finiscono
|
|
68
69
|
* le foto: nella colonna che elenca dove stanno, non in una barra da un'altra parte. */
|
|
69
|
-
export function MediaFolders({ lib, trash }) {
|
|
70
|
+
export function MediaFolders({ lib, trash, settings }) {
|
|
70
71
|
const [over, setOver] = useState(null);
|
|
71
72
|
const [draggingFolder, setDraggingFolder] = useState(false);
|
|
72
73
|
const canDrop = lib.mode === "manage";
|
|
@@ -112,5 +113,5 @@ export function MediaFolders({ lib, trash }) {
|
|
|
112
113
|
+ (over === f.prefix ? " is-over" : ""), onDragOver: (e) => { if (canDrop) {
|
|
113
114
|
e.preventDefault();
|
|
114
115
|
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
|
+
} }, 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" })] }) }) })), settings && (_jsx("ul", { className: "mf-list mf-settings-tail", children: _jsx("li", { className: "mf-item" + (settings.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", onClick: settings.open, children: [_jsx(IcoSettings, {}), _jsx("span", { className: "mf-name", children: "Impostazioni" })] }) }) }))] }));
|
|
116
117
|
}
|