@cardinali-studio/adminkit 2.2.21 → 2.2.23
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.css +15 -0
- package/dist/app/next.css +1 -0
- package/dist/app/routes/GallerySectionsEditor.d.ts +1 -1
- package/dist/app/routes/GallerySectionsEditor.js +14 -2
- package/dist/app/routes/PageEditor.d.ts +10 -0
- package/dist/app/routes/PageEditor.js +31 -8
- package/dist/app/routes/PortfolioWorksEditor.d.ts +1 -1
- package/dist/app/routes/PortfolioWorksEditor.js +11 -3
- 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 +1 -1
|
@@ -126,10 +126,27 @@ export async function onRequestPost({ request, env, data }) {
|
|
|
126
126
|
const operable = keyList.filter((key) => !pending.has(key));
|
|
127
127
|
const result = operable.length ? await d1DeleteMedia({ env, data, body: { ...body, keys: operable } }) : null;
|
|
128
128
|
for (const keepKey of keepKeys) await env.MEDIA.delete(keepKey);
|
|
129
|
-
return json({
|
|
129
|
+
return json({
|
|
130
|
+
ok: true,
|
|
131
|
+
trashed: result?.trashed ?? keyList.length,
|
|
132
|
+
operationId: result?.operationId,
|
|
133
|
+
pagesUpdated: result?.publishedDocuments?.map((document) => document.kind) ?? [],
|
|
134
|
+
requiresPublication: result?.requiresPublication === true,
|
|
135
|
+
deferred: result?.requiresPublication === true || Number(result?.failed ?? 0) > 0,
|
|
136
|
+
completed: result ? Number(result.completed ?? 0) : keyList.length,
|
|
137
|
+
failed: Number(result?.failed ?? 0),
|
|
138
|
+
d1: true,
|
|
139
|
+
warning: result?.requiresPublication ? "Le pagine verranno pubblicate senza questi Media prima di spostarli nel cestino." : undefined,
|
|
140
|
+
});
|
|
130
141
|
} catch (error) {
|
|
131
|
-
const code = error?.message === "unauthenticated" ? "unauthenticated"
|
|
132
|
-
|
|
142
|
+
const code = error?.message === "unauthenticated" ? "unauthenticated"
|
|
143
|
+
: error?.message === "media_source_busy" ? "media_source_busy"
|
|
144
|
+
: error?.message === "invalid_document" ? "media_replacement_required"
|
|
145
|
+
: "media_operation_unavailable";
|
|
146
|
+
const message = code === "media_source_busy" ? "Questo Media è già in un'operazione in corso."
|
|
147
|
+
: code === "media_replacement_required" ? "Una pagina richiede questa immagine: sostituiscila prima di eliminarla."
|
|
148
|
+
: "Non riesco a registrare l'operazione Media D1.";
|
|
149
|
+
return json({ ok: false, code, error: message }, code === "unauthenticated" ? 401 : code === "media_replacement_required" ? 422 : 409);
|
|
133
150
|
}
|
|
134
151
|
}
|
|
135
152
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/* D1 Media operation status and bounded recovery. These routes exist only
|
|
2
2
|
* after D1 cutover; Git-mode Media keeps its established pending-marker flow. */
|
|
3
|
-
import { json } from "../../_shared/assets.js";
|
|
3
|
+
import { guessMime, isKey, json } from "../../_shared/assets.js";
|
|
4
4
|
import { D1MediaOperationRepository } from "../../../media.js";
|
|
5
|
-
import { contentPersistenceMode } from "../../site.js";
|
|
5
|
+
import { contentPersistenceMode, languages } from "../../site.js";
|
|
6
6
|
import { cancelD1MediaOperation, resumeD1MediaOperation } from "../../_shared/d1-media.js";
|
|
7
|
+
import {
|
|
8
|
+
altLocales, mediaAltMetadataValue, normalizeMediaAlt,
|
|
9
|
+
MEDIA_ALT_METADATA_KEY, MEDIA_UPLOADED_METADATA_KEY,
|
|
10
|
+
} from "../../_shared/media-alt.js";
|
|
7
11
|
|
|
8
12
|
function unavailable() { return json({ ok: false, code: "feature_inactive", error: "Journal Media D1 non attivo." }, 404); }
|
|
9
13
|
function failure(error) {
|
|
@@ -11,6 +15,57 @@ function failure(error) {
|
|
|
11
15
|
return json({ ok: false, code, error: code === "unauthenticated" ? "Sessione non valida: ricarica la pagina per rientrare." : "L'operazione Media non può essere completata." }, code === "unauthenticated" ? 401 : 409);
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
/** Aggiorna solo i metadata editoriali del file. R2 richiede di riscrivere l'oggetto per
|
|
19
|
+
* cambiare customMetadata: conserviamo quindi esplicitamente la data originaria, oltre a tutti
|
|
20
|
+
* i metadata HTTP/EXIF già presenti. */
|
|
21
|
+
export async function onRequestPatch({ request, env }) {
|
|
22
|
+
if (!env.MEDIA) return json({ ok: false, error: "Binding MEDIA non configurato" }, 500);
|
|
23
|
+
let body;
|
|
24
|
+
try { body = await request.json(); } catch { return json({ ok: false, code: "media_alt_invalid", error: "JSON non valido." }, 400); }
|
|
25
|
+
const key = typeof body?.key === "string" ? body.key : "";
|
|
26
|
+
if (!isKey(key) || key.startsWith("_trash/") || key.startsWith("_adminkit/")) {
|
|
27
|
+
return json({ ok: false, code: "media_alt_invalid", error: "File Media non valido." }, 400);
|
|
28
|
+
}
|
|
29
|
+
const object = await env.MEDIA.get(key);
|
|
30
|
+
if (!object) return json({ ok: false, code: "media_not_found", error: "Il file non esiste più." }, 404);
|
|
31
|
+
const type = object.httpMetadata?.contentType || guessMime(key);
|
|
32
|
+
if (!type.startsWith("image/")) {
|
|
33
|
+
return json({ ok: false, code: "media_alt_not_applicable", error: "Il testo alternativo si applica alle immagini." }, 422);
|
|
34
|
+
}
|
|
35
|
+
let alt;
|
|
36
|
+
try { alt = normalizeMediaAlt(body.alt, altLocales(languages())); }
|
|
37
|
+
catch (error) {
|
|
38
|
+
const tooLarge = error?.message === "media_alt_too_large";
|
|
39
|
+
return json({
|
|
40
|
+
ok: false,
|
|
41
|
+
code: tooLarge ? "media_alt_too_large" : "media_alt_invalid",
|
|
42
|
+
error: tooLarge ? "I testi alternativi sono troppo estesi per i metadati del file." : "Testo alternativo non valido.",
|
|
43
|
+
}, 400);
|
|
44
|
+
}
|
|
45
|
+
const customMetadata = { ...(object.customMetadata || {}) };
|
|
46
|
+
const encoded = mediaAltMetadataValue(alt);
|
|
47
|
+
if (encoded) customMetadata[MEDIA_ALT_METADATA_KEY] = encoded;
|
|
48
|
+
else delete customMetadata[MEDIA_ALT_METADATA_KEY];
|
|
49
|
+
if (!customMetadata[MEDIA_UPLOADED_METADATA_KEY]) {
|
|
50
|
+
customMetadata[MEDIA_UPLOADED_METADATA_KEY] = object.uploaded instanceof Date
|
|
51
|
+
? object.uploaded.toISOString()
|
|
52
|
+
: new Date().toISOString();
|
|
53
|
+
}
|
|
54
|
+
const saved = await env.MEDIA.put(key, object.body, {
|
|
55
|
+
httpMetadata: object.httpMetadata || {},
|
|
56
|
+
customMetadata,
|
|
57
|
+
...(object.etag ? { onlyIf: { etagMatches: object.etag } } : {}),
|
|
58
|
+
});
|
|
59
|
+
if (object.etag && saved === null) {
|
|
60
|
+
return json({
|
|
61
|
+
ok: false,
|
|
62
|
+
code: "media_alt_conflict",
|
|
63
|
+
error: "Il file è cambiato durante il salvataggio. Riaprilo e riprova.",
|
|
64
|
+
}, 409);
|
|
65
|
+
}
|
|
66
|
+
return json({ ok: true, key, alt });
|
|
67
|
+
}
|
|
68
|
+
|
|
14
69
|
export async function onRequestGet({ env }) {
|
|
15
70
|
if (contentPersistenceMode() !== "d1") return unavailable();
|
|
16
71
|
if (!env.CONTENT_DB) return json({ ok: false, code: "content_unavailable", error: "Persistenza contenuti non disponibile." }, 503);
|
|
@@ -145,7 +145,11 @@ export async function onRequestPost({ request, env, data }) {
|
|
|
145
145
|
if (Object.hasOwn(body, "applyRefs")) return json({ ok: true, moved: 0, pagesUpdated: [], failed: [], d1: true });
|
|
146
146
|
try {
|
|
147
147
|
const result = await d1MoveMedia({ env, data, body });
|
|
148
|
-
return json({
|
|
148
|
+
return json({
|
|
149
|
+
ok: true, ...result,
|
|
150
|
+
pagesUpdated: result.publishedDocuments.map((document) => document.kind), failed: [],
|
|
151
|
+
warning: result.requiresPublication ? "Le pagine verranno pubblicate con i nuovi indirizzi prima di rimuovere le sorgenti." : undefined,
|
|
152
|
+
});
|
|
149
153
|
} catch (error) {
|
|
150
154
|
const code = error?.message === "unauthenticated" ? "unauthenticated" : error?.message === "media_source_busy" ? "media_source_busy" : "media_operation_unavailable";
|
|
151
155
|
return json({ ok: false, code, error: code === "media_source_busy" ? "Questo Media è già in un'operazione in corso." : "Non riesco a registrare l'operazione Media D1." }, code === "unauthenticated" ? 401 : 409);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { isPrefix, json, guessMime, uniqueKey } from "../../_shared/assets.js";
|
|
6
6
|
import { extractCapturedAt } from "../../_shared/exif.js";
|
|
7
7
|
import { sizeFromBuffer } from "../../_shared/imagesize.js";
|
|
8
|
+
import { MEDIA_UPLOADED_METADATA_KEY } from "../../_shared/media-alt.js";
|
|
8
9
|
|
|
9
10
|
export async function onRequestPost({ request, env }) {
|
|
10
11
|
if (!env.MEDIA) return json({ ok: false, error: "Binding MEDIA non configurato" }, 500);
|
|
@@ -21,7 +22,7 @@ export async function onRequestPost({ request, env }) {
|
|
|
21
22
|
if (!ctype || ctype === "application/octet-stream") ctype = guessMime(key);
|
|
22
23
|
|
|
23
24
|
// data di scatto (EXIF) per l'ordinamento "Data scatto"
|
|
24
|
-
const custom = {};
|
|
25
|
+
const custom = { [MEDIA_UPLOADED_METADATA_KEY]: new Date().toISOString() };
|
|
25
26
|
const suppliedCapturedAt = request.headers.get("X-Captured-At") || "";
|
|
26
27
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(suppliedCapturedAt)) {
|
|
27
28
|
custom.capturedAt = suppliedCapturedAt;
|
|
@@ -52,6 +53,7 @@ export async function onRequestPost({ request, env }) {
|
|
|
52
53
|
capturedAt: custom.capturedAt || null,
|
|
53
54
|
w: wh ? wh[0] : null,
|
|
54
55
|
h: wh ? wh[1] : null,
|
|
56
|
+
alt: { decorative: false, texts: {} },
|
|
55
57
|
url: "/media/" + key.split("/").map(encodeURIComponent).join("/"),
|
|
56
58
|
});
|
|
57
59
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
import { isPrefix, json, listKeysUnder } from "../_shared/assets.js";
|
|
5
5
|
import { sizeFromBucket } from "../_shared/imagesize.js";
|
|
6
6
|
import { pendingSourceKeys } from "../_shared/pending-media.js";
|
|
7
|
-
import { contentPersistenceMode } from "../site.js";
|
|
7
|
+
import { contentPersistenceMode, languages } from "../site.js";
|
|
8
8
|
import { d1PendingSourceKeys } from "../_shared/d1-media.js";
|
|
9
|
+
import { altLocales, mediaAltFromMetadata, MEDIA_UPLOADED_METADATA_KEY } from "../_shared/media-alt.js";
|
|
9
10
|
|
|
10
11
|
const IMG = /\.(webp|jpe?g|png|avif|gif)$/i;
|
|
11
12
|
|
|
@@ -15,6 +16,7 @@ export async function onRequestGet({ request, env }) {
|
|
|
15
16
|
let prefix = (url.searchParams.get("prefix") || "").trim();
|
|
16
17
|
if (!isPrefix(prefix)) return json({ ok: false, error: `prefisso non valido: ${prefix}` }, 400);
|
|
17
18
|
const pending = contentPersistenceMode() === "d1" ? await d1PendingSourceKeys(env.CONTENT_DB) : await pendingSourceKeys(env.MEDIA);
|
|
19
|
+
const locales = altLocales(languages());
|
|
18
20
|
|
|
19
21
|
/* `recursive=1` guarda TUTTO quello che c'è sotto, a qualunque profondità: serve a chi deve
|
|
20
22
|
* spostare o cancellare una cartella intera, che altrimenti si porterebbe dietro solo il primo
|
|
@@ -82,11 +84,12 @@ export async function onRequestGet({ request, env }) {
|
|
|
82
84
|
name: o.key.slice(prefix.length),
|
|
83
85
|
size: o.size,
|
|
84
86
|
url: "/media/" + o.key.split("/").map(encodeURIComponent).join("/"),
|
|
85
|
-
uploaded: o.uploaded,
|
|
87
|
+
uploaded: cm[MEDIA_UPLOADED_METADATA_KEY] || o.uploaded,
|
|
86
88
|
capturedAt: cm.capturedAt || null,
|
|
87
89
|
type: (o.httpMetadata && o.httpMetadata.contentType) || "",
|
|
88
90
|
w: w > 0 ? w : null,
|
|
89
91
|
h: h > 0 ? h : null,
|
|
92
|
+
alt: mediaAltFromMetadata(cm, locales),
|
|
90
93
|
};
|
|
91
94
|
}));
|
|
92
95
|
|
package/dist/gallery.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type GalleryImage = {
|
|
|
3
3
|
w?: number | null;
|
|
4
4
|
h?: number | null;
|
|
5
5
|
hidden?: boolean;
|
|
6
|
+
/** Copia iniziale del default Media, modificabile nel contesto della singola gallery. */
|
|
7
|
+
alt?: string;
|
|
6
8
|
};
|
|
7
9
|
export type SectionType = "header" | "text" | "image" | "gallery";
|
|
8
10
|
/** Una sezione della pagina gallery. Ha TUTTI i campi qualunque sia il tipo: Zod e React vogliono
|
package/dist/media.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { D1DatabaseLike } from "./persistence.js";
|
|
2
|
+
export declare function mediaPublicUrl(key: string): string;
|
|
3
|
+
/** Rewrites exactly the Media references owned by one operation. Exact array
|
|
4
|
+
* values are removed as array items; scalar slots become an empty string and
|
|
5
|
+
* are then accepted or rejected by the site's complete schema validator. */
|
|
6
|
+
export declare function rewriteMediaValue(value: unknown, input: {
|
|
7
|
+
replacements?: Readonly<Record<string, string>>;
|
|
8
|
+
removals?: readonly string[];
|
|
9
|
+
}): {
|
|
10
|
+
changed: boolean;
|
|
11
|
+
value?: unknown;
|
|
12
|
+
removed?: boolean;
|
|
13
|
+
};
|
|
2
14
|
export declare const mediaOperationStates: readonly ["preparing", "ready", "blocked_in_use", "bound", "finalizing", "completed", "completed_with_warning", "cancelled", "failed"];
|
|
3
15
|
export type MediaOperationState = typeof mediaOperationStates[number];
|
|
4
16
|
export type MediaOperationType = "move" | "delete";
|
|
@@ -95,6 +107,11 @@ export declare class D1MediaOperationRepository {
|
|
|
95
107
|
metadataJson: string | null;
|
|
96
108
|
}): Promise<"pending" | "ready">;
|
|
97
109
|
beginFinalization(operationId: string, releaseId: string, now: number): Promise<void>;
|
|
110
|
+
/** An operation whose sources occur in neither the public manifest nor any
|
|
111
|
+
* current draft needs no content release. The same guarded transition closes
|
|
112
|
+
* the last race before R2 cleanup; a concurrent reference makes it stay
|
|
113
|
+
* ready for the isolated-publication path instead. */
|
|
114
|
+
beginUnpublishedFinalization(operationId: string, now: number): Promise<boolean>;
|
|
98
115
|
/** Individual cleanup writes are retryable. A single failed entry reaches
|
|
99
116
|
* `completed_with_warning` only after every entry has a terminal result;
|
|
100
117
|
* the already-live release is never rolled back. */
|
|
@@ -124,9 +141,11 @@ export declare class D1MediaOperationRepository {
|
|
|
124
141
|
/** Work is named by its immutable release binding: there is intentionally no
|
|
125
142
|
* "latest operation" cleanup path. */
|
|
126
143
|
finalizationWork(releaseId: string): Promise<readonly BoundMediaEntry[]>;
|
|
144
|
+
unboundFinalizationWork(operationId: string): Promise<readonly BoundMediaEntry[]>;
|
|
127
145
|
/** A warning is a live release with incomplete housekeeping. Retrying resets
|
|
128
146
|
* only failed effects, never an already-completed entry. */
|
|
129
147
|
resumeFinalization(operationId: string, releaseId: string, now: number): Promise<void>;
|
|
148
|
+
resumeUnboundFinalization(operationId: string, now: number): Promise<void>;
|
|
130
149
|
readyDeleteEntry(input: {
|
|
131
150
|
operationId: string;
|
|
132
151
|
entryId: string;
|
package/dist/media.js
CHANGED
|
@@ -1,4 +1,81 @@
|
|
|
1
1
|
import { canonicalJson } from "./persistence.js";
|
|
2
|
+
const MEDIA_PREFIX = "/media/";
|
|
3
|
+
export function mediaPublicUrl(key) {
|
|
4
|
+
return MEDIA_PREFIX + key.split("/").map(encodeURIComponent).join("/");
|
|
5
|
+
}
|
|
6
|
+
function replaceBoundedMediaUrl(value, from, to) {
|
|
7
|
+
let cursor = 0;
|
|
8
|
+
let next = "";
|
|
9
|
+
while (cursor < value.length) {
|
|
10
|
+
const found = value.indexOf(from, cursor);
|
|
11
|
+
if (found < 0)
|
|
12
|
+
return next + value.slice(cursor);
|
|
13
|
+
const after = value[found + from.length];
|
|
14
|
+
/* A media key is a complete path, not a string prefix. Query/fragment
|
|
15
|
+
* suffixes and further URL/path characters belong to another reference. */
|
|
16
|
+
if (after && /[A-Za-z0-9._~%/?#-]/.test(after)) {
|
|
17
|
+
next += value.slice(cursor, found + from.length);
|
|
18
|
+
cursor = found + from.length;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
next += value.slice(cursor, found) + to;
|
|
22
|
+
cursor = found + from.length;
|
|
23
|
+
}
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
26
|
+
/** Rewrites exactly the Media references owned by one operation. Exact array
|
|
27
|
+
* values are removed as array items; scalar slots become an empty string and
|
|
28
|
+
* are then accepted or rejected by the site's complete schema validator. */
|
|
29
|
+
export function rewriteMediaValue(value, input) {
|
|
30
|
+
const replacements = Object.fromEntries(Object.entries(input.replacements ?? {})
|
|
31
|
+
.map(([from, to]) => [mediaPublicUrl(from), mediaPublicUrl(to)]));
|
|
32
|
+
const removals = new Set((input.removals ?? []).map(mediaPublicUrl));
|
|
33
|
+
const visit = (item, inArray) => {
|
|
34
|
+
if (typeof item === "string") {
|
|
35
|
+
if (inArray && removals.has(item))
|
|
36
|
+
return { changed: true, removed: true };
|
|
37
|
+
let next = item;
|
|
38
|
+
for (const [from, to] of Object.entries(replacements))
|
|
39
|
+
next = replaceBoundedMediaUrl(next, from, to);
|
|
40
|
+
for (const from of removals)
|
|
41
|
+
next = replaceBoundedMediaUrl(next, from, "");
|
|
42
|
+
return { changed: next !== item, value: next };
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(item)) {
|
|
45
|
+
let changed = false;
|
|
46
|
+
const next = [];
|
|
47
|
+
for (const child of item) {
|
|
48
|
+
const result = visit(child, true);
|
|
49
|
+
changed ||= result.changed;
|
|
50
|
+
if (!result.removed)
|
|
51
|
+
next.push(result.value);
|
|
52
|
+
}
|
|
53
|
+
return { changed, value: next };
|
|
54
|
+
}
|
|
55
|
+
if (item && typeof item === "object") {
|
|
56
|
+
/* Gallery entries conventionally carry the asset identity in `src` and
|
|
57
|
+
* exist only to describe that asset (dimensions/visibility may surround
|
|
58
|
+
* it). Removing the whole array item preserves a valid collection;
|
|
59
|
+
* broader object shapes remain intact and let the site validator decide
|
|
60
|
+
* whether an empty optional slot is acceptable or needs replacement. */
|
|
61
|
+
if (inArray && typeof item.src === "string"
|
|
62
|
+
&& removals.has(item.src)) {
|
|
63
|
+
return { changed: true, removed: true };
|
|
64
|
+
}
|
|
65
|
+
let changed = false;
|
|
66
|
+
const next = {};
|
|
67
|
+
for (const [key, child] of Object.entries(item)) {
|
|
68
|
+
const result = visit(child, false);
|
|
69
|
+
changed ||= result.changed;
|
|
70
|
+
if (!result.removed)
|
|
71
|
+
next[key] = result.value;
|
|
72
|
+
}
|
|
73
|
+
return { changed, value: next };
|
|
74
|
+
}
|
|
75
|
+
return { changed: false, value: item };
|
|
76
|
+
};
|
|
77
|
+
return visit(value, false);
|
|
78
|
+
}
|
|
2
79
|
export const mediaOperationStates = [
|
|
3
80
|
"preparing", "ready", "blocked_in_use", "bound", "finalizing", "completed", "completed_with_warning", "cancelled", "failed",
|
|
4
81
|
];
|
|
@@ -171,6 +248,37 @@ export class D1MediaOperationRepository {
|
|
|
171
248
|
if (result.meta?.changes !== 1)
|
|
172
249
|
throw new Error("media_operation_unavailable");
|
|
173
250
|
}
|
|
251
|
+
/** An operation whose sources occur in neither the public manifest nor any
|
|
252
|
+
* current draft needs no content release. The same guarded transition closes
|
|
253
|
+
* the last race before R2 cleanup; a concurrent reference makes it stay
|
|
254
|
+
* ready for the isolated-publication path instead. */
|
|
255
|
+
async beginUnpublishedFinalization(operationId, now) {
|
|
256
|
+
const result = await this.db.prepare(`UPDATE media_operations SET state = 'finalizing', updated_at = ?
|
|
257
|
+
WHERE operation_id = ? AND state = 'ready'
|
|
258
|
+
AND NOT EXISTS (SELECT 1 FROM content_release_media_operations WHERE operation_id = ?)
|
|
259
|
+
AND NOT EXISTS (SELECT 1 FROM media_operation_entries source
|
|
260
|
+
JOIN content_site_state state ON state.singleton = 1
|
|
261
|
+
JOIN content_release_entries published_entry ON published_entry.release_id = state.current_release_id
|
|
262
|
+
JOIN content_revisions published ON published.document_id = published_entry.document_id
|
|
263
|
+
AND published.revision_id = published_entry.revision_id
|
|
264
|
+
WHERE source.operation_id = media_operations.operation_id AND published.payload_json IS NOT NULL
|
|
265
|
+
AND instr(published.payload_json, '/media/' || source.source_key) > 0)
|
|
266
|
+
AND NOT EXISTS (SELECT 1 FROM media_operation_entries source
|
|
267
|
+
JOIN content_releases release ON release.status = 'publishing'
|
|
268
|
+
JOIN content_release_entries candidate_entry ON candidate_entry.release_id = release.release_id
|
|
269
|
+
JOIN content_revisions candidate ON candidate.document_id = candidate_entry.document_id
|
|
270
|
+
AND candidate.revision_id = candidate_entry.revision_id
|
|
271
|
+
WHERE source.operation_id = media_operations.operation_id AND candidate.payload_json IS NOT NULL
|
|
272
|
+
AND instr(candidate.payload_json, '/media/' || source.source_key) > 0)
|
|
273
|
+
AND NOT EXISTS (SELECT 1 FROM media_operation_entries source
|
|
274
|
+
JOIN content_documents document
|
|
275
|
+
JOIN content_revisions draft ON draft.document_id = document.document_id
|
|
276
|
+
AND draft.revision_id = document.draft_revision_id
|
|
277
|
+
WHERE source.operation_id = media_operations.operation_id AND draft.payload_json IS NOT NULL
|
|
278
|
+
AND instr(draft.payload_json, '/media/' || source.source_key) > 0)`)
|
|
279
|
+
.bind(now, operationId, operationId).run();
|
|
280
|
+
return result.meta?.changes === 1;
|
|
281
|
+
}
|
|
174
282
|
/** Individual cleanup writes are retryable. A single failed entry reaches
|
|
175
283
|
* `completed_with_warning` only after every entry has a terminal result;
|
|
176
284
|
* the already-live release is never rolled back. */
|
|
@@ -284,6 +392,17 @@ export class D1MediaOperationRepository {
|
|
|
284
392
|
ORDER BY operation.operation_id, entry.entry_id`).bind(releaseId).all()).results ?? [];
|
|
285
393
|
return rows;
|
|
286
394
|
}
|
|
395
|
+
async unboundFinalizationWork(operationId) {
|
|
396
|
+
return (await this.db.prepare(`SELECT operation.operation_id AS operationId, operation.type,
|
|
397
|
+
operation.state AS operationState, entry.entry_id AS entryId, entry.source_key AS sourceKey,
|
|
398
|
+
entry.destination_key AS destinationKey, entry.finalization_state AS finalizationState
|
|
399
|
+
FROM media_operations operation
|
|
400
|
+
JOIN media_operation_entries entry ON entry.operation_id = operation.operation_id
|
|
401
|
+
WHERE operation.operation_id = ? AND operation.state IN ('finalizing', 'completed_with_warning')
|
|
402
|
+
AND NOT EXISTS (SELECT 1 FROM content_release_media_operations WHERE operation_id = operation.operation_id)
|
|
403
|
+
AND entry.finalization_state IN ('pending', 'failed')
|
|
404
|
+
ORDER BY entry.entry_id`).bind(operationId).all()).results ?? [];
|
|
405
|
+
}
|
|
287
406
|
/** A warning is a live release with incomplete housekeeping. Retrying resets
|
|
288
407
|
* only failed effects, never an already-completed entry. */
|
|
289
408
|
async resumeFinalization(operationId, releaseId, now) {
|
|
@@ -300,6 +419,19 @@ export class D1MediaOperationRepository {
|
|
|
300
419
|
if (results[0]?.meta?.changes !== 1)
|
|
301
420
|
throw new Error("media_operation_unavailable");
|
|
302
421
|
}
|
|
422
|
+
async resumeUnboundFinalization(operationId, now) {
|
|
423
|
+
const resume = this.db.prepare(`UPDATE media_operations SET state = 'finalizing', updated_at = ?, retry_count = retry_count + 1
|
|
424
|
+
WHERE operation_id = ? AND state = 'completed_with_warning'
|
|
425
|
+
AND NOT EXISTS (SELECT 1 FROM content_release_media_operations WHERE operation_id = ?)`)
|
|
426
|
+
.bind(now, operationId, operationId);
|
|
427
|
+
const reset = this.db.prepare(`UPDATE media_operation_entries SET finalization_state = 'pending', safe_error = NULL
|
|
428
|
+
WHERE operation_id = ? AND finalization_state = 'failed'
|
|
429
|
+
AND EXISTS (SELECT 1 FROM media_operations WHERE operation_id = ? AND state = 'finalizing')`)
|
|
430
|
+
.bind(operationId, operationId);
|
|
431
|
+
const results = await this.db.batch([resume, reset]);
|
|
432
|
+
if (results[0]?.meta?.changes !== 1)
|
|
433
|
+
throw new Error("media_operation_unavailable");
|
|
434
|
+
}
|
|
303
435
|
async readyDeleteEntry(input) {
|
|
304
436
|
const entry = this.db.prepare(`UPDATE media_operation_entries
|
|
305
437
|
SET object_checksum = ?, etag = ?, metadata_json = ?, preparation_state = 'ready'
|
package/dist/normalize.d.ts
CHANGED
package/dist/normalize.js
CHANGED
|
@@ -33,7 +33,7 @@ export const CARD_KINDS = [
|
|
|
33
33
|
{ type: "frosted", label: "Testo su immagine", hint: "Parole sopra una fotografia" },
|
|
34
34
|
];
|
|
35
35
|
export function emptyWork() {
|
|
36
|
-
return { title: "", cover: "", description: [], info: [], cards: [] };
|
|
36
|
+
return { title: "", cover: "", coverAlt: "", description: [], info: [], cards: [] };
|
|
37
37
|
}
|
|
38
38
|
/* Il tipo di una card nuova è SEMPRE esplicito, mai stringa vuota: un `select` mostra comunque
|
|
39
39
|
* la prima opzione quando il valore non combacia con nessuna, quindi una card salvata senza
|
|
@@ -48,6 +48,7 @@ export function normalizeWorks(value) {
|
|
|
48
48
|
return {
|
|
49
49
|
title: str(item.title),
|
|
50
50
|
cover: str(item.cover),
|
|
51
|
+
coverAlt: str(item.coverAlt),
|
|
51
52
|
description: list(item.description).map(str).filter(Boolean),
|
|
52
53
|
info: list(item.info).map((entry) => {
|
|
53
54
|
const o = obj(entry);
|
package/dist/persistence.d.ts
CHANGED
|
@@ -103,6 +103,13 @@ export type SaveInput = Revision & {
|
|
|
103
103
|
authorSubject: string;
|
|
104
104
|
authorLabel: string;
|
|
105
105
|
};
|
|
106
|
+
/** A system-owned revision that may enter one release without becoming the
|
|
107
|
+
* document's editorial draft. Its ancestry is the exact revision in the
|
|
108
|
+
* current public manifest; an existing draft is deliberately neither selected
|
|
109
|
+
* nor cleared. */
|
|
110
|
+
export type DetachedRevisionInput = Omit<SaveInput, "expectedDraftRevisionId"> & {
|
|
111
|
+
baseRevisionId: string;
|
|
112
|
+
};
|
|
106
113
|
export type ReleaseInput = {
|
|
107
114
|
releaseId: string;
|
|
108
115
|
requestChecksum: string;
|
|
@@ -122,6 +129,7 @@ export type ReleaseInput = {
|
|
|
122
129
|
siteAccessVersionId?: string | null;
|
|
123
130
|
};
|
|
124
131
|
export type ReleaseDeploymentInput = ReleaseInput & {
|
|
132
|
+
detachedRevisions?: readonly DetachedRevisionInput[];
|
|
125
133
|
attempt: {
|
|
126
134
|
attemptId: string;
|
|
127
135
|
intentKind: string;
|
package/dist/persistence.js
CHANGED
|
@@ -332,11 +332,19 @@ export class D1ContentRepository {
|
|
|
332
332
|
: [];
|
|
333
333
|
const manifest = new Map(inherited.map((entry) => [entry.document_id, entry.revision_id]));
|
|
334
334
|
const selected = [...input.entries];
|
|
335
|
+
const detached = [...(input.detachedRevisions ?? [])];
|
|
335
336
|
const mediaOperationIds = [...new Set(input.mediaOperationIds ?? [])];
|
|
336
337
|
if (!selected.length && !mediaOperationIds.length && !input.siteAccessVersionId)
|
|
337
338
|
throw new Error("invalid_document");
|
|
339
|
+
if (new Set(detached.map((entry) => entry.documentId)).size !== detached.length
|
|
340
|
+
|| new Set(detached.map((entry) => entry.revisionId)).size !== detached.length
|
|
341
|
+
|| detached.some((entry) => selected.some((draft) => draft.documentId === entry.documentId)
|
|
342
|
+
|| (entry.payload === null) === !entry.tombstone))
|
|
343
|
+
throw new Error("invalid_document");
|
|
338
344
|
for (const entry of selected)
|
|
339
345
|
manifest.set(entry.documentId, entry.revisionId);
|
|
346
|
+
for (const entry of detached)
|
|
347
|
+
manifest.set(entry.documentId, entry.revisionId);
|
|
340
348
|
const selectionGuard = selected.length ? selected.map(() => "EXISTS (SELECT 1 FROM content_documents WHERE document_id = ? AND draft_revision_id = ?)").join(" AND ") : "1 = 1";
|
|
341
349
|
const accessGuard = input.siteAccessVersionId ? `AND EXISTS (SELECT 1 FROM site_access_versions version JOIN site_access_state access_state ON access_state.singleton = 1
|
|
342
350
|
WHERE version.version_id = ? AND version.state IN ('draft','active') AND version.version_id IN (access_state.draft_version_id, access_state.active_version_id))` : "";
|
|
@@ -346,6 +354,22 @@ export class D1ContentRepository {
|
|
|
346
354
|
AND NOT EXISTS (SELECT 1 FROM production_deployment_attempts WHERE active_slot = 1) AND ${selectionGuard} ${accessGuard}`)
|
|
347
355
|
.bind(input.releaseId, input.requestChecksum, input.manifestChecksum, input.schemaPackageVersion, input.materializerVersion, input.authorSubject, input.now, state.current_release_id, ...selected.flatMap((entry) => [entry.documentId, entry.revisionId]), ...(input.siteAccessVersionId ? [input.siteAccessVersionId] : []));
|
|
348
356
|
const statements = [];
|
|
357
|
+
if (detached.length) {
|
|
358
|
+
const eligible = detached.map(() => `(
|
|
359
|
+
EXISTS (SELECT 1 FROM content_documents document WHERE document.document_id = ? AND document.kind = ? AND document.locale = ?)
|
|
360
|
+
AND EXISTS (SELECT 1 FROM content_site_state state JOIN content_release_entries entry
|
|
361
|
+
ON entry.release_id = state.current_release_id WHERE state.singleton = 1
|
|
362
|
+
AND entry.document_id = ? AND entry.revision_id = ?)
|
|
363
|
+
AND NOT EXISTS (SELECT 1 FROM content_revisions WHERE revision_id = ?)
|
|
364
|
+
)`).join(" AND ");
|
|
365
|
+
statements.push(this.db.prepare(`INSERT INTO content_site_state
|
|
366
|
+
(singleton, current_release_id, schema_generation, updated_at)
|
|
367
|
+
SELECT 2, NULL, 0, ? WHERE NOT (${eligible})`)
|
|
368
|
+
.bind(input.now, ...detached.flatMap((entry) => [
|
|
369
|
+
entry.documentId, entry.kind, entry.locale, entry.documentId, entry.baseRevisionId, entry.revisionId,
|
|
370
|
+
])));
|
|
371
|
+
}
|
|
372
|
+
let mediaEligibility = null;
|
|
349
373
|
if (mediaOperationIds.length) {
|
|
350
374
|
const revisionIds = [...manifest.values()];
|
|
351
375
|
const eligible = mediaOperationIds.map(() => `(
|
|
@@ -353,17 +377,45 @@ export class D1ContentRepository {
|
|
|
353
377
|
AND NOT EXISTS (SELECT 1 FROM media_operation_entries source
|
|
354
378
|
JOIN content_revisions revision ON revision.revision_id IN (${revisionIds.map(() => "?").join(",")})
|
|
355
379
|
WHERE source.operation_id = ? AND revision.payload_json IS NOT NULL
|
|
356
|
-
AND instr(revision.payload_json, '/media/' || source.source_key) > 0)
|
|
380
|
+
AND instr(revision.payload_json, '/media/' || source.source_key) > 0)
|
|
381
|
+
AND NOT EXISTS (SELECT 1 FROM media_operation_entries source
|
|
382
|
+
JOIN content_documents document
|
|
383
|
+
JOIN content_revisions draft ON draft.document_id = document.document_id
|
|
384
|
+
AND draft.revision_id = document.draft_revision_id
|
|
385
|
+
WHERE source.operation_id = ? AND draft.payload_json IS NOT NULL
|
|
386
|
+
AND instr(draft.payload_json, '/media/' || source.source_key) > 0))`).join(" AND ");
|
|
357
387
|
/* As with multi-document Save, zero affected rows do not abort a D1
|
|
358
388
|
* batch. Convert an ineligible Media binding into a CHECK violation
|
|
359
|
-
*
|
|
360
|
-
|
|
389
|
+
* inside the same transaction, after detached revisions exist but before
|
|
390
|
+
* any dependent binding can become durable. */
|
|
391
|
+
mediaEligibility = this.db.prepare(`INSERT INTO content_site_state
|
|
361
392
|
(singleton, current_release_id, schema_generation, updated_at)
|
|
362
393
|
SELECT 2, NULL, 0, ? WHERE NOT (${eligible})`)
|
|
363
|
-
.bind(input.now, ...mediaOperationIds.flatMap((operationId) => [operationId, ...revisionIds, operationId]))
|
|
394
|
+
.bind(input.now, ...mediaOperationIds.flatMap((operationId) => [operationId, ...revisionIds, operationId, operationId]));
|
|
364
395
|
}
|
|
365
396
|
const candidateIndex = statements.length;
|
|
366
397
|
statements.push(candidate);
|
|
398
|
+
/* Detached revisions are guarded by the candidate itself. If the lane is
|
|
399
|
+
* busy or the base release moved, they affect zero rows together with the
|
|
400
|
+
* rest of the release instead of becoming orphan system revisions. */
|
|
401
|
+
for (const entry of detached) {
|
|
402
|
+
statements.push(this.db.prepare(`INSERT INTO content_revisions
|
|
403
|
+
(revision_id, document_id, base_revision_id, payload_json, is_tombstone, schema_version,
|
|
404
|
+
payload_checksum, request_checksum, author_subject, author_label, created_at)
|
|
405
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM content_documents document
|
|
406
|
+
JOIN content_site_state state ON state.singleton = 1
|
|
407
|
+
JOIN content_release_entries published ON published.release_id = state.current_release_id
|
|
408
|
+
AND published.document_id = document.document_id
|
|
409
|
+
WHERE document.document_id = ? AND document.kind = ? AND document.locale = ?
|
|
410
|
+
AND published.revision_id = ? AND NOT EXISTS
|
|
411
|
+
(SELECT 1 FROM content_revisions existing WHERE existing.revision_id = ?)
|
|
412
|
+
AND EXISTS (SELECT 1 FROM content_releases release
|
|
413
|
+
WHERE release.release_id = ? AND release.status = 'publishing')`)
|
|
414
|
+
.bind(entry.revisionId, entry.documentId, entry.baseRevisionId, entry.payload === null ? null : canonicalJson(entry.payload), Number(entry.tombstone), entry.schemaVersion, entry.payloadChecksum, entry.requestChecksum, entry.authorSubject, entry.authorLabel, entry.now, entry.documentId, entry.kind, entry.locale, entry.baseRevisionId, entry.revisionId, input.releaseId));
|
|
415
|
+
}
|
|
416
|
+
const mediaEligibilityIndex = mediaEligibility ? statements.length : -1;
|
|
417
|
+
if (mediaEligibility)
|
|
418
|
+
statements.push(mediaEligibility);
|
|
367
419
|
/* Every dependent write is guarded by the exact candidate. D1 batches are
|
|
368
420
|
* atomic on SQL errors, but a guarded INSERT that affects zero rows is not
|
|
369
421
|
* an error: without this predicate, a busy lane made these FK inserts
|
|
@@ -391,7 +443,13 @@ export class D1ContentRepository {
|
|
|
391
443
|
JOIN content_release_entries entry ON entry.release_id = ?
|
|
392
444
|
JOIN content_revisions revision ON revision.document_id = entry.document_id AND revision.revision_id = entry.revision_id
|
|
393
445
|
WHERE source.operation_id = media_operations.operation_id
|
|
394
|
-
AND revision.payload_json IS NOT NULL AND instr(revision.payload_json, '/media/' || source.source_key) > 0)
|
|
446
|
+
AND revision.payload_json IS NOT NULL AND instr(revision.payload_json, '/media/' || source.source_key) > 0)
|
|
447
|
+
AND NOT EXISTS (SELECT 1 FROM media_operation_entries source
|
|
448
|
+
JOIN content_documents document
|
|
449
|
+
JOIN content_revisions draft ON draft.document_id = document.document_id
|
|
450
|
+
AND draft.revision_id = document.draft_revision_id
|
|
451
|
+
WHERE source.operation_id = media_operations.operation_id AND draft.payload_json IS NOT NULL
|
|
452
|
+
AND instr(draft.payload_json, '/media/' || source.source_key) > 0)`)
|
|
395
453
|
.bind(input.now, operationId, input.releaseId));
|
|
396
454
|
statements.push(this.db.prepare(`INSERT INTO content_release_media_operations (release_id, operation_id)
|
|
397
455
|
SELECT ?, ? WHERE EXISTS (SELECT 1 FROM content_releases WHERE release_id = ? AND status = 'publishing')
|
|
@@ -405,7 +463,9 @@ export class D1ContentRepository {
|
|
|
405
463
|
AND EXISTS (SELECT 1 FROM content_releases WHERE release_id = ? AND status = 'publishing')`)
|
|
406
464
|
.bind(input.attempt.attemptId, input.attempt.intentKind, input.attempt.requestedReleaseId, input.attempt.requestedCodeSha, input.attempt.codeSha, input.releaseId, input.attempt.expectedFingerprint, input.now, input.now, input.releaseId));
|
|
407
465
|
const results = await this.db.batch(statements);
|
|
408
|
-
if (results[candidateIndex]?.meta?.changes !== 1
|
|
466
|
+
if (results[candidateIndex]?.meta?.changes !== 1
|
|
467
|
+
|| results.slice(candidateIndex + 1, -1).some((result, offset) => candidateIndex + 1 + offset !== mediaEligibilityIndex
|
|
468
|
+
&& result.meta?.changes !== 1)
|
|
409
469
|
|| results.at(-1)?.meta?.changes !== 1)
|
|
410
470
|
throw new Error("deployment_lane_busy");
|
|
411
471
|
return "created";
|
|
@@ -424,6 +484,18 @@ export class D1ContentRepository {
|
|
|
424
484
|
SET phase = 'failed', active_slot = NULL, error_code = ?, error_stage = ?, safe_message = ?, updated_at = ?, finished_at = ?
|
|
425
485
|
WHERE attempt_id = ? AND release_id = ? AND phase = 'waiting' AND ci_claim_token_checksum IS NULL AND ci_claim_expires_at IS NULL`)
|
|
426
486
|
.bind(input.code, input.stage, input.message, input.now, input.now, input.attemptId, input.releaseId),
|
|
487
|
+
/* Dispatch failed before CI could claim the immutable candidate. Its
|
|
488
|
+
* Media intents must become publishable again in this same recovery
|
|
489
|
+
* transaction; otherwise one transient GitHub error strands them bound
|
|
490
|
+
* forever to a failed release. */
|
|
491
|
+
this.db.prepare(`UPDATE media_operations SET state = 'ready', updated_at = ?
|
|
492
|
+
WHERE state = 'bound' AND operation_id IN
|
|
493
|
+
(SELECT operation_id FROM content_release_media_operations WHERE release_id = ?)
|
|
494
|
+
AND EXISTS (SELECT 1 FROM content_releases WHERE release_id = ? AND status = 'failed')`)
|
|
495
|
+
.bind(input.now, input.releaseId, input.releaseId),
|
|
496
|
+
this.db.prepare(`DELETE FROM content_release_media_operations WHERE release_id = ?
|
|
497
|
+
AND EXISTS (SELECT 1 FROM content_releases WHERE release_id = ? AND status = 'failed')`)
|
|
498
|
+
.bind(input.releaseId, input.releaseId),
|
|
427
499
|
]);
|
|
428
500
|
if (results[0]?.meta?.changes !== 1 || results[1]?.meta?.changes !== 1)
|
|
429
501
|
throw new Error("attempt_unavailable");
|
package/dist/theme.css
CHANGED
|
@@ -405,6 +405,13 @@ a.ak-btn { text-decoration: none; display: inline-flex; align-items: center; }
|
|
|
405
405
|
max-width: 100%; max-height: 100%; width: auto; height: auto; display: block;
|
|
406
406
|
background: var(--ak-line); box-shadow: 0 1px 4px rgba(0, 0, 0, .16);
|
|
407
407
|
}
|
|
408
|
+
.mg-alt-badge {
|
|
409
|
+
position: absolute; top: 5px; right: 5px; z-index: 2; pointer-events: none;
|
|
410
|
+
padding: 4px 6px; border-radius: 999px; background: rgba(20, 20, 20, .78); color: #fff;
|
|
411
|
+
font: 600 9px/1 var(--ak-font); text-transform: uppercase; letter-spacing: .04em;
|
|
412
|
+
}
|
|
413
|
+
.mg-alt-badge.is-missing { background: var(--ak-danger); }
|
|
414
|
+
.mg-alt-badge.is-decorative { background: rgba(80, 80, 80, .82); }
|
|
408
415
|
/* La cornice della selezione è DECORAZIONE: `pointer-events: none` o si prende lei il click.
|
|
409
416
|
Essendo un ::after del frame viene disegnata dopo la casella, quindi le stava sopra: premere
|
|
410
417
|
la spunta di un elemento già selezionato non lo deselezionava, lo apriva. */
|
|
@@ -494,6 +501,41 @@ a.ak-btn { text-decoration: none; display: inline-flex; align-items: center; }
|
|
|
494
501
|
.lb-info dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 5px 14px; font: 400 12px/1.4 var(--ak-font); }
|
|
495
502
|
.lb-info dt { color: #9b9b98; }
|
|
496
503
|
.lb-info dd { margin: 0; word-break: break-word; }
|
|
504
|
+
.lb-alt-button { font: 700 11px/1 var(--ak-font); letter-spacing: .04em; }
|
|
505
|
+
.lb-alt {
|
|
506
|
+
position: absolute; right: 12px; top: 58px; z-index: 3; width: min(390px, calc(100vw - 24px));
|
|
507
|
+
max-height: calc(100vh - 76px); overflow: auto; display: flex; flex-direction: column; gap: 14px;
|
|
508
|
+
background: rgba(22, 22, 22, .97); border: 1px solid rgba(255, 255, 255, .16);
|
|
509
|
+
border-radius: 9px; padding: 16px; color: #eaeaea; box-shadow: 0 18px 48px rgba(0, 0, 0, .4);
|
|
510
|
+
}
|
|
511
|
+
.lb-alt > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
|
512
|
+
.lb-alt > header div { display: flex; flex-direction: column; gap: 3px; }
|
|
513
|
+
.lb-alt > header strong { font: 600 15px/1.2 var(--ak-font); }
|
|
514
|
+
.lb-alt > header span { color: #a9a9a5; font: 400 11px/1.3 var(--ak-font); }
|
|
515
|
+
.lb-alt > header button {
|
|
516
|
+
border: 0; background: transparent; color: #fff; cursor: pointer; padding: 0 2px;
|
|
517
|
+
font: 300 22px/1 var(--ak-font);
|
|
518
|
+
}
|
|
519
|
+
.lb-alt-decorative { display: flex; align-items: flex-start; gap: 9px; cursor: pointer; }
|
|
520
|
+
.lb-alt-decorative input { margin: 2px 0 0; }
|
|
521
|
+
.lb-alt-decorative span { display: flex; flex-direction: column; gap: 3px; }
|
|
522
|
+
.lb-alt-decorative strong { font: 500 13px/1.3 var(--ak-font); }
|
|
523
|
+
.lb-alt-decorative small, .lb-alt-guidance { color: #adada8; font: 400 11px/1.45 var(--ak-font); }
|
|
524
|
+
.lb-alt-decorative code { color: #fff; }
|
|
525
|
+
.lb-alt-fields { display: flex; flex-direction: column; gap: 11px; }
|
|
526
|
+
.lb-alt-fields label { display: flex; flex-direction: column; gap: 6px; }
|
|
527
|
+
.lb-alt-fields label > span { display: flex; justify-content: space-between; gap: 12px; font: 500 12px/1.2 var(--ak-font); }
|
|
528
|
+
.lb-alt-fields label > span small { color: #8f8f8b; font-weight: 400; }
|
|
529
|
+
.lb-alt-fields textarea {
|
|
530
|
+
width: 100%; resize: vertical; box-sizing: border-box; padding: 9px 10px;
|
|
531
|
+
border: 1px solid #555; border-radius: 6px; background: #111; color: #fff;
|
|
532
|
+
font: 400 13px/1.45 var(--ak-font);
|
|
533
|
+
}
|
|
534
|
+
.lb-alt-fields textarea:focus { outline: 2px solid var(--ak-accent); outline-offset: 1px; }
|
|
535
|
+
.lb-alt-guidance, .lb-alt-error, .lb-alt-saved { margin: 0; }
|
|
536
|
+
.lb-alt-error { color: #ff9b9b; font: 500 11px/1.4 var(--ak-font); }
|
|
537
|
+
.lb-alt-saved { color: #b9e6bd; font: 500 11px/1.4 var(--ak-font); }
|
|
538
|
+
.lb-alt footer { display: flex; justify-content: flex-end; }
|
|
497
539
|
|
|
498
540
|
/* ---- Media: il dialogo di caricamento ------------------------------------------------- */
|
|
499
541
|
.up { position: fixed; inset: 0; z-index: 70; background: rgba(0, 0, 0, .42); display: grid; place-items: center; padding: 5vh 5vw; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardinali-studio/adminkit",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Motore riusabile di un pannello di amministrazione per siti Hugo su Cloudflare Pages: codec del front matter guidato dallo schema, form React, libreria media su R2. NIENTE di specifico di un sito qui dentro.",
|
|
6
6
|
"license": "UNLICENSED",
|