@cancia/toolbar 0.0.1
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/LICENSE +21 -0
- package/dist/cancia.d.ts +50 -0
- package/dist/cancia.iife.js +605 -0
- package/dist/cancia.js +2629 -0
- package/dist/cancia.js.map +1 -0
- package/package.json +34 -0
package/dist/cancia.js
ADDED
|
@@ -0,0 +1,2629 @@
|
|
|
1
|
+
// src/state.ts
|
|
2
|
+
var state = {
|
|
3
|
+
config: null,
|
|
4
|
+
cmsData: {},
|
|
5
|
+
pending: /* @__PURE__ */ new Map(),
|
|
6
|
+
activeLang: "",
|
|
7
|
+
editMode: false,
|
|
8
|
+
/** Auth token from sessionStorage — set by toolbar/index.ts after session validation */
|
|
9
|
+
sessionToken: "",
|
|
10
|
+
/** Called by toolbar logout button — wired up by index.ts to avoid circular deps */
|
|
11
|
+
onLogout: null,
|
|
12
|
+
/** List schemas, keyed by name. Loaded once on toolbar init. */
|
|
13
|
+
schemas: {},
|
|
14
|
+
/** Currently-selected locale for list-panel + modal. Defaults to activeLang on open. */
|
|
15
|
+
activeListLocale: ""
|
|
16
|
+
};
|
|
17
|
+
function pendingKey(key, lang) {
|
|
18
|
+
return `${key}.${lang}`;
|
|
19
|
+
}
|
|
20
|
+
function getValue(key, lang) {
|
|
21
|
+
const full = `${key}.${lang}`;
|
|
22
|
+
const p = state.pending.get(full);
|
|
23
|
+
if (p) return p.value;
|
|
24
|
+
if (state.cmsData[full] !== void 0) return state.cmsData[full];
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
function setPending(key, lang, value) {
|
|
28
|
+
const full = pendingKey(key, lang);
|
|
29
|
+
state.pending.set(full, { key, lang, value });
|
|
30
|
+
}
|
|
31
|
+
function applyOverlay() {
|
|
32
|
+
document.querySelectorAll("[data-cms]").forEach((el) => {
|
|
33
|
+
if (el.dataset.cmsList !== void 0) return;
|
|
34
|
+
const key = el.dataset.cms;
|
|
35
|
+
if (!key) return;
|
|
36
|
+
const savedValue = state.cmsData[`${key}.${state.activeLang}`];
|
|
37
|
+
if (savedValue === void 0) return;
|
|
38
|
+
if (el.tagName === "IMG") {
|
|
39
|
+
el.src = savedValue;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (el.childElementCount > 0) {
|
|
43
|
+
console.warn(
|
|
44
|
+
`Cancia: skipping overlay for "${key}" \u2014 element has child elements (textContent would destroy them).`
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
el.textContent = savedValue;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function revertPending() {
|
|
52
|
+
for (const [fullKey, { key, lang }] of state.pending) {
|
|
53
|
+
const savedValue = state.cmsData[fullKey] ?? "";
|
|
54
|
+
document.querySelectorAll(`[data-cms="${key}"]`).forEach((el) => {
|
|
55
|
+
if (el.tagName === "IMG") {
|
|
56
|
+
el.src = savedValue;
|
|
57
|
+
} else {
|
|
58
|
+
el.textContent = savedValue;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
state.pending.clear();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/api.ts
|
|
66
|
+
function headers() {
|
|
67
|
+
const h = { "Content-Type": "application/json" };
|
|
68
|
+
if (state.sessionToken) h["Authorization"] = `Bearer ${state.sessionToken}`;
|
|
69
|
+
return h;
|
|
70
|
+
}
|
|
71
|
+
async function fetchContent() {
|
|
72
|
+
const { apiUrl, site } = state.config;
|
|
73
|
+
const res = await fetch(`${apiUrl}/api/cancia/content?site=${encodeURIComponent(site)}`, {
|
|
74
|
+
headers: headers()
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) throw new Error(`Cancia: failed to fetch content (${res.status})`);
|
|
77
|
+
return res.json();
|
|
78
|
+
}
|
|
79
|
+
async function saveEntry(key, lang, value) {
|
|
80
|
+
const { apiUrl, site } = state.config;
|
|
81
|
+
const res = await fetch(`${apiUrl}/api/cancia/save`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: headers(),
|
|
84
|
+
body: JSON.stringify({ key, lang, value, site })
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) throw new Error(`Cancia: failed to save (${res.status})`);
|
|
87
|
+
}
|
|
88
|
+
function isAuthError(err) {
|
|
89
|
+
return err instanceof Error && err.message.includes("(401)");
|
|
90
|
+
}
|
|
91
|
+
function uploadImage(file, onProgress) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const { apiUrl, site } = state.config;
|
|
94
|
+
const form = new FormData();
|
|
95
|
+
form.append("file", file);
|
|
96
|
+
form.append("site", site);
|
|
97
|
+
const xhr = new XMLHttpRequest();
|
|
98
|
+
xhr.open("POST", `${apiUrl}/api/cancia/upload`);
|
|
99
|
+
if (state.sessionToken) xhr.setRequestHeader("Authorization", `Bearer ${state.sessionToken}`);
|
|
100
|
+
xhr.upload.addEventListener("progress", (e) => {
|
|
101
|
+
if (e.lengthComputable) onProgress?.(Math.round(e.loaded / e.total * 100));
|
|
102
|
+
});
|
|
103
|
+
xhr.addEventListener("load", () => {
|
|
104
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
105
|
+
try {
|
|
106
|
+
resolve(JSON.parse(xhr.responseText).url);
|
|
107
|
+
} catch {
|
|
108
|
+
reject(new Error("Cancia: invalid upload response"));
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
reject(new Error(`Cancia: failed to upload image (${xhr.status})`));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
xhr.addEventListener("error", () => reject(new Error("Cancia: upload network error")));
|
|
115
|
+
xhr.send(form);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async function triggerPublish() {
|
|
119
|
+
const { apiUrl, site } = state.config;
|
|
120
|
+
const res = await fetch(`${apiUrl}/api/cancia/publish`, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: headers(),
|
|
123
|
+
body: JSON.stringify({ site })
|
|
124
|
+
});
|
|
125
|
+
if (!res.ok) throw new Error(`Cancia: publish failed (${res.status})`);
|
|
126
|
+
}
|
|
127
|
+
async function fetchSchemas() {
|
|
128
|
+
const { apiUrl } = state.config;
|
|
129
|
+
const res = await fetch(`${apiUrl}/api/cancia/schemas`, { headers: headers() });
|
|
130
|
+
if (!res.ok) throw new Error(`Cancia: failed to fetch schemas (${res.status})`);
|
|
131
|
+
const body = await res.json();
|
|
132
|
+
return body.schemas;
|
|
133
|
+
}
|
|
134
|
+
function localeParam(locale) {
|
|
135
|
+
return locale ? `&locale=${encodeURIComponent(locale)}` : "";
|
|
136
|
+
}
|
|
137
|
+
async function fetchList(listName, locale) {
|
|
138
|
+
const { apiUrl, site } = state.config;
|
|
139
|
+
const res = await fetch(
|
|
140
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}?site=${encodeURIComponent(site)}${localeParam(locale)}`,
|
|
141
|
+
{ headers: headers() }
|
|
142
|
+
);
|
|
143
|
+
if (!res.ok) throw new Error(`Cancia: failed to fetch list "${listName}" (${res.status})`);
|
|
144
|
+
const body = await res.json();
|
|
145
|
+
return body.entries;
|
|
146
|
+
}
|
|
147
|
+
async function fetchTranslations(listName) {
|
|
148
|
+
const { apiUrl, site } = state.config;
|
|
149
|
+
const res = await fetch(
|
|
150
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/_translations?site=${encodeURIComponent(site)}`,
|
|
151
|
+
{ headers: headers() }
|
|
152
|
+
);
|
|
153
|
+
if (!res.ok) throw new Error(`Cancia: failed to fetch translations (${res.status})`);
|
|
154
|
+
const body = await res.json();
|
|
155
|
+
return body.translations;
|
|
156
|
+
}
|
|
157
|
+
async function createListEntry(listName, data, locale, id) {
|
|
158
|
+
const { apiUrl, site } = state.config;
|
|
159
|
+
const res = await fetch(
|
|
160
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}?site=${encodeURIComponent(site)}${localeParam(locale)}`,
|
|
161
|
+
{ method: "POST", headers: headers(), body: JSON.stringify({ data, id }) }
|
|
162
|
+
);
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
const err = await res.json().catch(() => ({}));
|
|
165
|
+
throw new Error(err.error ?? `Cancia: failed to create entry (${res.status})`);
|
|
166
|
+
}
|
|
167
|
+
const body = await res.json();
|
|
168
|
+
return body.entry;
|
|
169
|
+
}
|
|
170
|
+
async function updateListEntry(listName, id, data, rev, locale) {
|
|
171
|
+
const { apiUrl, site } = state.config;
|
|
172
|
+
const res = await fetch(
|
|
173
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}`,
|
|
174
|
+
{ method: "PATCH", headers: headers(), body: JSON.stringify({ data, _rev: rev }) }
|
|
175
|
+
);
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const err = await res.json().catch(() => ({}));
|
|
178
|
+
const message = err.error ?? `Cancia: failed to update entry (${res.status})`;
|
|
179
|
+
throw Object.assign(new Error(message), { code: err.code });
|
|
180
|
+
}
|
|
181
|
+
const body = await res.json();
|
|
182
|
+
return body.entry;
|
|
183
|
+
}
|
|
184
|
+
async function deleteListEntry(listName, id, locale) {
|
|
185
|
+
const { apiUrl, site } = state.config;
|
|
186
|
+
const res = await fetch(
|
|
187
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}`,
|
|
188
|
+
{ method: "DELETE", headers: headers() }
|
|
189
|
+
);
|
|
190
|
+
if (!res.ok) throw new Error(`Cancia: failed to delete entry (${res.status})`);
|
|
191
|
+
}
|
|
192
|
+
async function flushPending() {
|
|
193
|
+
const entries = Array.from(state.pending.values());
|
|
194
|
+
const results = await Promise.allSettled(
|
|
195
|
+
entries.map(({ key, lang, value }) => saveEntry(key, lang, value))
|
|
196
|
+
);
|
|
197
|
+
results.forEach((result, i) => {
|
|
198
|
+
if (result.status === "fulfilled") {
|
|
199
|
+
const { key, lang } = entries[i];
|
|
200
|
+
state.pending.delete(`${key}.${lang}`);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
204
|
+
if (failed > 0) throw new Error(`Cancia: ${failed} save(s) failed`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/highlight.ts
|
|
208
|
+
var CMS_SELECTOR = "[data-cms], [data-cms-list]";
|
|
209
|
+
var onSelect = null;
|
|
210
|
+
var currentHighlighted = null;
|
|
211
|
+
var cleanupFns = [];
|
|
212
|
+
var scrollRAF = null;
|
|
213
|
+
var overlayEl = null;
|
|
214
|
+
var tooltipEl = null;
|
|
215
|
+
var styleInjected = false;
|
|
216
|
+
function accent() {
|
|
217
|
+
return state.config?.accentColor ?? "#6366f1";
|
|
218
|
+
}
|
|
219
|
+
function fieldType(el) {
|
|
220
|
+
if (el.dataset.cmsType === "image") return "image";
|
|
221
|
+
return el.tagName === "IMG" ? "image" : "text";
|
|
222
|
+
}
|
|
223
|
+
function elementMode(el) {
|
|
224
|
+
return el.dataset.cmsList ? "list" : "field";
|
|
225
|
+
}
|
|
226
|
+
function injectStyles() {
|
|
227
|
+
if (styleInjected) return;
|
|
228
|
+
styleInjected = true;
|
|
229
|
+
const s = document.createElement("style");
|
|
230
|
+
s.textContent = `
|
|
231
|
+
@keyframes cancia-highlight-in {
|
|
232
|
+
from { opacity: 0; transform: scale(0.98); }
|
|
233
|
+
to { opacity: 1; transform: scale(1); }
|
|
234
|
+
}
|
|
235
|
+
@keyframes cancia-tooltip-in {
|
|
236
|
+
from { opacity: 0; transform: scale(0.95) translateY(3px); }
|
|
237
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
240
|
+
document.head.appendChild(s);
|
|
241
|
+
}
|
|
242
|
+
function getOrCreateOverlay() {
|
|
243
|
+
if (!overlayEl) {
|
|
244
|
+
overlayEl = document.createElement("div");
|
|
245
|
+
overlayEl.dataset.canciaOverlay = "1";
|
|
246
|
+
overlayEl.style.cssText = `
|
|
247
|
+
position: fixed;
|
|
248
|
+
pointer-events: none !important;
|
|
249
|
+
box-sizing: border-box;
|
|
250
|
+
border-radius: 5px;
|
|
251
|
+
z-index: 2147483644;
|
|
252
|
+
will-change: top, left, width, height, opacity;
|
|
253
|
+
transition: top 0.08s cubic-bezier(0.16,1,0.3,1), left 0.08s cubic-bezier(0.16,1,0.3,1),
|
|
254
|
+
width 0.08s cubic-bezier(0.16,1,0.3,1), height 0.08s cubic-bezier(0.16,1,0.3,1);
|
|
255
|
+
`;
|
|
256
|
+
document.body.appendChild(overlayEl);
|
|
257
|
+
}
|
|
258
|
+
return overlayEl;
|
|
259
|
+
}
|
|
260
|
+
function getOrCreateTooltip() {
|
|
261
|
+
if (!tooltipEl) {
|
|
262
|
+
tooltipEl = document.createElement("div");
|
|
263
|
+
tooltipEl.dataset.canciaTooltip = "1";
|
|
264
|
+
tooltipEl.style.cssText = `
|
|
265
|
+
position: fixed;
|
|
266
|
+
pointer-events: none !important;
|
|
267
|
+
z-index: 2147483645;
|
|
268
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
269
|
+
font-size: 11px;
|
|
270
|
+
font-weight: 500;
|
|
271
|
+
letter-spacing: 0.02em;
|
|
272
|
+
color: #fff;
|
|
273
|
+
background: rgba(10,10,12,0.92);
|
|
274
|
+
backdrop-filter: blur(8px);
|
|
275
|
+
-webkit-backdrop-filter: blur(8px);
|
|
276
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
277
|
+
padding: 4px 8px;
|
|
278
|
+
border-radius: 6px;
|
|
279
|
+
white-space: nowrap;
|
|
280
|
+
max-width: 260px;
|
|
281
|
+
overflow: hidden;
|
|
282
|
+
text-overflow: ellipsis;
|
|
283
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
284
|
+
`;
|
|
285
|
+
document.body.appendChild(tooltipEl);
|
|
286
|
+
}
|
|
287
|
+
return tooltipEl;
|
|
288
|
+
}
|
|
289
|
+
var lastOverlayEl = null;
|
|
290
|
+
function listAccent(hex) {
|
|
291
|
+
if (!/^#[0-9a-f]{6}$/i.test(hex)) return "#059669";
|
|
292
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
293
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
294
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
295
|
+
const shifted = [g, b, r].map((c) => c.toString(16).padStart(2, "0")).join("");
|
|
296
|
+
return `#${shifted}`;
|
|
297
|
+
}
|
|
298
|
+
var FIELD_ICON = `<svg width="10" height="10" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
|
299
|
+
<path d="M2 4h10M2 7h7M2 10h5"/>
|
|
300
|
+
</svg>`;
|
|
301
|
+
var IMAGE_ICON = `<svg width="10" height="10" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
302
|
+
<rect x="1" y="1" width="12" height="12" rx="2"/>
|
|
303
|
+
<circle cx="4.5" cy="4.5" r="1.2"/>
|
|
304
|
+
<path d="M1 9.5l3.5-3 2.5 2.5 2-1.5 3 3.5"/>
|
|
305
|
+
</svg>`;
|
|
306
|
+
var LIST_ICON = `<svg width="10" height="10" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
307
|
+
<rect x="1" y="2" width="3" height="3" rx="0.5"/>
|
|
308
|
+
<rect x="1" y="7.5" width="3" height="3" rx="0.5"/>
|
|
309
|
+
<path d="M6 3.5h7M6 9h7"/>
|
|
310
|
+
</svg>`;
|
|
311
|
+
function positionOverlay(el, animate = false) {
|
|
312
|
+
const rect = el.getBoundingClientRect();
|
|
313
|
+
const mode = elementMode(el);
|
|
314
|
+
const a = mode === "list" ? listAccent(accent()) : accent();
|
|
315
|
+
const overlay = getOrCreateOverlay();
|
|
316
|
+
const tooltip = getOrCreateTooltip();
|
|
317
|
+
const padding = 3;
|
|
318
|
+
overlay.style.top = `${rect.top - padding}px`;
|
|
319
|
+
overlay.style.left = `${rect.left - padding}px`;
|
|
320
|
+
overlay.style.width = `${rect.width + padding * 2}px`;
|
|
321
|
+
overlay.style.height = `${rect.height + padding * 2}px`;
|
|
322
|
+
if (lastOverlayEl !== el) {
|
|
323
|
+
lastOverlayEl = el;
|
|
324
|
+
overlay.style.border = `2px ${mode === "list" ? "dashed" : "solid"} ${a}`;
|
|
325
|
+
overlay.style.background = `${a}12`;
|
|
326
|
+
let badgeIcon;
|
|
327
|
+
let badgeLabel;
|
|
328
|
+
let tooltipText;
|
|
329
|
+
if (mode === "list") {
|
|
330
|
+
badgeIcon = LIST_ICON;
|
|
331
|
+
badgeLabel = "list";
|
|
332
|
+
tooltipText = `list: ${el.dataset.cmsList}`;
|
|
333
|
+
} else {
|
|
334
|
+
const type = fieldType(el);
|
|
335
|
+
badgeIcon = type === "image" ? IMAGE_ICON : FIELD_ICON;
|
|
336
|
+
badgeLabel = type;
|
|
337
|
+
tooltipText = el.dataset.cms ?? "";
|
|
338
|
+
}
|
|
339
|
+
overlay.innerHTML = `
|
|
340
|
+
<div style="
|
|
341
|
+
position: absolute; top: -1px; left: -1px;
|
|
342
|
+
background: ${a}; color: #fff;
|
|
343
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
344
|
+
font-size: 10px; font-weight: 600; letter-spacing: 0.04em;
|
|
345
|
+
padding: 2px 6px; border-radius: 3px 0 4px 0;
|
|
346
|
+
display: flex; align-items: center; gap: 4px;
|
|
347
|
+
line-height: 1;
|
|
348
|
+
">
|
|
349
|
+
${badgeIcon}
|
|
350
|
+
${badgeLabel}
|
|
351
|
+
</div>
|
|
352
|
+
`;
|
|
353
|
+
tooltip.textContent = tooltipText;
|
|
354
|
+
}
|
|
355
|
+
overlay.style.display = "block";
|
|
356
|
+
if (animate) overlay.style.animation = "cancia-highlight-in 0.12s ease-out forwards";
|
|
357
|
+
tooltip.style.display = "block";
|
|
358
|
+
if (animate) tooltip.style.animation = "cancia-tooltip-in 0.1s ease-out forwards";
|
|
359
|
+
const tooltipMargin = 8;
|
|
360
|
+
const tooltipH = 26;
|
|
361
|
+
if (rect.top - tooltipH - tooltipMargin > 0) {
|
|
362
|
+
tooltip.style.top = `${rect.top - tooltipH - tooltipMargin + padding}px`;
|
|
363
|
+
tooltip.style.left = `${rect.left - padding}px`;
|
|
364
|
+
} else {
|
|
365
|
+
tooltip.style.top = `${rect.bottom + tooltipMargin - padding}px`;
|
|
366
|
+
tooltip.style.left = `${rect.left - padding}px`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function hideOverlay() {
|
|
370
|
+
lastOverlayEl = null;
|
|
371
|
+
if (overlayEl) {
|
|
372
|
+
overlayEl.style.display = "none";
|
|
373
|
+
overlayEl.style.animation = "none";
|
|
374
|
+
}
|
|
375
|
+
if (tooltipEl) {
|
|
376
|
+
tooltipEl.style.display = "none";
|
|
377
|
+
tooltipEl.style.animation = "none";
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function handleScroll() {
|
|
381
|
+
if (scrollRAF !== null) return;
|
|
382
|
+
scrollRAF = requestAnimationFrame(() => {
|
|
383
|
+
scrollRAF = null;
|
|
384
|
+
if (currentHighlighted) {
|
|
385
|
+
positionOverlay(currentHighlighted);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function handleMouseOver(e) {
|
|
390
|
+
const target = e.target.closest(CMS_SELECTOR);
|
|
391
|
+
if (!target) return;
|
|
392
|
+
currentHighlighted = target;
|
|
393
|
+
target.style.cursor = "pointer";
|
|
394
|
+
positionOverlay(target, true);
|
|
395
|
+
}
|
|
396
|
+
function handleMouseOut(e) {
|
|
397
|
+
const target = e.target.closest(CMS_SELECTOR);
|
|
398
|
+
if (!target) return;
|
|
399
|
+
const related = e.relatedTarget;
|
|
400
|
+
if (related && target.contains(related)) return;
|
|
401
|
+
target.style.cursor = "";
|
|
402
|
+
if (currentHighlighted === target) {
|
|
403
|
+
currentHighlighted = null;
|
|
404
|
+
hideOverlay();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function handleClick(e) {
|
|
408
|
+
const target = e.target.closest(CMS_SELECTOR);
|
|
409
|
+
if (!target) return;
|
|
410
|
+
e.preventDefault();
|
|
411
|
+
e.stopPropagation();
|
|
412
|
+
hideOverlay();
|
|
413
|
+
if (elementMode(target) === "list") {
|
|
414
|
+
const listName = target.dataset.cmsList;
|
|
415
|
+
onSelect?.({ kind: "list", el: target, listName });
|
|
416
|
+
} else {
|
|
417
|
+
const key = target.dataset.cms;
|
|
418
|
+
onSelect?.({ kind: "field", el: target, key, fieldType: fieldType(target) });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function attachHighlight(selectCallback) {
|
|
422
|
+
injectStyles();
|
|
423
|
+
onSelect = selectCallback;
|
|
424
|
+
document.addEventListener("mouseover", handleMouseOver, true);
|
|
425
|
+
document.addEventListener("mouseout", handleMouseOut, true);
|
|
426
|
+
document.addEventListener("click", handleClick, true);
|
|
427
|
+
window.addEventListener("scroll", handleScroll, { passive: true, capture: true });
|
|
428
|
+
cleanupFns = [
|
|
429
|
+
() => document.removeEventListener("mouseover", handleMouseOver, true),
|
|
430
|
+
() => document.removeEventListener("mouseout", handleMouseOut, true),
|
|
431
|
+
() => document.removeEventListener("click", handleClick, true),
|
|
432
|
+
() => window.removeEventListener("scroll", handleScroll, true)
|
|
433
|
+
];
|
|
434
|
+
}
|
|
435
|
+
function detachHighlight() {
|
|
436
|
+
if (currentHighlighted) {
|
|
437
|
+
currentHighlighted.style.cursor = "";
|
|
438
|
+
currentHighlighted = null;
|
|
439
|
+
}
|
|
440
|
+
if (scrollRAF !== null) {
|
|
441
|
+
cancelAnimationFrame(scrollRAF);
|
|
442
|
+
scrollRAF = null;
|
|
443
|
+
}
|
|
444
|
+
hideOverlay();
|
|
445
|
+
overlayEl?.remove();
|
|
446
|
+
overlayEl = null;
|
|
447
|
+
tooltipEl?.remove();
|
|
448
|
+
tooltipEl = null;
|
|
449
|
+
cleanupFns.forEach((fn) => fn());
|
|
450
|
+
cleanupFns = [];
|
|
451
|
+
onSelect = null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/events.ts
|
|
455
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
456
|
+
function onPendingChange(cb) {
|
|
457
|
+
if (cb) {
|
|
458
|
+
listeners.add(cb);
|
|
459
|
+
return () => listeners.delete(cb);
|
|
460
|
+
}
|
|
461
|
+
listeners.forEach((fn) => fn());
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/popup.ts
|
|
465
|
+
var popupEl = null;
|
|
466
|
+
var outsideListener = null;
|
|
467
|
+
var keyListener = null;
|
|
468
|
+
var dragover = false;
|
|
469
|
+
var inputHandlers = /* @__PURE__ */ new WeakMap();
|
|
470
|
+
function accent2() {
|
|
471
|
+
return state.config?.accentColor ?? "#6366f1";
|
|
472
|
+
}
|
|
473
|
+
function getPopupPosition(anchor) {
|
|
474
|
+
const rect = anchor.getBoundingClientRect();
|
|
475
|
+
const scrollY = window.scrollY;
|
|
476
|
+
const scrollX = window.scrollX;
|
|
477
|
+
const popupW = 320;
|
|
478
|
+
const popupH = 260;
|
|
479
|
+
const margin = 10;
|
|
480
|
+
let left = rect.left + scrollX;
|
|
481
|
+
let top = rect.bottom + scrollY + margin;
|
|
482
|
+
let origin = "top left";
|
|
483
|
+
if (rect.bottom + popupH + margin > window.innerHeight) {
|
|
484
|
+
top = rect.top + scrollY - popupH - margin;
|
|
485
|
+
origin = "bottom left";
|
|
486
|
+
}
|
|
487
|
+
if (left + popupW > window.innerWidth + scrollX) {
|
|
488
|
+
left = window.innerWidth + scrollX - popupW - margin;
|
|
489
|
+
origin = origin.replace("left", "right");
|
|
490
|
+
}
|
|
491
|
+
if (left < scrollX + margin) left = scrollX + margin;
|
|
492
|
+
return { top, left, origin };
|
|
493
|
+
}
|
|
494
|
+
function buildHeader(key, onClose) {
|
|
495
|
+
const header = document.createElement("div");
|
|
496
|
+
header.style.cssText = `
|
|
497
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
498
|
+
margin-bottom: 14px;
|
|
499
|
+
`;
|
|
500
|
+
const titleWrap = document.createElement("div");
|
|
501
|
+
titleWrap.style.cssText = `display: flex; flex-direction: column; gap: 1px; min-width: 0;`;
|
|
502
|
+
const titleEl = document.createElement("span");
|
|
503
|
+
const keyParts = key.split(".");
|
|
504
|
+
titleEl.textContent = keyParts[keyParts.length - 1].replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
505
|
+
titleEl.style.cssText = `
|
|
506
|
+
font-size: 13px; font-weight: 600;
|
|
507
|
+
color: rgba(255,255,255,0.9);
|
|
508
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
509
|
+
`;
|
|
510
|
+
const keyEl = document.createElement("span");
|
|
511
|
+
keyEl.textContent = key;
|
|
512
|
+
keyEl.style.cssText = `
|
|
513
|
+
font-size: 10px; font-family: "SF Mono", "Fira Code", ui-monospace, monospace;
|
|
514
|
+
color: rgba(255,255,255,0.22); letter-spacing: 0.03em;
|
|
515
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
516
|
+
`;
|
|
517
|
+
titleWrap.appendChild(titleEl);
|
|
518
|
+
titleWrap.appendChild(keyEl);
|
|
519
|
+
header.appendChild(titleWrap);
|
|
520
|
+
header.appendChild(makeCloseButton(onClose));
|
|
521
|
+
return header;
|
|
522
|
+
}
|
|
523
|
+
function buildTextPopup(key, anchorEl, onClose) {
|
|
524
|
+
const langs = state.config?.languages ?? ["en"];
|
|
525
|
+
let activeLang = state.activeLang || langs[0];
|
|
526
|
+
const wrap = document.createElement("div");
|
|
527
|
+
wrap.dataset.canciaPopup = "1";
|
|
528
|
+
applyPopupStyles(wrap);
|
|
529
|
+
wrap.appendChild(buildHeader(key, onClose));
|
|
530
|
+
if (langs.length > 1) {
|
|
531
|
+
const tabs = document.createElement("div");
|
|
532
|
+
tabs.style.cssText = `display: flex; gap: 0; margin-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.06);`;
|
|
533
|
+
const renderTabs2 = () => {
|
|
534
|
+
tabs.innerHTML = "";
|
|
535
|
+
langs.forEach((lang) => {
|
|
536
|
+
const tab = document.createElement("button");
|
|
537
|
+
tab.textContent = lang.toUpperCase();
|
|
538
|
+
const isActive = lang === activeLang;
|
|
539
|
+
tab.style.cssText = `
|
|
540
|
+
padding: 5px 10px 6px; border: none; border-bottom: 2px solid;
|
|
541
|
+
margin-bottom: -1px;
|
|
542
|
+
font-size: 11px; font-weight: 600; cursor: pointer; letter-spacing: 0.06em;
|
|
543
|
+
background: transparent;
|
|
544
|
+
border-bottom-color: ${isActive ? accent2() : "transparent"};
|
|
545
|
+
color: ${isActive ? "#fff" : "rgba(255,255,255,0.3)"};
|
|
546
|
+
transition: color 0.15s, border-color 0.15s;
|
|
547
|
+
`;
|
|
548
|
+
tab.addEventListener("mouseenter", () => {
|
|
549
|
+
if (!isActive) tab.style.color = "rgba(255,255,255,0.6)";
|
|
550
|
+
});
|
|
551
|
+
tab.addEventListener("mouseleave", () => {
|
|
552
|
+
if (!isActive) tab.style.color = "rgba(255,255,255,0.3)";
|
|
553
|
+
});
|
|
554
|
+
tab.addEventListener("click", () => {
|
|
555
|
+
const current = wrap.querySelector("textarea");
|
|
556
|
+
if (current) {
|
|
557
|
+
const existing = getValue(key, activeLang);
|
|
558
|
+
const fallback = anchorEl.textContent?.trim() || "";
|
|
559
|
+
if (current.value !== (existing || fallback)) {
|
|
560
|
+
setPending(key, activeLang, current.value);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
activeLang = lang;
|
|
564
|
+
state.activeLang = lang;
|
|
565
|
+
applyOverlay();
|
|
566
|
+
renderTabs2();
|
|
567
|
+
renderTextarea();
|
|
568
|
+
});
|
|
569
|
+
tabs.appendChild(tab);
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
renderTabs2();
|
|
573
|
+
wrap.appendChild(tabs);
|
|
574
|
+
}
|
|
575
|
+
let textarea;
|
|
576
|
+
const attachInputHandler = () => {
|
|
577
|
+
const prev = inputHandlers.get(textarea);
|
|
578
|
+
if (prev) textarea.removeEventListener("input", prev);
|
|
579
|
+
const handler = () => {
|
|
580
|
+
setPending(key, activeLang, textarea.value);
|
|
581
|
+
onPendingChange();
|
|
582
|
+
anchorEl.textContent = textarea.value;
|
|
583
|
+
};
|
|
584
|
+
inputHandlers.set(textarea, handler);
|
|
585
|
+
textarea.addEventListener("input", handler);
|
|
586
|
+
};
|
|
587
|
+
const renderTextarea = (isInit = false) => {
|
|
588
|
+
if (!isInit && textarea) {
|
|
589
|
+
textarea.value = getValue(key, activeLang) || anchorEl.textContent?.trim() || "";
|
|
590
|
+
textarea.style.borderColor = `${accent2()}66`;
|
|
591
|
+
textarea.style.background = "rgba(255,255,255,0.05)";
|
|
592
|
+
attachInputHandler();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const footerEl = wrap.querySelector("[data-cancia-footer]");
|
|
596
|
+
textarea = document.createElement("textarea");
|
|
597
|
+
textarea.value = getValue(key, activeLang) || anchorEl.textContent?.trim() || "";
|
|
598
|
+
textarea.rows = 4;
|
|
599
|
+
textarea.placeholder = "Enter text\u2026";
|
|
600
|
+
textarea.style.cssText = `
|
|
601
|
+
width: 100%; box-sizing: border-box;
|
|
602
|
+
background: rgba(255,255,255,0.03); color: rgba(255,255,255,0.9);
|
|
603
|
+
border: 1px solid rgba(255,255,255,0.07); border-radius: 10px;
|
|
604
|
+
padding: 10px 12px;
|
|
605
|
+
font-size: 13px; font-family: inherit; resize: none; outline: none;
|
|
606
|
+
transition: border-color 0.18s, background 0.18s;
|
|
607
|
+
line-height: 1.55;
|
|
608
|
+
caret-color: ${accent2()};
|
|
609
|
+
`;
|
|
610
|
+
textarea.addEventListener("focus", () => {
|
|
611
|
+
textarea.style.borderColor = `${accent2()}66`;
|
|
612
|
+
textarea.style.background = "rgba(255,255,255,0.05)";
|
|
613
|
+
});
|
|
614
|
+
textarea.addEventListener("blur", () => {
|
|
615
|
+
textarea.style.borderColor = "rgba(255,255,255,0.07)";
|
|
616
|
+
textarea.style.background = "rgba(255,255,255,0.03)";
|
|
617
|
+
});
|
|
618
|
+
attachInputHandler();
|
|
619
|
+
if (footerEl) {
|
|
620
|
+
wrap.insertBefore(textarea, footerEl);
|
|
621
|
+
} else {
|
|
622
|
+
wrap.appendChild(textarea);
|
|
623
|
+
}
|
|
624
|
+
setTimeout(() => textarea.focus(), 80);
|
|
625
|
+
};
|
|
626
|
+
renderTextarea(true);
|
|
627
|
+
const footer = document.createElement("div");
|
|
628
|
+
footer.dataset.canciaFooter = "1";
|
|
629
|
+
footer.style.cssText = `display: flex; justify-content: flex-end; margin-top: 10px;`;
|
|
630
|
+
const saveBtn = makePrimaryButton("Save", accent2());
|
|
631
|
+
saveBtn.dataset.canciaSave = "1";
|
|
632
|
+
saveBtn.title = "Save (\u2318S)";
|
|
633
|
+
saveBtn.addEventListener("click", () => {
|
|
634
|
+
const existing = getValue(key, activeLang);
|
|
635
|
+
const fallback = anchorEl.textContent?.trim() || "";
|
|
636
|
+
if (textarea.value !== (existing || fallback)) {
|
|
637
|
+
setPending(key, activeLang, textarea.value);
|
|
638
|
+
}
|
|
639
|
+
onPendingChange();
|
|
640
|
+
onClose();
|
|
641
|
+
});
|
|
642
|
+
footer.appendChild(saveBtn);
|
|
643
|
+
wrap.appendChild(footer);
|
|
644
|
+
return wrap;
|
|
645
|
+
}
|
|
646
|
+
function buildImagePopup(key, anchorEl, onClose) {
|
|
647
|
+
const wrap = document.createElement("div");
|
|
648
|
+
wrap.dataset.canciaPopup = "1";
|
|
649
|
+
applyPopupStyles(wrap);
|
|
650
|
+
wrap.appendChild(buildHeader(key, onClose));
|
|
651
|
+
const currentSrc = anchorEl.tagName === "IMG" ? anchorEl.src : anchorEl.querySelector("img")?.src ?? "";
|
|
652
|
+
if (currentSrc && !currentSrc.startsWith("data:")) {
|
|
653
|
+
const previewWrap = document.createElement("div");
|
|
654
|
+
previewWrap.style.cssText = `
|
|
655
|
+
border-radius: 10px; overflow: hidden; margin-bottom: 10px;
|
|
656
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
657
|
+
position: relative; height: 100px;
|
|
658
|
+
`;
|
|
659
|
+
const previewImg = document.createElement("img");
|
|
660
|
+
previewImg.src = currentSrc;
|
|
661
|
+
previewImg.style.cssText = `width: 100%; height: 100%; object-fit: cover; display: block;`;
|
|
662
|
+
const previewLabel = document.createElement("div");
|
|
663
|
+
previewLabel.textContent = "Current";
|
|
664
|
+
previewLabel.style.cssText = `
|
|
665
|
+
position: absolute; bottom: 0; left: 0; right: 0;
|
|
666
|
+
font-size: 10px; color: rgba(255,255,255,0.45); letter-spacing: 0.04em;
|
|
667
|
+
padding: 16px 8px 6px;
|
|
668
|
+
background: linear-gradient(transparent, rgba(0,0,0,0.55));
|
|
669
|
+
`;
|
|
670
|
+
previewWrap.appendChild(previewImg);
|
|
671
|
+
previewWrap.appendChild(previewLabel);
|
|
672
|
+
wrap.appendChild(previewWrap);
|
|
673
|
+
}
|
|
674
|
+
const dropZone = document.createElement("label");
|
|
675
|
+
dropZone.style.cssText = `
|
|
676
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
677
|
+
gap: 8px;
|
|
678
|
+
border: 1.5px dashed rgba(255,255,255,0.1); border-radius: 10px;
|
|
679
|
+
padding: 24px 20px;
|
|
680
|
+
cursor: pointer;
|
|
681
|
+
transition: border-color 0.18s, background 0.18s;
|
|
682
|
+
background: rgba(255,255,255,0.015);
|
|
683
|
+
`;
|
|
684
|
+
const uploadIcon = document.createElement("div");
|
|
685
|
+
uploadIcon.style.cssText = `color: rgba(255,255,255,0.35); transition: color 0.18s;`;
|
|
686
|
+
uploadIcon.innerHTML = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
687
|
+
<path d="M12 15V3m0 0L8 7m4-4l4 4M2 17l.621 2.485A2 2 0 004.561 21h14.878a2 2 0 001.94-1.515L22 17"/>
|
|
688
|
+
</svg>`;
|
|
689
|
+
const dropText = document.createElement("div");
|
|
690
|
+
dropText.style.cssText = `text-align: center;`;
|
|
691
|
+
dropText.innerHTML = `
|
|
692
|
+
<div style="font-size:12px;font-weight:500;color:rgba(255,255,255,0.5);">Drop an image</div>
|
|
693
|
+
<div style="font-size:11px;color:rgba(255,255,255,0.25);margin-top:2px;">or click to browse</div>
|
|
694
|
+
`;
|
|
695
|
+
dropZone.appendChild(uploadIcon);
|
|
696
|
+
dropZone.appendChild(dropText);
|
|
697
|
+
const fileInput = document.createElement("input");
|
|
698
|
+
fileInput.type = "file";
|
|
699
|
+
fileInput.accept = "image/*";
|
|
700
|
+
fileInput.style.display = "none";
|
|
701
|
+
dropZone.appendChild(fileInput);
|
|
702
|
+
const statusWrap = document.createElement("div");
|
|
703
|
+
statusWrap.style.cssText = `margin-top: 8px; min-height: 18px;`;
|
|
704
|
+
const statusMsg = document.createElement("p");
|
|
705
|
+
statusMsg.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.35); margin: 0; text-align: center; transition: color 0.2s;`;
|
|
706
|
+
const progressBar = document.createElement("div");
|
|
707
|
+
progressBar.style.cssText = `
|
|
708
|
+
height: 2px; border-radius: 2px; background: rgba(255,255,255,0.05);
|
|
709
|
+
overflow: hidden; margin-top: 6px; display: none;
|
|
710
|
+
`;
|
|
711
|
+
const progressFill = document.createElement("div");
|
|
712
|
+
progressFill.style.cssText = `
|
|
713
|
+
height: 100%; border-radius: 2px; background: ${accent2()};
|
|
714
|
+
width: 0%; transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
|
715
|
+
`;
|
|
716
|
+
progressBar.appendChild(progressFill);
|
|
717
|
+
statusWrap.appendChild(statusMsg);
|
|
718
|
+
statusWrap.appendChild(progressBar);
|
|
719
|
+
const handleFile = async (file) => {
|
|
720
|
+
if (!file.type.startsWith("image/")) {
|
|
721
|
+
statusMsg.textContent = "Only image files are supported";
|
|
722
|
+
statusMsg.style.color = "#f87171";
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const maxMb = 10;
|
|
726
|
+
if (file.size > maxMb * 1024 * 1024) {
|
|
727
|
+
statusMsg.textContent = `File too large (max ${maxMb}MB)`;
|
|
728
|
+
statusMsg.style.color = "#f87171";
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
dropZone.style.borderColor = `${accent2()}55`;
|
|
732
|
+
dropZone.style.background = `${accent2()}0a`;
|
|
733
|
+
uploadIcon.style.color = accent2();
|
|
734
|
+
statusMsg.textContent = "Uploading\u2026";
|
|
735
|
+
statusMsg.style.color = "rgba(255,255,255,0.45)";
|
|
736
|
+
progressBar.style.display = "block";
|
|
737
|
+
progressFill.style.width = "0%";
|
|
738
|
+
try {
|
|
739
|
+
const url = await uploadImage(file, (percent) => {
|
|
740
|
+
progressFill.style.width = `${percent}%`;
|
|
741
|
+
});
|
|
742
|
+
progressFill.style.width = "100%";
|
|
743
|
+
setPending(key, state.activeLang, url);
|
|
744
|
+
onPendingChange();
|
|
745
|
+
if (anchorEl.tagName === "IMG") {
|
|
746
|
+
const img = anchorEl;
|
|
747
|
+
img.srcset = "";
|
|
748
|
+
img.src = url;
|
|
749
|
+
} else {
|
|
750
|
+
const img = document.createElement("img");
|
|
751
|
+
img.src = url;
|
|
752
|
+
img.alt = "";
|
|
753
|
+
img.style.cssText = "width:100%;height:100%;object-fit:cover;display:block;";
|
|
754
|
+
img.dataset.cms = key;
|
|
755
|
+
anchorEl.replaceWith(img);
|
|
756
|
+
}
|
|
757
|
+
setTimeout(() => {
|
|
758
|
+
statusMsg.textContent = "Done";
|
|
759
|
+
statusMsg.style.color = "#4ade80";
|
|
760
|
+
setTimeout(onClose, 600);
|
|
761
|
+
}, 200);
|
|
762
|
+
} catch {
|
|
763
|
+
progressBar.style.display = "none";
|
|
764
|
+
statusMsg.textContent = "Upload failed \u2014 try again";
|
|
765
|
+
statusMsg.style.color = "#f87171";
|
|
766
|
+
dropZone.style.borderColor = "rgba(255,255,255,0.1)";
|
|
767
|
+
dropZone.style.background = "rgba(255,255,255,0.015)";
|
|
768
|
+
uploadIcon.style.color = "rgba(255,255,255,0.35)";
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
fileInput.addEventListener("change", () => {
|
|
772
|
+
if (fileInput.files?.[0]) handleFile(fileInput.files[0]);
|
|
773
|
+
});
|
|
774
|
+
dropZone.addEventListener("dragover", (e) => {
|
|
775
|
+
e.preventDefault();
|
|
776
|
+
if (!dragover) {
|
|
777
|
+
dragover = true;
|
|
778
|
+
dropZone.style.borderColor = `${accent2()}88`;
|
|
779
|
+
dropZone.style.background = `${accent2()}0d`;
|
|
780
|
+
uploadIcon.style.color = accent2();
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
dropZone.addEventListener("dragleave", () => {
|
|
784
|
+
dragover = false;
|
|
785
|
+
dropZone.style.borderColor = "rgba(255,255,255,0.1)";
|
|
786
|
+
dropZone.style.background = "rgba(255,255,255,0.015)";
|
|
787
|
+
uploadIcon.style.color = "rgba(255,255,255,0.35)";
|
|
788
|
+
});
|
|
789
|
+
dropZone.addEventListener("drop", (e) => {
|
|
790
|
+
e.preventDefault();
|
|
791
|
+
dragover = false;
|
|
792
|
+
dropZone.style.borderColor = "rgba(255,255,255,0.1)";
|
|
793
|
+
dropZone.style.background = "rgba(255,255,255,0.015)";
|
|
794
|
+
const file = e.dataTransfer?.files[0];
|
|
795
|
+
if (file) handleFile(file);
|
|
796
|
+
});
|
|
797
|
+
dropZone.addEventListener("mouseenter", () => {
|
|
798
|
+
if (!dragover) {
|
|
799
|
+
dropZone.style.borderColor = "rgba(255,255,255,0.18)";
|
|
800
|
+
dropZone.style.background = "rgba(255,255,255,0.03)";
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
dropZone.addEventListener("mouseleave", () => {
|
|
804
|
+
if (!dragover) {
|
|
805
|
+
dropZone.style.borderColor = "rgba(255,255,255,0.1)";
|
|
806
|
+
dropZone.style.background = "rgba(255,255,255,0.015)";
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
wrap.appendChild(dropZone);
|
|
810
|
+
wrap.appendChild(statusWrap);
|
|
811
|
+
return wrap;
|
|
812
|
+
}
|
|
813
|
+
function makeCloseButton(onClose) {
|
|
814
|
+
const btn = document.createElement("button");
|
|
815
|
+
btn.style.cssText = `
|
|
816
|
+
display: flex; align-items: center; justify-content: center;
|
|
817
|
+
width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;
|
|
818
|
+
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06);
|
|
819
|
+
cursor: pointer; color: rgba(255,255,255,0.35); padding: 0;
|
|
820
|
+
transition: background 0.15s, color 0.15s;
|
|
821
|
+
`;
|
|
822
|
+
btn.innerHTML = `<svg width="9" height="9" viewBox="0 0 10 10" fill="none">
|
|
823
|
+
<path d="M1 1l8 8M9 1L1 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
824
|
+
</svg>`;
|
|
825
|
+
btn.addEventListener("mouseenter", () => {
|
|
826
|
+
btn.style.background = "rgba(255,255,255,0.08)";
|
|
827
|
+
btn.style.color = "rgba(255,255,255,0.75)";
|
|
828
|
+
});
|
|
829
|
+
btn.addEventListener("mouseleave", () => {
|
|
830
|
+
btn.style.background = "rgba(255,255,255,0.04)";
|
|
831
|
+
btn.style.color = "rgba(255,255,255,0.35)";
|
|
832
|
+
});
|
|
833
|
+
btn.addEventListener("click", onClose);
|
|
834
|
+
return btn;
|
|
835
|
+
}
|
|
836
|
+
function makePrimaryButton(label, color) {
|
|
837
|
+
const btn = document.createElement("button");
|
|
838
|
+
btn.textContent = label;
|
|
839
|
+
btn.style.cssText = `
|
|
840
|
+
padding: 7px 16px; border-radius: 8px; border: none; cursor: pointer;
|
|
841
|
+
background: #fff; color: #0c0c0e;
|
|
842
|
+
font-size: 12px; font-weight: 600; letter-spacing: 0.01em;
|
|
843
|
+
transition: opacity 0.15s, transform 0.1s cubic-bezier(0.2, 0, 0, 1);
|
|
844
|
+
`;
|
|
845
|
+
btn.addEventListener("mouseenter", () => btn.style.opacity = "0.88");
|
|
846
|
+
btn.addEventListener("mouseleave", () => btn.style.opacity = "1");
|
|
847
|
+
return btn;
|
|
848
|
+
}
|
|
849
|
+
function applyPopupStyles(el) {
|
|
850
|
+
el.style.cssText = `
|
|
851
|
+
position: absolute;
|
|
852
|
+
z-index: 2147483646;
|
|
853
|
+
width: 320px;
|
|
854
|
+
background: rgba(14, 14, 16, 0.97);
|
|
855
|
+
backdrop-filter: blur(24px) saturate(180%);
|
|
856
|
+
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
|
857
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
858
|
+
border-radius: 14px;
|
|
859
|
+
padding: 14px;
|
|
860
|
+
box-shadow: 0 0 0 1px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.5), 0 24px 64px rgba(0,0,0,0.4);
|
|
861
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
862
|
+
animation: cancia-popup-in 0.25s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
863
|
+
`;
|
|
864
|
+
}
|
|
865
|
+
function openPopup(key, fieldType2, anchorEl, onClose) {
|
|
866
|
+
closePopup();
|
|
867
|
+
const popup = fieldType2 === "image" ? buildImagePopup(key, anchorEl, () => {
|
|
868
|
+
onClose();
|
|
869
|
+
closePopup();
|
|
870
|
+
}) : buildTextPopup(key, anchorEl, () => {
|
|
871
|
+
onClose();
|
|
872
|
+
closePopup();
|
|
873
|
+
});
|
|
874
|
+
document.body.appendChild(popup);
|
|
875
|
+
popupEl = popup;
|
|
876
|
+
const { top, left, origin } = getPopupPosition(anchorEl);
|
|
877
|
+
popup.style.top = `${top}px`;
|
|
878
|
+
popup.style.left = `${left}px`;
|
|
879
|
+
popup.style.transformOrigin = origin;
|
|
880
|
+
outsideListener = (e) => {
|
|
881
|
+
if (!popup.contains(e.target)) {
|
|
882
|
+
closePopup();
|
|
883
|
+
onClose();
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
setTimeout(() => {
|
|
887
|
+
if (outsideListener) document.addEventListener("click", outsideListener, true);
|
|
888
|
+
}, 100);
|
|
889
|
+
keyListener = (e) => {
|
|
890
|
+
if (e.key === "Escape") {
|
|
891
|
+
e.preventDefault();
|
|
892
|
+
closePopup();
|
|
893
|
+
onClose();
|
|
894
|
+
}
|
|
895
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
896
|
+
e.preventDefault();
|
|
897
|
+
popup.querySelector("[data-cancia-save]")?.click();
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
document.addEventListener("keydown", keyListener, true);
|
|
901
|
+
}
|
|
902
|
+
function closePopup() {
|
|
903
|
+
if (outsideListener) {
|
|
904
|
+
document.removeEventListener("click", outsideListener, true);
|
|
905
|
+
outsideListener = null;
|
|
906
|
+
}
|
|
907
|
+
if (keyListener) {
|
|
908
|
+
document.removeEventListener("keydown", keyListener, true);
|
|
909
|
+
keyListener = null;
|
|
910
|
+
}
|
|
911
|
+
if (popupEl) {
|
|
912
|
+
const el = popupEl;
|
|
913
|
+
popupEl = null;
|
|
914
|
+
el.style.animation = "none";
|
|
915
|
+
el.style.transition = "opacity 0.18s cubic-bezier(0.4, 0, 1, 1), transform 0.18s cubic-bezier(0.4, 0, 1, 1)";
|
|
916
|
+
el.style.opacity = "0";
|
|
917
|
+
el.style.transform = "scale(0.96) translateY(3px)";
|
|
918
|
+
setTimeout(() => el.remove(), 200);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/list-panel.ts
|
|
923
|
+
var PANEL_Z = 2147483646;
|
|
924
|
+
var BACKDROP_Z = 2147483646;
|
|
925
|
+
var panelEl = null;
|
|
926
|
+
var backdropEl = null;
|
|
927
|
+
var styleInjected2 = false;
|
|
928
|
+
var currentSchema = null;
|
|
929
|
+
var currentBody = null;
|
|
930
|
+
var currentTabsRow = null;
|
|
931
|
+
var currentOnEditEntry = null;
|
|
932
|
+
var currentOnAddEntry = null;
|
|
933
|
+
var currentOnTranslateEntry = null;
|
|
934
|
+
function injectStyles2() {
|
|
935
|
+
if (styleInjected2) return;
|
|
936
|
+
styleInjected2 = true;
|
|
937
|
+
const s = document.createElement("style");
|
|
938
|
+
s.textContent = `
|
|
939
|
+
@keyframes cancia-panel-in {
|
|
940
|
+
from { transform: translateX(100%); }
|
|
941
|
+
to { transform: translateX(0); }
|
|
942
|
+
}
|
|
943
|
+
@keyframes cancia-panel-out {
|
|
944
|
+
from { transform: translateX(0); }
|
|
945
|
+
to { transform: translateX(100%); }
|
|
946
|
+
}
|
|
947
|
+
@keyframes cancia-backdrop-in {
|
|
948
|
+
from { opacity: 0; }
|
|
949
|
+
to { opacity: 1; }
|
|
950
|
+
}
|
|
951
|
+
`;
|
|
952
|
+
document.head.appendChild(s);
|
|
953
|
+
}
|
|
954
|
+
function accent3() {
|
|
955
|
+
return state.config?.accentColor ?? "#6366f1";
|
|
956
|
+
}
|
|
957
|
+
function escapeHtml(s) {
|
|
958
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
959
|
+
}
|
|
960
|
+
function formatExcerpt(value, maxLength = 120) {
|
|
961
|
+
if (typeof value !== "string") return "";
|
|
962
|
+
const trimmed = value.trim();
|
|
963
|
+
if (trimmed.length <= maxLength) return trimmed;
|
|
964
|
+
return trimmed.slice(0, maxLength).trimEnd() + "\u2026";
|
|
965
|
+
}
|
|
966
|
+
function locales() {
|
|
967
|
+
return state.config?.languages ?? [];
|
|
968
|
+
}
|
|
969
|
+
function buildShell(schema) {
|
|
970
|
+
const panel = document.createElement("div");
|
|
971
|
+
panel.dataset.canciaListPanel = "1";
|
|
972
|
+
panel.style.cssText = `
|
|
973
|
+
position: fixed;
|
|
974
|
+
top: 0; right: 0; bottom: 0;
|
|
975
|
+
width: min(420px, 100vw);
|
|
976
|
+
background: #fff;
|
|
977
|
+
color: #1a1a1d;
|
|
978
|
+
z-index: ${PANEL_Z};
|
|
979
|
+
box-shadow: -8px 0 32px rgba(0,0,0,0.18);
|
|
980
|
+
display: flex;
|
|
981
|
+
flex-direction: column;
|
|
982
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
983
|
+
animation: cancia-panel-in 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
984
|
+
`;
|
|
985
|
+
panel.innerHTML = `
|
|
986
|
+
<header style="
|
|
987
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
988
|
+
padding: 16px 20px;
|
|
989
|
+
border-bottom: 1px solid #eaeaea;
|
|
990
|
+
">
|
|
991
|
+
<div>
|
|
992
|
+
<div style="font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: #777;">List</div>
|
|
993
|
+
<div style="font-size: 17px; font-weight: 600; margin-top: 2px;">${escapeHtml(schema.label)}</div>
|
|
994
|
+
</div>
|
|
995
|
+
<button data-cancia-close style="
|
|
996
|
+
appearance: none; border: 0; background: transparent;
|
|
997
|
+
cursor: pointer; padding: 6px; border-radius: 6px;
|
|
998
|
+
color: #555; transition: background 0.12s, color 0.12s;
|
|
999
|
+
" aria-label="Close panel">
|
|
1000
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
1001
|
+
<path d="M4 4l10 10M14 4L4 14"/>
|
|
1002
|
+
</svg>
|
|
1003
|
+
</button>
|
|
1004
|
+
</header>
|
|
1005
|
+
<div data-cancia-tabs style="
|
|
1006
|
+
display: flex; gap: 4px;
|
|
1007
|
+
padding: 8px 16px 0;
|
|
1008
|
+
border-bottom: 1px solid #f3f3f3;
|
|
1009
|
+
overflow-x: auto;
|
|
1010
|
+
"></div>
|
|
1011
|
+
<div style="padding: 12px 20px; border-bottom: 1px solid #f3f3f3;">
|
|
1012
|
+
<button data-cancia-add style="
|
|
1013
|
+
appearance: none; border: 1px dashed ${accent3()}; background: ${accent3()}10;
|
|
1014
|
+
color: ${accent3()}; font-weight: 600; font-size: 13px;
|
|
1015
|
+
padding: 10px 14px; border-radius: 8px; width: 100%; cursor: pointer;
|
|
1016
|
+
display: flex; align-items: center; justify-content: center; gap: 6px;
|
|
1017
|
+
transition: background 0.12s;
|
|
1018
|
+
">
|
|
1019
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
1020
|
+
<path d="M7 2v10M2 7h10"/>
|
|
1021
|
+
</svg>
|
|
1022
|
+
Add ${escapeHtml(schema.labelSingular.toLowerCase())}
|
|
1023
|
+
</button>
|
|
1024
|
+
</div>
|
|
1025
|
+
<div data-cancia-entries style="
|
|
1026
|
+
flex: 1; overflow-y: auto;
|
|
1027
|
+
padding: 8px 12px 16px;
|
|
1028
|
+
">
|
|
1029
|
+
<div data-cancia-loading style="text-align: center; padding: 32px 12px; color: #888; font-size: 13px;">Loading\u2026</div>
|
|
1030
|
+
</div>
|
|
1031
|
+
`;
|
|
1032
|
+
const body = panel.querySelector("[data-cancia-entries]");
|
|
1033
|
+
const tabsRow = panel.querySelector("[data-cancia-tabs]");
|
|
1034
|
+
return { panel, body, tabsRow };
|
|
1035
|
+
}
|
|
1036
|
+
function renderTabs(tabsRow, activeLocale, onSwitch) {
|
|
1037
|
+
tabsRow.innerHTML = "";
|
|
1038
|
+
const langs = locales();
|
|
1039
|
+
if (langs.length <= 1) {
|
|
1040
|
+
tabsRow.style.display = "none";
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
tabsRow.style.display = "flex";
|
|
1044
|
+
for (const loc of langs) {
|
|
1045
|
+
const tab = document.createElement("button");
|
|
1046
|
+
const isActive = loc === activeLocale;
|
|
1047
|
+
tab.style.cssText = `
|
|
1048
|
+
appearance: none; border: 0; background: transparent;
|
|
1049
|
+
font-family: inherit; font-size: 12px; font-weight: 600;
|
|
1050
|
+
letter-spacing: 0.04em; text-transform: uppercase;
|
|
1051
|
+
padding: 8px 10px 9px;
|
|
1052
|
+
cursor: ${isActive ? "default" : "pointer"};
|
|
1053
|
+
color: ${isActive ? accent3() : "#777"};
|
|
1054
|
+
border-bottom: 2px solid ${isActive ? accent3() : "transparent"};
|
|
1055
|
+
margin-bottom: -1px;
|
|
1056
|
+
transition: color 0.12s, border-color 0.12s;
|
|
1057
|
+
`;
|
|
1058
|
+
tab.textContent = loc;
|
|
1059
|
+
if (!isActive) {
|
|
1060
|
+
tab.addEventListener("mouseenter", () => {
|
|
1061
|
+
tab.style.color = "#333";
|
|
1062
|
+
});
|
|
1063
|
+
tab.addEventListener("mouseleave", () => {
|
|
1064
|
+
tab.style.color = "#777";
|
|
1065
|
+
});
|
|
1066
|
+
tab.addEventListener("click", () => onSwitch(loc));
|
|
1067
|
+
}
|
|
1068
|
+
tabsRow.appendChild(tab);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function renderEntries(body, schema, activeLocale, entriesInLocale, translations, allEntries) {
|
|
1072
|
+
const entryById = /* @__PURE__ */ new Map();
|
|
1073
|
+
for (const e of entriesInLocale) entryById.set(e.id, e);
|
|
1074
|
+
const orderedIds = entriesInLocale.map((e) => e.id);
|
|
1075
|
+
const seen = new Set(orderedIds);
|
|
1076
|
+
for (const t of translations) {
|
|
1077
|
+
if (!seen.has(t.id)) {
|
|
1078
|
+
orderedIds.push(t.id);
|
|
1079
|
+
seen.add(t.id);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const rows = orderedIds.map((id) => {
|
|
1083
|
+
const entry = entryById.get(id) ?? null;
|
|
1084
|
+
let translatedFrom = null;
|
|
1085
|
+
if (!entry) {
|
|
1086
|
+
const t = translations.find((x) => x.id === id);
|
|
1087
|
+
if (t && t.locales.length > 0) translatedFrom = t.locales[0];
|
|
1088
|
+
}
|
|
1089
|
+
return { id, locale: activeLocale, entry, translatedFrom };
|
|
1090
|
+
});
|
|
1091
|
+
if (rows.length === 0) {
|
|
1092
|
+
body.innerHTML = `
|
|
1093
|
+
<div style="text-align: center; padding: 40px 12px; color: #888; font-size: 13px;">
|
|
1094
|
+
No entries yet. Click "Add ${escapeHtml(schema.labelSingular.toLowerCase())}" above to create one.
|
|
1095
|
+
</div>
|
|
1096
|
+
`;
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
body.innerHTML = "";
|
|
1100
|
+
rows.forEach((row) => {
|
|
1101
|
+
const item = document.createElement("button");
|
|
1102
|
+
const isStub = row.entry === null;
|
|
1103
|
+
item.style.cssText = `
|
|
1104
|
+
appearance: none; border: 1px solid transparent; background: ${isStub ? "#fff8ee" : "#fafafa"};
|
|
1105
|
+
text-align: left; width: 100%;
|
|
1106
|
+
padding: 12px 14px; border-radius: 8px; cursor: pointer;
|
|
1107
|
+
margin-bottom: 6px;
|
|
1108
|
+
transition: background 0.12s, border-color 0.12s, transform 0.12s;
|
|
1109
|
+
display: block;
|
|
1110
|
+
`;
|
|
1111
|
+
if (row.entry) {
|
|
1112
|
+
const titleValue = row.entry.data[schema.titleField];
|
|
1113
|
+
const title = typeof titleValue === "string" && titleValue.trim().length > 0 ? titleValue : `(untitled ${schema.labelSingular.toLowerCase()})`;
|
|
1114
|
+
const excerptValue = schema.bodyField ? row.entry.data[schema.bodyField] : void 0;
|
|
1115
|
+
const excerpt = formatExcerpt(excerptValue);
|
|
1116
|
+
item.innerHTML = `
|
|
1117
|
+
<div style="font-weight: 600; font-size: 14px; color: #1a1a1d;">${escapeHtml(title)}</div>
|
|
1118
|
+
${excerpt ? `<div style="font-size: 12px; color: #666; margin-top: 4px; line-height: 1.4;">${escapeHtml(excerpt)}</div>` : ""}
|
|
1119
|
+
`;
|
|
1120
|
+
item.addEventListener("mouseenter", () => {
|
|
1121
|
+
item.style.background = "#f3f3f3";
|
|
1122
|
+
item.style.borderColor = "#e3e3e3";
|
|
1123
|
+
});
|
|
1124
|
+
item.addEventListener("mouseleave", () => {
|
|
1125
|
+
item.style.background = "#fafafa";
|
|
1126
|
+
item.style.borderColor = "transparent";
|
|
1127
|
+
});
|
|
1128
|
+
item.addEventListener("click", () => currentOnEditEntry?.(row.entry, activeLocale));
|
|
1129
|
+
} else {
|
|
1130
|
+
const sourceLocale = row.translatedFrom;
|
|
1131
|
+
const sourceEntry = allEntries.get(sourceLocale)?.find((e) => e.id === row.id) ?? null;
|
|
1132
|
+
const sourceTitleValue = sourceEntry?.data[schema.titleField];
|
|
1133
|
+
const sourceTitle = typeof sourceTitleValue === "string" && sourceTitleValue.trim().length > 0 ? sourceTitleValue : row.id;
|
|
1134
|
+
item.innerHTML = `
|
|
1135
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
1136
|
+
<span style="
|
|
1137
|
+
font-size: 9px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
|
|
1138
|
+
background: #f5b400; color: #fff;
|
|
1139
|
+
padding: 2px 6px; border-radius: 4px;
|
|
1140
|
+
">Not translated</span>
|
|
1141
|
+
<span style="font-size: 11px; color: #888;">from ${escapeHtml(sourceLocale)}</span>
|
|
1142
|
+
</div>
|
|
1143
|
+
<div style="font-weight: 600; font-size: 14px; color: #6b5616; margin-top: 6px;">${escapeHtml(sourceTitle)}</div>
|
|
1144
|
+
<div style="font-size: 11px; color: #b08800; margin-top: 4px;">Click to translate into ${escapeHtml(activeLocale)}</div>
|
|
1145
|
+
`;
|
|
1146
|
+
item.addEventListener("mouseenter", () => {
|
|
1147
|
+
item.style.background = "#fff2d5";
|
|
1148
|
+
item.style.borderColor = "#f5d27a";
|
|
1149
|
+
});
|
|
1150
|
+
item.addEventListener("mouseleave", () => {
|
|
1151
|
+
item.style.background = "#fff8ee";
|
|
1152
|
+
item.style.borderColor = "transparent";
|
|
1153
|
+
});
|
|
1154
|
+
item.addEventListener(
|
|
1155
|
+
"click",
|
|
1156
|
+
() => currentOnTranslateEntry?.(row.id, sourceEntry, activeLocale)
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
body.appendChild(item);
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
async function openListPanel(opts) {
|
|
1163
|
+
closeListPanel();
|
|
1164
|
+
injectStyles2();
|
|
1165
|
+
const backdrop = document.createElement("div");
|
|
1166
|
+
backdrop.dataset.canciaPanelBackdrop = "1";
|
|
1167
|
+
backdrop.style.cssText = `
|
|
1168
|
+
position: fixed; inset: 0;
|
|
1169
|
+
background: rgba(10,10,12,0.32);
|
|
1170
|
+
z-index: ${BACKDROP_Z};
|
|
1171
|
+
animation: cancia-backdrop-in 0.18s ease-out forwards;
|
|
1172
|
+
`;
|
|
1173
|
+
backdrop.addEventListener("click", () => closeListPanel());
|
|
1174
|
+
document.body.appendChild(backdrop);
|
|
1175
|
+
backdropEl = backdrop;
|
|
1176
|
+
const { panel, body, tabsRow } = buildShell(opts.schema);
|
|
1177
|
+
document.body.appendChild(panel);
|
|
1178
|
+
panelEl = panel;
|
|
1179
|
+
currentSchema = opts.schema;
|
|
1180
|
+
currentBody = body;
|
|
1181
|
+
currentTabsRow = tabsRow;
|
|
1182
|
+
currentOnEditEntry = opts.onEditEntry;
|
|
1183
|
+
currentOnAddEntry = opts.onAddEntry;
|
|
1184
|
+
currentOnTranslateEntry = opts.onTranslateEntry;
|
|
1185
|
+
const langs = locales();
|
|
1186
|
+
const initial = opts.initialLocale ?? state.activeLang ?? langs[0] ?? "";
|
|
1187
|
+
state.activeListLocale = langs.includes(initial) ? initial : langs[0] ?? initial;
|
|
1188
|
+
panel.querySelector("[data-cancia-close]")?.addEventListener(
|
|
1189
|
+
"click",
|
|
1190
|
+
() => closeListPanel()
|
|
1191
|
+
);
|
|
1192
|
+
panel.querySelector("[data-cancia-add]")?.addEventListener(
|
|
1193
|
+
"click",
|
|
1194
|
+
() => currentOnAddEntry?.(state.activeListLocale)
|
|
1195
|
+
);
|
|
1196
|
+
await refreshListPanel();
|
|
1197
|
+
}
|
|
1198
|
+
async function refreshListPanel() {
|
|
1199
|
+
if (!panelEl || !currentSchema || !currentBody || !currentTabsRow) return;
|
|
1200
|
+
const schema = currentSchema;
|
|
1201
|
+
const body = currentBody;
|
|
1202
|
+
const tabsRow = currentTabsRow;
|
|
1203
|
+
renderTabs(tabsRow, state.activeListLocale, async (newLocale) => {
|
|
1204
|
+
state.activeListLocale = newLocale;
|
|
1205
|
+
await refreshListPanel();
|
|
1206
|
+
});
|
|
1207
|
+
try {
|
|
1208
|
+
const langs = locales();
|
|
1209
|
+
const [translations, ...perLocale] = await Promise.all([
|
|
1210
|
+
fetchTranslations(schema.name),
|
|
1211
|
+
...langs.map((l) => fetchList(schema.name, l))
|
|
1212
|
+
]);
|
|
1213
|
+
if (!panelEl) return;
|
|
1214
|
+
const allEntries = /* @__PURE__ */ new Map();
|
|
1215
|
+
langs.forEach((l, i) => allEntries.set(l, perLocale[i] ?? []));
|
|
1216
|
+
const activeEntries = allEntries.get(state.activeListLocale) ?? [];
|
|
1217
|
+
renderEntries(body, schema, state.activeListLocale, activeEntries, translations, allEntries);
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
if (!panelEl) return;
|
|
1220
|
+
body.innerHTML = `
|
|
1221
|
+
<div style="text-align: center; padding: 32px 12px; color: #c0392b; font-size: 13px;">
|
|
1222
|
+
Failed to load entries: ${escapeHtml(err instanceof Error ? err.message : String(err))}
|
|
1223
|
+
</div>
|
|
1224
|
+
`;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
function closeListPanel() {
|
|
1228
|
+
if (panelEl) {
|
|
1229
|
+
panelEl.style.animation = "cancia-panel-out 0.18s cubic-bezier(0.7, 0, 0.84, 0) forwards";
|
|
1230
|
+
const el = panelEl;
|
|
1231
|
+
setTimeout(() => el.remove(), 180);
|
|
1232
|
+
panelEl = null;
|
|
1233
|
+
}
|
|
1234
|
+
if (backdropEl) {
|
|
1235
|
+
const el = backdropEl;
|
|
1236
|
+
el.style.opacity = "0";
|
|
1237
|
+
el.style.transition = "opacity 0.18s ease-out";
|
|
1238
|
+
setTimeout(() => el.remove(), 180);
|
|
1239
|
+
backdropEl = null;
|
|
1240
|
+
}
|
|
1241
|
+
currentSchema = null;
|
|
1242
|
+
currentBody = null;
|
|
1243
|
+
currentTabsRow = null;
|
|
1244
|
+
currentOnEditEntry = null;
|
|
1245
|
+
currentOnAddEntry = null;
|
|
1246
|
+
currentOnTranslateEntry = null;
|
|
1247
|
+
}
|
|
1248
|
+
function isListPanelOpen() {
|
|
1249
|
+
return panelEl !== null;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/entry-modal.ts
|
|
1253
|
+
var MODAL_Z = 2147483647;
|
|
1254
|
+
var BACKDROP_Z2 = 2147483646;
|
|
1255
|
+
var modalEl = null;
|
|
1256
|
+
var backdropEl2 = null;
|
|
1257
|
+
var escListener = null;
|
|
1258
|
+
var styleInjected3 = false;
|
|
1259
|
+
function injectStyles3() {
|
|
1260
|
+
if (styleInjected3) return;
|
|
1261
|
+
styleInjected3 = true;
|
|
1262
|
+
const s = document.createElement("style");
|
|
1263
|
+
s.textContent = `
|
|
1264
|
+
@keyframes cancia-modal-in {
|
|
1265
|
+
from { opacity: 0; transform: translate(-50%, calc(-50% + 8px)) scale(0.985); }
|
|
1266
|
+
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
|
1267
|
+
}
|
|
1268
|
+
@keyframes cancia-modal-out {
|
|
1269
|
+
from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
|
1270
|
+
to { opacity: 0; transform: translate(-50%, calc(-50% + 8px)) scale(0.985); }
|
|
1271
|
+
}
|
|
1272
|
+
.cancia-form-input:focus,
|
|
1273
|
+
.cancia-form-textarea:focus,
|
|
1274
|
+
.cancia-form-select:focus {
|
|
1275
|
+
border-color: var(--cancia-accent-border, rgba(99,102,241,0.6));
|
|
1276
|
+
background: rgba(255,255,255,0.05);
|
|
1277
|
+
outline: none;
|
|
1278
|
+
}
|
|
1279
|
+
.cancia-form-input::placeholder,
|
|
1280
|
+
.cancia-form-textarea::placeholder {
|
|
1281
|
+
color: rgba(255,255,255,0.25);
|
|
1282
|
+
}
|
|
1283
|
+
.cancia-form-select option {
|
|
1284
|
+
background: #15151a;
|
|
1285
|
+
color: rgba(255,255,255,0.9);
|
|
1286
|
+
}
|
|
1287
|
+
.cancia-field-error {
|
|
1288
|
+
color: #ff8786;
|
|
1289
|
+
font-size: 11px;
|
|
1290
|
+
margin-top: 5px;
|
|
1291
|
+
line-height: 1.35;
|
|
1292
|
+
}
|
|
1293
|
+
`;
|
|
1294
|
+
document.head.appendChild(s);
|
|
1295
|
+
}
|
|
1296
|
+
function accent4() {
|
|
1297
|
+
return state.config?.accentColor ?? "#6366f1";
|
|
1298
|
+
}
|
|
1299
|
+
function accentBorder() {
|
|
1300
|
+
const a = accent4();
|
|
1301
|
+
if (/^#[0-9a-f]{6}$/i.test(a)) return `${a}99`;
|
|
1302
|
+
return "rgba(99,102,241,0.6)";
|
|
1303
|
+
}
|
|
1304
|
+
function isoToLocalInput(iso) {
|
|
1305
|
+
if (!iso) return "";
|
|
1306
|
+
const d = new Date(iso);
|
|
1307
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
1308
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
1309
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1310
|
+
}
|
|
1311
|
+
function localInputToIso(local) {
|
|
1312
|
+
if (!local) return "";
|
|
1313
|
+
const d = new Date(local);
|
|
1314
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
1315
|
+
return d.toISOString();
|
|
1316
|
+
}
|
|
1317
|
+
var INPUT_BASE = `
|
|
1318
|
+
width: 100%; box-sizing: border-box;
|
|
1319
|
+
background: rgba(255,255,255,0.03);
|
|
1320
|
+
color: rgba(255,255,255,0.9);
|
|
1321
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
1322
|
+
border-radius: 8px;
|
|
1323
|
+
padding: 8px 11px;
|
|
1324
|
+
font-size: 13px;
|
|
1325
|
+
font-family: inherit;
|
|
1326
|
+
line-height: 1.5;
|
|
1327
|
+
outline: none;
|
|
1328
|
+
transition: border-color 0.18s, background 0.18s;
|
|
1329
|
+
`;
|
|
1330
|
+
function renderField(field, initial) {
|
|
1331
|
+
const wrapper = document.createElement("div");
|
|
1332
|
+
wrapper.style.cssText = `margin-bottom: 14px;`;
|
|
1333
|
+
const labelRow = document.createElement("label");
|
|
1334
|
+
labelRow.style.cssText = `
|
|
1335
|
+
display: flex; align-items: baseline; justify-content: space-between;
|
|
1336
|
+
gap: 8px;
|
|
1337
|
+
font-size: 11px; font-weight: 600;
|
|
1338
|
+
color: rgba(255,255,255,0.75);
|
|
1339
|
+
letter-spacing: 0.04em;
|
|
1340
|
+
margin-bottom: 5px;
|
|
1341
|
+
`;
|
|
1342
|
+
const labelText = document.createElement("span");
|
|
1343
|
+
labelText.textContent = field.label;
|
|
1344
|
+
if (field.required) {
|
|
1345
|
+
const star = document.createElement("span");
|
|
1346
|
+
star.textContent = " *";
|
|
1347
|
+
star.style.color = "rgba(255,135,134,0.8)";
|
|
1348
|
+
labelText.appendChild(star);
|
|
1349
|
+
}
|
|
1350
|
+
labelRow.appendChild(labelText);
|
|
1351
|
+
wrapper.appendChild(labelRow);
|
|
1352
|
+
if (field.description) {
|
|
1353
|
+
const help = document.createElement("div");
|
|
1354
|
+
help.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.35); margin-bottom: 6px; line-height: 1.4;`;
|
|
1355
|
+
help.textContent = field.description;
|
|
1356
|
+
wrapper.appendChild(help);
|
|
1357
|
+
}
|
|
1358
|
+
const errorEl = document.createElement("div");
|
|
1359
|
+
errorEl.className = "cancia-field-error";
|
|
1360
|
+
errorEl.style.display = "none";
|
|
1361
|
+
const setError = (msg) => {
|
|
1362
|
+
if (msg) {
|
|
1363
|
+
errorEl.textContent = msg;
|
|
1364
|
+
errorEl.style.display = "block";
|
|
1365
|
+
} else {
|
|
1366
|
+
errorEl.textContent = "";
|
|
1367
|
+
errorEl.style.display = "none";
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
let getValue2;
|
|
1371
|
+
switch (field.widget) {
|
|
1372
|
+
case "textarea": {
|
|
1373
|
+
const ta = document.createElement("textarea");
|
|
1374
|
+
ta.className = "cancia-form-textarea";
|
|
1375
|
+
ta.style.cssText = `${INPUT_BASE} resize: vertical; min-height: 110px; max-height: 320px; caret-color: ${accent4()};`;
|
|
1376
|
+
ta.rows = 5;
|
|
1377
|
+
if (field.placeholder) ta.placeholder = field.placeholder;
|
|
1378
|
+
if (typeof initial === "string") ta.value = initial;
|
|
1379
|
+
wrapper.appendChild(ta);
|
|
1380
|
+
getValue2 = () => ta.value;
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
case "checkbox": {
|
|
1384
|
+
const row = document.createElement("label");
|
|
1385
|
+
row.style.cssText = `display: flex; align-items: center; gap: 9px; cursor: pointer; user-select: none; padding: 6px 0;`;
|
|
1386
|
+
const cb = document.createElement("input");
|
|
1387
|
+
cb.type = "checkbox";
|
|
1388
|
+
cb.style.cssText = `width: 16px; height: 16px; accent-color: ${accent4()};`;
|
|
1389
|
+
if (initial === true) cb.checked = true;
|
|
1390
|
+
const txt = document.createElement("span");
|
|
1391
|
+
txt.style.cssText = `font-size: 13px; color: rgba(255,255,255,0.7);`;
|
|
1392
|
+
txt.textContent = field.placeholder ?? `Enable ${field.label.toLowerCase()}`;
|
|
1393
|
+
row.appendChild(cb);
|
|
1394
|
+
row.appendChild(txt);
|
|
1395
|
+
wrapper.appendChild(row);
|
|
1396
|
+
getValue2 = () => cb.checked;
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
case "select": {
|
|
1400
|
+
const sel = document.createElement("select");
|
|
1401
|
+
sel.className = "cancia-form-select";
|
|
1402
|
+
sel.style.cssText = `${INPUT_BASE} appearance: none; padding-right: 30px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2.5 3.75l2.5 2.5 2.5-2.5' stroke='rgba(255,255,255,0.4)' stroke-width='1.3' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 10px center; cursor: pointer;`;
|
|
1403
|
+
if (!field.required) {
|
|
1404
|
+
const empty = document.createElement("option");
|
|
1405
|
+
empty.value = "";
|
|
1406
|
+
empty.textContent = "\u2014";
|
|
1407
|
+
sel.appendChild(empty);
|
|
1408
|
+
}
|
|
1409
|
+
for (const opt of field.options ?? []) {
|
|
1410
|
+
const o = document.createElement("option");
|
|
1411
|
+
o.value = opt;
|
|
1412
|
+
o.textContent = opt;
|
|
1413
|
+
if (initial === opt) o.selected = true;
|
|
1414
|
+
sel.appendChild(o);
|
|
1415
|
+
}
|
|
1416
|
+
wrapper.appendChild(sel);
|
|
1417
|
+
getValue2 = () => sel.value === "" ? void 0 : sel.value;
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1420
|
+
case "image": {
|
|
1421
|
+
const initialUrl = typeof initial === "string" ? initial : "";
|
|
1422
|
+
let currentUrl = initialUrl;
|
|
1423
|
+
const container = document.createElement("div");
|
|
1424
|
+
container.style.cssText = `
|
|
1425
|
+
display: flex; gap: 10px; align-items: stretch;
|
|
1426
|
+
background: rgba(255,255,255,0.02);
|
|
1427
|
+
border: 1px dashed rgba(255,255,255,0.1);
|
|
1428
|
+
border-radius: 10px;
|
|
1429
|
+
padding: 10px;
|
|
1430
|
+
`;
|
|
1431
|
+
const preview = document.createElement("div");
|
|
1432
|
+
preview.style.cssText = `
|
|
1433
|
+
width: 72px; height: 72px; flex-shrink: 0;
|
|
1434
|
+
background: rgba(255,255,255,0.04) no-repeat center / cover;
|
|
1435
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
1436
|
+
border-radius: 6px;
|
|
1437
|
+
display: flex; align-items: center; justify-content: center;
|
|
1438
|
+
color: rgba(255,255,255,0.25);
|
|
1439
|
+
`;
|
|
1440
|
+
const updatePreview = (url) => {
|
|
1441
|
+
if (url) {
|
|
1442
|
+
preview.style.backgroundImage = `url("${url.replace(/"/g, '\\"')}")`;
|
|
1443
|
+
preview.innerHTML = "";
|
|
1444
|
+
} else {
|
|
1445
|
+
preview.style.backgroundImage = "";
|
|
1446
|
+
preview.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>`;
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
updatePreview(initialUrl);
|
|
1450
|
+
const right = document.createElement("div");
|
|
1451
|
+
right.style.cssText = `flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px;`;
|
|
1452
|
+
const fileInput = document.createElement("input");
|
|
1453
|
+
fileInput.type = "file";
|
|
1454
|
+
fileInput.accept = "image/*";
|
|
1455
|
+
fileInput.style.display = "none";
|
|
1456
|
+
const btnRow = document.createElement("div");
|
|
1457
|
+
btnRow.style.cssText = `display: flex; gap: 6px;`;
|
|
1458
|
+
const uploadBtn = document.createElement("button");
|
|
1459
|
+
uploadBtn.type = "button";
|
|
1460
|
+
uploadBtn.style.cssText = `
|
|
1461
|
+
appearance: none; cursor: pointer;
|
|
1462
|
+
background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.85);
|
|
1463
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
1464
|
+
font-size: 11px; font-weight: 500; letter-spacing: 0.02em;
|
|
1465
|
+
padding: 5px 10px; border-radius: 6px;
|
|
1466
|
+
transition: background 0.15s;
|
|
1467
|
+
`;
|
|
1468
|
+
uploadBtn.textContent = "Upload\u2026";
|
|
1469
|
+
uploadBtn.addEventListener("mouseenter", () => {
|
|
1470
|
+
uploadBtn.style.background = "rgba(255,255,255,0.1)";
|
|
1471
|
+
});
|
|
1472
|
+
uploadBtn.addEventListener("mouseleave", () => {
|
|
1473
|
+
uploadBtn.style.background = "rgba(255,255,255,0.06)";
|
|
1474
|
+
});
|
|
1475
|
+
uploadBtn.addEventListener("click", () => fileInput.click());
|
|
1476
|
+
const clearBtn = document.createElement("button");
|
|
1477
|
+
clearBtn.type = "button";
|
|
1478
|
+
clearBtn.style.cssText = `
|
|
1479
|
+
appearance: none; cursor: pointer;
|
|
1480
|
+
background: transparent; color: rgba(255,255,255,0.4);
|
|
1481
|
+
border: 1px solid transparent;
|
|
1482
|
+
font-size: 11px;
|
|
1483
|
+
padding: 5px 8px; border-radius: 6px;
|
|
1484
|
+
`;
|
|
1485
|
+
clearBtn.textContent = "Clear";
|
|
1486
|
+
clearBtn.addEventListener("click", () => {
|
|
1487
|
+
currentUrl = "";
|
|
1488
|
+
urlField.value = "";
|
|
1489
|
+
updatePreview("");
|
|
1490
|
+
});
|
|
1491
|
+
btnRow.appendChild(uploadBtn);
|
|
1492
|
+
btnRow.appendChild(clearBtn);
|
|
1493
|
+
const urlField = document.createElement("input");
|
|
1494
|
+
urlField.type = "url";
|
|
1495
|
+
urlField.className = "cancia-form-input";
|
|
1496
|
+
urlField.style.cssText = `${INPUT_BASE} font-size: 11px; padding: 6px 9px;`;
|
|
1497
|
+
urlField.placeholder = "https://\u2026 or upload";
|
|
1498
|
+
urlField.value = initialUrl;
|
|
1499
|
+
urlField.addEventListener("input", () => {
|
|
1500
|
+
currentUrl = urlField.value.trim();
|
|
1501
|
+
updatePreview(currentUrl);
|
|
1502
|
+
});
|
|
1503
|
+
const progressEl = document.createElement("div");
|
|
1504
|
+
progressEl.style.cssText = `font-size: 10px; color: rgba(255,255,255,0.5); height: 12px;`;
|
|
1505
|
+
right.appendChild(btnRow);
|
|
1506
|
+
right.appendChild(urlField);
|
|
1507
|
+
right.appendChild(progressEl);
|
|
1508
|
+
container.appendChild(preview);
|
|
1509
|
+
container.appendChild(right);
|
|
1510
|
+
container.appendChild(fileInput);
|
|
1511
|
+
wrapper.appendChild(container);
|
|
1512
|
+
fileInput.addEventListener("change", async () => {
|
|
1513
|
+
const file = fileInput.files?.[0];
|
|
1514
|
+
if (!file) return;
|
|
1515
|
+
uploadBtn.disabled = true;
|
|
1516
|
+
progressEl.style.color = "rgba(255,255,255,0.5)";
|
|
1517
|
+
try {
|
|
1518
|
+
const url = await uploadImage(file, (pct) => {
|
|
1519
|
+
progressEl.textContent = `Uploading\u2026 ${pct}%`;
|
|
1520
|
+
});
|
|
1521
|
+
currentUrl = url;
|
|
1522
|
+
urlField.value = url;
|
|
1523
|
+
updatePreview(url);
|
|
1524
|
+
progressEl.textContent = "Uploaded";
|
|
1525
|
+
setTimeout(() => {
|
|
1526
|
+
progressEl.textContent = "";
|
|
1527
|
+
}, 1500);
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
progressEl.textContent = `Upload failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1530
|
+
progressEl.style.color = "#ff8786";
|
|
1531
|
+
} finally {
|
|
1532
|
+
uploadBtn.disabled = false;
|
|
1533
|
+
fileInput.value = "";
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
getValue2 = () => currentUrl === "" ? void 0 : currentUrl;
|
|
1537
|
+
break;
|
|
1538
|
+
}
|
|
1539
|
+
case "datetime": {
|
|
1540
|
+
const input = document.createElement("input");
|
|
1541
|
+
input.type = "datetime-local";
|
|
1542
|
+
input.className = "cancia-form-input";
|
|
1543
|
+
input.style.cssText = `${INPUT_BASE} color-scheme: dark; caret-color: ${accent4()};`;
|
|
1544
|
+
if (typeof initial === "string") input.value = isoToLocalInput(initial);
|
|
1545
|
+
wrapper.appendChild(input);
|
|
1546
|
+
getValue2 = () => {
|
|
1547
|
+
const v = input.value.trim();
|
|
1548
|
+
if (!v) return void 0;
|
|
1549
|
+
return localInputToIso(v);
|
|
1550
|
+
};
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
case "number": {
|
|
1554
|
+
const input = document.createElement("input");
|
|
1555
|
+
input.type = "number";
|
|
1556
|
+
input.className = "cancia-form-input";
|
|
1557
|
+
input.style.cssText = `${INPUT_BASE} caret-color: ${accent4()};`;
|
|
1558
|
+
if (field.min !== void 0) input.min = String(field.min);
|
|
1559
|
+
if (field.max !== void 0) input.max = String(field.max);
|
|
1560
|
+
if (typeof initial === "number") input.value = String(initial);
|
|
1561
|
+
else if (typeof initial === "string" && initial !== "") input.value = initial;
|
|
1562
|
+
wrapper.appendChild(input);
|
|
1563
|
+
getValue2 = () => {
|
|
1564
|
+
const v = input.value.trim();
|
|
1565
|
+
if (v === "") return void 0;
|
|
1566
|
+
const n = Number(v);
|
|
1567
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
1568
|
+
};
|
|
1569
|
+
break;
|
|
1570
|
+
}
|
|
1571
|
+
default: {
|
|
1572
|
+
const input = document.createElement("input");
|
|
1573
|
+
input.type = field.widget === "url" ? "url" : field.widget === "email" ? "email" : "text";
|
|
1574
|
+
input.className = "cancia-form-input";
|
|
1575
|
+
input.style.cssText = `${INPUT_BASE} caret-color: ${accent4()};`;
|
|
1576
|
+
if (field.placeholder) input.placeholder = field.placeholder;
|
|
1577
|
+
if (field.minLength !== void 0) input.minLength = field.minLength;
|
|
1578
|
+
if (field.maxLength !== void 0) input.maxLength = field.maxLength;
|
|
1579
|
+
if (typeof initial === "string") input.value = initial;
|
|
1580
|
+
wrapper.appendChild(input);
|
|
1581
|
+
getValue2 = () => input.value;
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
wrapper.appendChild(errorEl);
|
|
1586
|
+
return { wrapper, fieldState: { field, getValue: getValue2, setError } };
|
|
1587
|
+
}
|
|
1588
|
+
function preValidate(fieldStates) {
|
|
1589
|
+
const data = {};
|
|
1590
|
+
let ok = true;
|
|
1591
|
+
for (const f of fieldStates) {
|
|
1592
|
+
const value = f.getValue();
|
|
1593
|
+
f.setError(null);
|
|
1594
|
+
if (f.field.required && (value === void 0 || value === "" || value === null)) {
|
|
1595
|
+
f.setError(`${f.field.label} is required`);
|
|
1596
|
+
ok = false;
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
if (typeof value === "string") {
|
|
1600
|
+
if (f.field.minLength !== void 0 && value.length < f.field.minLength) {
|
|
1601
|
+
f.setError(`Must be at least ${f.field.minLength} character${f.field.minLength === 1 ? "" : "s"}`);
|
|
1602
|
+
ok = false;
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
if (f.field.maxLength !== void 0 && value.length > f.field.maxLength) {
|
|
1606
|
+
f.setError(`Must be at most ${f.field.maxLength} characters`);
|
|
1607
|
+
ok = false;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
if (value !== void 0 && value !== "") {
|
|
1612
|
+
data[f.field.name] = value;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return { data, ok };
|
|
1616
|
+
}
|
|
1617
|
+
function openEntryModal(opts) {
|
|
1618
|
+
closeEntryModal();
|
|
1619
|
+
injectStyles3();
|
|
1620
|
+
const isEdit = opts.entry !== null;
|
|
1621
|
+
const isTranslate = !isEdit && (opts.translateFromEntry ?? null) !== null;
|
|
1622
|
+
document.documentElement.style.setProperty("--cancia-accent-border", accentBorder());
|
|
1623
|
+
const backdrop = document.createElement("div");
|
|
1624
|
+
backdrop.dataset.canciaModalBackdrop = "1";
|
|
1625
|
+
backdrop.style.cssText = `
|
|
1626
|
+
position: fixed; inset: 0;
|
|
1627
|
+
background: rgba(8,8,10,0.55);
|
|
1628
|
+
backdrop-filter: blur(3px);
|
|
1629
|
+
-webkit-backdrop-filter: blur(3px);
|
|
1630
|
+
z-index: ${BACKDROP_Z2};
|
|
1631
|
+
opacity: 0;
|
|
1632
|
+
transition: opacity 0.2s ease-out;
|
|
1633
|
+
`;
|
|
1634
|
+
document.body.appendChild(backdrop);
|
|
1635
|
+
requestAnimationFrame(() => {
|
|
1636
|
+
backdrop.style.opacity = "1";
|
|
1637
|
+
});
|
|
1638
|
+
backdropEl2 = backdrop;
|
|
1639
|
+
const modal = document.createElement("div");
|
|
1640
|
+
modal.dataset.canciaModal = "1";
|
|
1641
|
+
modal.style.cssText = `
|
|
1642
|
+
position: fixed;
|
|
1643
|
+
top: 50%; left: 50%;
|
|
1644
|
+
transform: translate(-50%, -50%);
|
|
1645
|
+
width: min(480px, calc(100vw - 32px));
|
|
1646
|
+
max-height: min(680px, calc(100vh - 48px));
|
|
1647
|
+
display: flex; flex-direction: column;
|
|
1648
|
+
background: rgba(14, 14, 16, 0.97);
|
|
1649
|
+
backdrop-filter: blur(24px) saturate(180%);
|
|
1650
|
+
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
|
1651
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
1652
|
+
border-radius: 14px;
|
|
1653
|
+
box-shadow: 0 0 0 1px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.5), 0 24px 64px rgba(0,0,0,0.4);
|
|
1654
|
+
z-index: ${MODAL_Z};
|
|
1655
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1656
|
+
color: rgba(255,255,255,0.9);
|
|
1657
|
+
animation: cancia-modal-in 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
1658
|
+
overflow: hidden;
|
|
1659
|
+
`;
|
|
1660
|
+
document.body.appendChild(modal);
|
|
1661
|
+
modalEl = modal;
|
|
1662
|
+
const header = document.createElement("div");
|
|
1663
|
+
header.style.cssText = `
|
|
1664
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
1665
|
+
padding: 14px 16px 12px;
|
|
1666
|
+
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
1667
|
+
flex-shrink: 0;
|
|
1668
|
+
`;
|
|
1669
|
+
const titleWrap = document.createElement("div");
|
|
1670
|
+
titleWrap.style.cssText = `display: flex; flex-direction: column; gap: 2px; min-width: 0;`;
|
|
1671
|
+
const eyebrow = document.createElement("span");
|
|
1672
|
+
eyebrow.style.cssText = `
|
|
1673
|
+
font-size: 10px; font-weight: 600;
|
|
1674
|
+
color: rgba(255,255,255,0.32);
|
|
1675
|
+
letter-spacing: 0.08em; text-transform: uppercase;
|
|
1676
|
+
`;
|
|
1677
|
+
const action = isEdit ? "Edit" : isTranslate ? "Translate" : "New";
|
|
1678
|
+
eyebrow.textContent = `${action} ${opts.schema.labelSingular.toLowerCase()} \xB7 ${opts.locale}`;
|
|
1679
|
+
const titleEl = document.createElement("span");
|
|
1680
|
+
titleEl.style.cssText = `
|
|
1681
|
+
font-size: 14px; font-weight: 600;
|
|
1682
|
+
color: rgba(255,255,255,0.92);
|
|
1683
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
1684
|
+
`;
|
|
1685
|
+
titleEl.textContent = opts.schema.label;
|
|
1686
|
+
titleWrap.appendChild(eyebrow);
|
|
1687
|
+
titleWrap.appendChild(titleEl);
|
|
1688
|
+
header.appendChild(titleWrap);
|
|
1689
|
+
header.appendChild(makeCloseButton(() => closeEntryModal()));
|
|
1690
|
+
modal.appendChild(header);
|
|
1691
|
+
const body = document.createElement("div");
|
|
1692
|
+
body.style.cssText = `
|
|
1693
|
+
padding: 14px 16px 4px;
|
|
1694
|
+
overflow-y: auto;
|
|
1695
|
+
flex: 1 1 auto;
|
|
1696
|
+
min-height: 0;
|
|
1697
|
+
`;
|
|
1698
|
+
modal.appendChild(body);
|
|
1699
|
+
const formError = document.createElement("div");
|
|
1700
|
+
formError.style.cssText = `
|
|
1701
|
+
display: none;
|
|
1702
|
+
background: rgba(255,135,134,0.08);
|
|
1703
|
+
color: #ff8786;
|
|
1704
|
+
border: 1px solid rgba(255,135,134,0.18);
|
|
1705
|
+
border-radius: 8px;
|
|
1706
|
+
padding: 9px 11px;
|
|
1707
|
+
font-size: 12px;
|
|
1708
|
+
line-height: 1.4;
|
|
1709
|
+
margin-bottom: 12px;
|
|
1710
|
+
`;
|
|
1711
|
+
body.appendChild(formError);
|
|
1712
|
+
function showFormError(msg) {
|
|
1713
|
+
formError.textContent = msg;
|
|
1714
|
+
formError.style.display = "block";
|
|
1715
|
+
}
|
|
1716
|
+
function clearFormError() {
|
|
1717
|
+
formError.textContent = "";
|
|
1718
|
+
formError.style.display = "none";
|
|
1719
|
+
}
|
|
1720
|
+
if (isTranslate) {
|
|
1721
|
+
const banner = document.createElement("div");
|
|
1722
|
+
banner.style.cssText = `
|
|
1723
|
+
background: rgba(245,180,0,0.1);
|
|
1724
|
+
color: #f5b400;
|
|
1725
|
+
border: 1px solid rgba(245,180,0,0.25);
|
|
1726
|
+
border-radius: 8px;
|
|
1727
|
+
padding: 9px 11px;
|
|
1728
|
+
font-size: 12px;
|
|
1729
|
+
line-height: 1.4;
|
|
1730
|
+
margin-bottom: 12px;
|
|
1731
|
+
`;
|
|
1732
|
+
const src = opts.translateFromEntry;
|
|
1733
|
+
banner.textContent = `Translating from ${src.locale} into ${opts.locale}. Fields are pre-filled from the source.`;
|
|
1734
|
+
body.appendChild(banner);
|
|
1735
|
+
}
|
|
1736
|
+
const fieldStates = [];
|
|
1737
|
+
const sourceForInitial = opts.entry ?? opts.translateFromEntry ?? null;
|
|
1738
|
+
for (const f of opts.schema.fields) {
|
|
1739
|
+
const initial = sourceForInitial?.data[f.name];
|
|
1740
|
+
const { wrapper, fieldState } = renderField(f, initial);
|
|
1741
|
+
body.appendChild(wrapper);
|
|
1742
|
+
fieldStates.push(fieldState);
|
|
1743
|
+
}
|
|
1744
|
+
const footer = document.createElement("div");
|
|
1745
|
+
footer.style.cssText = `
|
|
1746
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
1747
|
+
gap: 10px;
|
|
1748
|
+
padding: 12px 16px;
|
|
1749
|
+
border-top: 1px solid rgba(255,255,255,0.06);
|
|
1750
|
+
background: rgba(0,0,0,0.18);
|
|
1751
|
+
flex-shrink: 0;
|
|
1752
|
+
`;
|
|
1753
|
+
const leftActions = document.createElement("div");
|
|
1754
|
+
const rightActions = document.createElement("div");
|
|
1755
|
+
rightActions.style.cssText = `display: flex; gap: 8px;`;
|
|
1756
|
+
if (isEdit) {
|
|
1757
|
+
const deleteBtn = document.createElement("button");
|
|
1758
|
+
deleteBtn.type = "button";
|
|
1759
|
+
deleteBtn.style.cssText = `
|
|
1760
|
+
appearance: none; cursor: pointer;
|
|
1761
|
+
background: transparent; border: 1px solid transparent;
|
|
1762
|
+
color: #ff8786;
|
|
1763
|
+
font-size: 12px; font-weight: 500;
|
|
1764
|
+
padding: 6px 10px; border-radius: 7px;
|
|
1765
|
+
transition: background 0.15s;
|
|
1766
|
+
`;
|
|
1767
|
+
deleteBtn.textContent = "Delete";
|
|
1768
|
+
deleteBtn.addEventListener("mouseenter", () => {
|
|
1769
|
+
deleteBtn.style.background = "rgba(255,135,134,0.08)";
|
|
1770
|
+
});
|
|
1771
|
+
deleteBtn.addEventListener("mouseleave", () => {
|
|
1772
|
+
deleteBtn.style.background = "transparent";
|
|
1773
|
+
});
|
|
1774
|
+
deleteBtn.addEventListener("click", async () => {
|
|
1775
|
+
if (!confirm(`Delete the ${opts.locale} version of this ${opts.schema.labelSingular.toLowerCase()}? This can't be undone.`)) return;
|
|
1776
|
+
deleteBtn.disabled = true;
|
|
1777
|
+
try {
|
|
1778
|
+
await deleteListEntry(opts.schema.name, opts.entry.id, opts.locale);
|
|
1779
|
+
opts.onSaved();
|
|
1780
|
+
closeEntryModal();
|
|
1781
|
+
} catch (err) {
|
|
1782
|
+
showFormError(err instanceof Error ? err.message : String(err));
|
|
1783
|
+
deleteBtn.disabled = false;
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
leftActions.appendChild(deleteBtn);
|
|
1787
|
+
}
|
|
1788
|
+
const cancelBtn = document.createElement("button");
|
|
1789
|
+
cancelBtn.type = "button";
|
|
1790
|
+
cancelBtn.style.cssText = `
|
|
1791
|
+
appearance: none; cursor: pointer;
|
|
1792
|
+
background: rgba(255,255,255,0.04);
|
|
1793
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
1794
|
+
color: rgba(255,255,255,0.75);
|
|
1795
|
+
font-size: 12px; font-weight: 500;
|
|
1796
|
+
padding: 7px 14px; border-radius: 8px;
|
|
1797
|
+
transition: background 0.15s, color 0.15s;
|
|
1798
|
+
`;
|
|
1799
|
+
cancelBtn.textContent = "Cancel";
|
|
1800
|
+
cancelBtn.addEventListener("mouseenter", () => {
|
|
1801
|
+
cancelBtn.style.background = "rgba(255,255,255,0.08)";
|
|
1802
|
+
cancelBtn.style.color = "rgba(255,255,255,0.9)";
|
|
1803
|
+
});
|
|
1804
|
+
cancelBtn.addEventListener("mouseleave", () => {
|
|
1805
|
+
cancelBtn.style.background = "rgba(255,255,255,0.04)";
|
|
1806
|
+
cancelBtn.style.color = "rgba(255,255,255,0.75)";
|
|
1807
|
+
});
|
|
1808
|
+
cancelBtn.addEventListener("click", () => closeEntryModal());
|
|
1809
|
+
const saveBtn = makePrimaryButton(isEdit ? "Save" : "Create", accent4());
|
|
1810
|
+
saveBtn.addEventListener("click", async () => {
|
|
1811
|
+
clearFormError();
|
|
1812
|
+
const { data, ok } = preValidate(fieldStates);
|
|
1813
|
+
if (!ok) return;
|
|
1814
|
+
saveBtn.disabled = true;
|
|
1815
|
+
saveBtn.style.opacity = "0.6";
|
|
1816
|
+
const original = saveBtn.textContent;
|
|
1817
|
+
saveBtn.textContent = isEdit ? "Saving\u2026" : "Creating\u2026";
|
|
1818
|
+
try {
|
|
1819
|
+
if (isEdit) {
|
|
1820
|
+
await updateListEntry(opts.schema.name, opts.entry.id, data, opts.entry._rev, opts.locale);
|
|
1821
|
+
} else {
|
|
1822
|
+
let id;
|
|
1823
|
+
if (opts.fixedId) {
|
|
1824
|
+
id = opts.fixedId;
|
|
1825
|
+
} else {
|
|
1826
|
+
const slugFieldName = opts.schema.slugField;
|
|
1827
|
+
id = slugFieldName && typeof data[slugFieldName] === "string" ? data[slugFieldName] : void 0;
|
|
1828
|
+
}
|
|
1829
|
+
await createListEntry(opts.schema.name, data, opts.locale, id);
|
|
1830
|
+
}
|
|
1831
|
+
opts.onSaved();
|
|
1832
|
+
closeEntryModal();
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
const error = err;
|
|
1835
|
+
if (error.message.includes("Validation failed")) {
|
|
1836
|
+
showFormError("Server-side validation failed. Check the fields above.");
|
|
1837
|
+
} else if (error.code === "REV_CONFLICT") {
|
|
1838
|
+
showFormError("This entry was changed by someone else. Close and reopen to see the latest version.");
|
|
1839
|
+
} else {
|
|
1840
|
+
showFormError(error.message);
|
|
1841
|
+
}
|
|
1842
|
+
saveBtn.disabled = false;
|
|
1843
|
+
saveBtn.style.opacity = "1";
|
|
1844
|
+
saveBtn.textContent = original ?? (isEdit ? "Save" : "Create");
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1847
|
+
rightActions.appendChild(cancelBtn);
|
|
1848
|
+
rightActions.appendChild(saveBtn);
|
|
1849
|
+
footer.appendChild(leftActions);
|
|
1850
|
+
footer.appendChild(rightActions);
|
|
1851
|
+
modal.appendChild(footer);
|
|
1852
|
+
backdrop.addEventListener("click", () => closeEntryModal());
|
|
1853
|
+
escListener = (e) => {
|
|
1854
|
+
if (e.key === "Escape") {
|
|
1855
|
+
e.stopPropagation();
|
|
1856
|
+
closeEntryModal();
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
document.addEventListener("keydown", escListener, true);
|
|
1860
|
+
setTimeout(() => {
|
|
1861
|
+
const firstInput = body.querySelector("input, textarea, select");
|
|
1862
|
+
firstInput?.focus();
|
|
1863
|
+
}, 90);
|
|
1864
|
+
}
|
|
1865
|
+
function closeEntryModal() {
|
|
1866
|
+
if (modalEl) {
|
|
1867
|
+
modalEl.style.animation = "cancia-modal-out 0.16s cubic-bezier(0.7, 0, 0.84, 0) forwards";
|
|
1868
|
+
const el = modalEl;
|
|
1869
|
+
setTimeout(() => el.remove(), 160);
|
|
1870
|
+
modalEl = null;
|
|
1871
|
+
}
|
|
1872
|
+
if (backdropEl2) {
|
|
1873
|
+
const el = backdropEl2;
|
|
1874
|
+
el.style.opacity = "0";
|
|
1875
|
+
setTimeout(() => el.remove(), 180);
|
|
1876
|
+
backdropEl2 = null;
|
|
1877
|
+
}
|
|
1878
|
+
if (escListener) {
|
|
1879
|
+
document.removeEventListener("keydown", escListener, true);
|
|
1880
|
+
escListener = null;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function isEntryModalOpen() {
|
|
1884
|
+
return modalEl !== null;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// src/toolbar.ts
|
|
1888
|
+
var toolbarEl = null;
|
|
1889
|
+
var pendingPanelEl = null;
|
|
1890
|
+
var isExpanded = false;
|
|
1891
|
+
var expandedEscListener = null;
|
|
1892
|
+
function accent5() {
|
|
1893
|
+
return state.config?.accentColor ?? "#6366f1";
|
|
1894
|
+
}
|
|
1895
|
+
var styleInjected4 = false;
|
|
1896
|
+
function injectStyles4() {
|
|
1897
|
+
if (styleInjected4) return;
|
|
1898
|
+
styleInjected4 = true;
|
|
1899
|
+
const s = document.createElement("style");
|
|
1900
|
+
s.textContent = `
|
|
1901
|
+
@keyframes cancia-enter {
|
|
1902
|
+
from { opacity: 0; transform: scale(0.5) rotate(90deg); }
|
|
1903
|
+
to { opacity: 1; transform: scale(1) rotate(0deg); }
|
|
1904
|
+
}
|
|
1905
|
+
@keyframes cancia-exit {
|
|
1906
|
+
from { opacity: 1; transform: scale(1); }
|
|
1907
|
+
to { opacity: 0; transform: scale(0.8); }
|
|
1908
|
+
}
|
|
1909
|
+
@keyframes cancia-controls-in {
|
|
1910
|
+
from { opacity: 0; filter: blur(8px); transform: scale(0.6); }
|
|
1911
|
+
to { opacity: 1; filter: blur(0px); transform: scale(1); }
|
|
1912
|
+
}
|
|
1913
|
+
@keyframes cancia-controls-out {
|
|
1914
|
+
from { opacity: 1; filter: blur(0px); transform: scale(1); }
|
|
1915
|
+
to { opacity: 0; filter: blur(6px); transform: scale(0.5); }
|
|
1916
|
+
}
|
|
1917
|
+
@keyframes cancia-fade-in {
|
|
1918
|
+
from { opacity: 0; transform: scale(0.94) translateY(5px); }
|
|
1919
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
1920
|
+
}
|
|
1921
|
+
@keyframes cancia-popup-in {
|
|
1922
|
+
from { opacity: 0; transform: scale(0.93); }
|
|
1923
|
+
to { opacity: 1; transform: scale(1); }
|
|
1924
|
+
}
|
|
1925
|
+
@keyframes cancia-badge-pop {
|
|
1926
|
+
0% { transform: scale(0); }
|
|
1927
|
+
60% { transform: scale(1.25); }
|
|
1928
|
+
100% { transform: scale(1); }
|
|
1929
|
+
}
|
|
1930
|
+
@keyframes cancia-icon-slide-in {
|
|
1931
|
+
from { transform: translateY(-150%); }
|
|
1932
|
+
to { transform: translateY(0); }
|
|
1933
|
+
}
|
|
1934
|
+
@keyframes cancia-tooltip-in {
|
|
1935
|
+
from { opacity: 0; transform: translateX(-50%) translateY(4px); }
|
|
1936
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
1937
|
+
}
|
|
1938
|
+
[data-cancia-toolbar] * { box-sizing: border-box; }
|
|
1939
|
+
[data-cancia-toolbar] button:active:not(:disabled) { transform: scale(0.92) !important; }
|
|
1940
|
+
[data-cancia-popup] * { box-sizing: border-box; }
|
|
1941
|
+
[data-cancia-popup] button:active:not(:disabled) { transform: scale(0.94) !important; }
|
|
1942
|
+
/* Protect stroke-based icons from host page "svg { fill: currentColor }" rules */
|
|
1943
|
+
[data-cancia-toolbar] svg[fill="none"] { fill: none !important; }
|
|
1944
|
+
[data-cancia-toolbar] svg[fill="none"] :not([fill]) { fill: none !important; }
|
|
1945
|
+
[data-cancia-popup] svg[fill="none"] { fill: none !important; }
|
|
1946
|
+
[data-cancia-popup] svg[fill="none"] :not([fill]) { fill: none !important; }
|
|
1947
|
+
/* Reset cosmetic host CSS leaking into toolbar buttons */
|
|
1948
|
+
[data-cancia-toolbar] :where(button) {
|
|
1949
|
+
background: unset; border: unset; border-radius: unset; padding: unset;
|
|
1950
|
+
margin: unset; color: unset; font-family: unset; font-weight: unset;
|
|
1951
|
+
font-size: unset; line-height: unset; letter-spacing: unset;
|
|
1952
|
+
box-shadow: unset; outline: unset; text-transform: unset;
|
|
1953
|
+
}
|
|
1954
|
+
`;
|
|
1955
|
+
document.head.appendChild(s);
|
|
1956
|
+
}
|
|
1957
|
+
var btnTooltipEl = null;
|
|
1958
|
+
var tooltipHideTimer = null;
|
|
1959
|
+
var tooltipShowTimer = null;
|
|
1960
|
+
var tooltipVisible = false;
|
|
1961
|
+
function getOrCreateBtnTooltip() {
|
|
1962
|
+
if (!btnTooltipEl) {
|
|
1963
|
+
btnTooltipEl = document.createElement("div");
|
|
1964
|
+
btnTooltipEl.style.cssText = `
|
|
1965
|
+
position: fixed;
|
|
1966
|
+
pointer-events: none;
|
|
1967
|
+
z-index: 2147483646;
|
|
1968
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1969
|
+
font-size: 11px; font-weight: 500; letter-spacing: 0.02em;
|
|
1970
|
+
color: #fff;
|
|
1971
|
+
background: rgba(10,10,12,0.92);
|
|
1972
|
+
backdrop-filter: blur(8px);
|
|
1973
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1974
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
1975
|
+
padding: 4px 8px; border-radius: 6px;
|
|
1976
|
+
white-space: nowrap;
|
|
1977
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
1978
|
+
display: none;
|
|
1979
|
+
`;
|
|
1980
|
+
document.body.appendChild(btnTooltipEl);
|
|
1981
|
+
}
|
|
1982
|
+
return btnTooltipEl;
|
|
1983
|
+
}
|
|
1984
|
+
function showBtnTooltip(btn, label) {
|
|
1985
|
+
if (tooltipHideTimer) {
|
|
1986
|
+
clearTimeout(tooltipHideTimer);
|
|
1987
|
+
tooltipHideTimer = null;
|
|
1988
|
+
}
|
|
1989
|
+
if (tooltipShowTimer) {
|
|
1990
|
+
clearTimeout(tooltipShowTimer);
|
|
1991
|
+
tooltipShowTimer = null;
|
|
1992
|
+
}
|
|
1993
|
+
const doShow = () => {
|
|
1994
|
+
tooltipVisible = true;
|
|
1995
|
+
const tooltip = getOrCreateBtnTooltip();
|
|
1996
|
+
tooltip.textContent = label;
|
|
1997
|
+
tooltip.style.display = "block";
|
|
1998
|
+
tooltip.style.animation = "cancia-tooltip-in 0.12s cubic-bezier(0.16,1,0.3,1) both";
|
|
1999
|
+
const rect = btn.getBoundingClientRect();
|
|
2000
|
+
const tooltipH = 26;
|
|
2001
|
+
const gap = 8;
|
|
2002
|
+
tooltip.style.top = `${rect.top - tooltipH - gap}px`;
|
|
2003
|
+
tooltip.style.left = `${rect.left + rect.width / 2}px`;
|
|
2004
|
+
};
|
|
2005
|
+
if (tooltipVisible) {
|
|
2006
|
+
doShow();
|
|
2007
|
+
} else {
|
|
2008
|
+
tooltipShowTimer = setTimeout(doShow, 400);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
function hideBtnTooltip() {
|
|
2012
|
+
if (tooltipShowTimer) {
|
|
2013
|
+
clearTimeout(tooltipShowTimer);
|
|
2014
|
+
tooltipShowTimer = null;
|
|
2015
|
+
}
|
|
2016
|
+
if (tooltipHideTimer) clearTimeout(tooltipHideTimer);
|
|
2017
|
+
tooltipHideTimer = setTimeout(() => {
|
|
2018
|
+
tooltipVisible = false;
|
|
2019
|
+
if (btnTooltipEl) btnTooltipEl.style.display = "none";
|
|
2020
|
+
}, 80);
|
|
2021
|
+
}
|
|
2022
|
+
function buildToolbar() {
|
|
2023
|
+
injectStyles4();
|
|
2024
|
+
const bar = document.createElement("div");
|
|
2025
|
+
bar.dataset.canciaToolbar = "1";
|
|
2026
|
+
bar.style.cssText = `
|
|
2027
|
+
position: fixed;
|
|
2028
|
+
bottom: 24px;
|
|
2029
|
+
right: 24px;
|
|
2030
|
+
z-index: 2147483647;
|
|
2031
|
+
width: 44px;
|
|
2032
|
+
height: 44px;
|
|
2033
|
+
border-radius: 22px;
|
|
2034
|
+
background: rgba(12, 12, 14, 0.92);
|
|
2035
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
2036
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
2037
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
2038
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3), 0 8px 32px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.05);
|
|
2039
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2040
|
+
font-size: 13px;
|
|
2041
|
+
color: #f0f0f0;
|
|
2042
|
+
user-select: none;
|
|
2043
|
+
cursor: pointer;
|
|
2044
|
+
overflow: hidden;
|
|
2045
|
+
display: flex;
|
|
2046
|
+
align-items: center;
|
|
2047
|
+
justify-content: center;
|
|
2048
|
+
transition: width 0.45s cubic-bezier(0.19, 1, 0.22, 1), border-radius 0.45s cubic-bezier(0.19, 1, 0.22, 1);
|
|
2049
|
+
animation: cancia-enter 0.5s cubic-bezier(0.34, 1.2, 0.64, 1) both;
|
|
2050
|
+
`;
|
|
2051
|
+
const collapseIcon = document.createElement("div");
|
|
2052
|
+
collapseIcon.style.cssText = `
|
|
2053
|
+
position: absolute;
|
|
2054
|
+
display: flex; align-items: center; justify-content: center;
|
|
2055
|
+
color: rgba(255,255,255,0.7);
|
|
2056
|
+
transition: opacity 0.15s, transform 0.15s cubic-bezier(0.2,0,0,1);
|
|
2057
|
+
pointer-events: none;
|
|
2058
|
+
`;
|
|
2059
|
+
collapseIcon.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
2060
|
+
<path d="M12 20h9"/>
|
|
2061
|
+
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
|
2062
|
+
</svg>`;
|
|
2063
|
+
const controls = document.createElement("div");
|
|
2064
|
+
controls.style.cssText = `
|
|
2065
|
+
display: flex;
|
|
2066
|
+
align-items: center;
|
|
2067
|
+
gap: 0.375rem;
|
|
2068
|
+
padding: 5px;
|
|
2069
|
+
white-space: nowrap;
|
|
2070
|
+
opacity: 0;
|
|
2071
|
+
pointer-events: none;
|
|
2072
|
+
transform-origin: right center;
|
|
2073
|
+
`;
|
|
2074
|
+
let editActive = false;
|
|
2075
|
+
const editBtn = makeIconButton(
|
|
2076
|
+
`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
2077
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
|
2078
|
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
2079
|
+
</svg>`,
|
|
2080
|
+
"Edit",
|
|
2081
|
+
() => toggleEditMode()
|
|
2082
|
+
);
|
|
2083
|
+
controls.appendChild(editBtn);
|
|
2084
|
+
const hasHook = state.config?.hasDeployHook ?? true;
|
|
2085
|
+
const publishBtn = makeIconButton(
|
|
2086
|
+
`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
2087
|
+
<path d="M22 2L11 13"/>
|
|
2088
|
+
<path d="M22 2L15 22l-4-9-9-4 20-7z"/>
|
|
2089
|
+
</svg>`,
|
|
2090
|
+
hasHook ? "Publish" : "Publish (no deploy hook configured)",
|
|
2091
|
+
() => handlePublish(publishBtn)
|
|
2092
|
+
);
|
|
2093
|
+
if (!hasHook) {
|
|
2094
|
+
publishBtn.disabled = true;
|
|
2095
|
+
publishBtn.style.opacity = "0.3";
|
|
2096
|
+
publishBtn.style.cursor = "not-allowed";
|
|
2097
|
+
}
|
|
2098
|
+
controls.appendChild(publishBtn);
|
|
2099
|
+
const logoutBtn = makeIconButton(
|
|
2100
|
+
`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
2101
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
|
2102
|
+
<path d="M16 17l5-5-5-5"/>
|
|
2103
|
+
<path d="M21 12H9"/>
|
|
2104
|
+
</svg>`,
|
|
2105
|
+
"Log out",
|
|
2106
|
+
() => state.onLogout?.()
|
|
2107
|
+
);
|
|
2108
|
+
controls.appendChild(logoutBtn);
|
|
2109
|
+
controls.appendChild(makeDivider());
|
|
2110
|
+
const collapseBtn = makeIconButton(
|
|
2111
|
+
`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
|
2112
|
+
<path d="M6 6l12 12M18 6L6 18"/>
|
|
2113
|
+
</svg>`,
|
|
2114
|
+
"Close",
|
|
2115
|
+
() => collapse(bar, collapseIcon, controls)
|
|
2116
|
+
);
|
|
2117
|
+
controls.appendChild(collapseBtn);
|
|
2118
|
+
bar.appendChild(collapseIcon);
|
|
2119
|
+
bar.appendChild(controls);
|
|
2120
|
+
bar.addEventListener("click", () => {
|
|
2121
|
+
if (!isExpanded) expand(bar, collapseIcon, controls);
|
|
2122
|
+
});
|
|
2123
|
+
bar.addEventListener("mouseenter", () => {
|
|
2124
|
+
if (!isExpanded) bar.style.background = "rgba(24, 24, 28, 0.96)";
|
|
2125
|
+
});
|
|
2126
|
+
bar.addEventListener("mouseleave", () => {
|
|
2127
|
+
bar.style.background = "rgba(12, 12, 14, 0.92)";
|
|
2128
|
+
});
|
|
2129
|
+
onPendingChange(() => {
|
|
2130
|
+
const count = state.pending.size;
|
|
2131
|
+
if (count > 0) {
|
|
2132
|
+
showPendingPanel(count);
|
|
2133
|
+
} else {
|
|
2134
|
+
hidePendingPanel();
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
let editModeEscListener = null;
|
|
2138
|
+
function toggleEditMode() {
|
|
2139
|
+
editActive = !editActive;
|
|
2140
|
+
state.editMode = editActive;
|
|
2141
|
+
const svgEl = editBtn.querySelector("svg");
|
|
2142
|
+
if (editActive) {
|
|
2143
|
+
editBtn.style.background = `${accent5()}22`;
|
|
2144
|
+
editBtn.style.color = accent5();
|
|
2145
|
+
if (svgEl) svgEl.style.stroke = accent5();
|
|
2146
|
+
editBtn.dataset.canciaTooltip = "Stop editing (Esc)";
|
|
2147
|
+
attachHighlight((selection) => {
|
|
2148
|
+
if (selection.kind === "field") {
|
|
2149
|
+
openPopup(selection.key, selection.fieldType, selection.el, () => {
|
|
2150
|
+
});
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const schema = state.schemas[selection.listName];
|
|
2154
|
+
if (!schema) {
|
|
2155
|
+
console.warn(`[cancia] No schema found for list "${selection.listName}". Define it in src/cms/schemas.ts.`);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
openListPanel({
|
|
2159
|
+
schema,
|
|
2160
|
+
onAddEntry: (locale) => {
|
|
2161
|
+
openEntryModal({
|
|
2162
|
+
schema,
|
|
2163
|
+
locale,
|
|
2164
|
+
entry: null,
|
|
2165
|
+
onSaved: () => {
|
|
2166
|
+
refreshListPanel();
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
},
|
|
2170
|
+
onEditEntry: (entry, locale) => {
|
|
2171
|
+
openEntryModal({
|
|
2172
|
+
schema,
|
|
2173
|
+
locale,
|
|
2174
|
+
entry,
|
|
2175
|
+
onSaved: () => {
|
|
2176
|
+
refreshListPanel();
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
},
|
|
2180
|
+
onTranslateEntry: (id, sourceEntry, targetLocale) => {
|
|
2181
|
+
openEntryModal({
|
|
2182
|
+
schema,
|
|
2183
|
+
locale: targetLocale,
|
|
2184
|
+
entry: null,
|
|
2185
|
+
translateFromEntry: sourceEntry,
|
|
2186
|
+
fixedId: id,
|
|
2187
|
+
onSaved: () => {
|
|
2188
|
+
refreshListPanel();
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
editModeEscListener = (e) => {
|
|
2195
|
+
if (e.key !== "Escape") return;
|
|
2196
|
+
if (isEntryModalOpen()) {
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
if (isListPanelOpen()) {
|
|
2200
|
+
closeListPanel();
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
if (!document.querySelector("[data-cancia-popup]")) {
|
|
2204
|
+
toggleEditMode();
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
document.addEventListener("keydown", editModeEscListener, true);
|
|
2208
|
+
} else {
|
|
2209
|
+
editBtn.style.background = "transparent";
|
|
2210
|
+
editBtn.style.color = "rgba(255,255,255,0.85)";
|
|
2211
|
+
if (svgEl) svgEl.style.stroke = "";
|
|
2212
|
+
editBtn.dataset.canciaTooltip = "Edit";
|
|
2213
|
+
detachHighlight();
|
|
2214
|
+
closePopup();
|
|
2215
|
+
closeEntryModal();
|
|
2216
|
+
closeListPanel();
|
|
2217
|
+
if (editModeEscListener) {
|
|
2218
|
+
document.removeEventListener("keydown", editModeEscListener, true);
|
|
2219
|
+
editModeEscListener = null;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
return bar;
|
|
2224
|
+
}
|
|
2225
|
+
function expand(bar, icon, controls) {
|
|
2226
|
+
if (isExpanded) return;
|
|
2227
|
+
isExpanded = true;
|
|
2228
|
+
expandedEscListener = (e) => {
|
|
2229
|
+
if (e.key === "Escape" && !document.querySelector("[data-cancia-popup]") && !state.editMode) {
|
|
2230
|
+
collapse(bar, icon, controls);
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
document.addEventListener("keydown", expandedEscListener, true);
|
|
2234
|
+
controls.style.visibility = "hidden";
|
|
2235
|
+
controls.style.opacity = "0";
|
|
2236
|
+
controls.style.pointerEvents = "none";
|
|
2237
|
+
bar.style.width = "max-content";
|
|
2238
|
+
bar.style.borderRadius = "100px";
|
|
2239
|
+
requestAnimationFrame(() => {
|
|
2240
|
+
const naturalW = bar.scrollWidth;
|
|
2241
|
+
bar.style.width = "44px";
|
|
2242
|
+
controls.style.visibility = "";
|
|
2243
|
+
requestAnimationFrame(() => {
|
|
2244
|
+
bar.style.width = `${naturalW}px`;
|
|
2245
|
+
bar.style.borderRadius = "100px";
|
|
2246
|
+
bar.style.cursor = "default";
|
|
2247
|
+
icon.style.opacity = "0";
|
|
2248
|
+
icon.style.transform = "scale(0.5) rotate(-90deg)";
|
|
2249
|
+
setTimeout(() => {
|
|
2250
|
+
controls.style.pointerEvents = "auto";
|
|
2251
|
+
controls.style.animation = "cancia-controls-in 0.4s cubic-bezier(0.19, 1, 0.22, 1) both";
|
|
2252
|
+
controls.style.opacity = "1";
|
|
2253
|
+
}, 80);
|
|
2254
|
+
});
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
function collapse(bar, icon, controls) {
|
|
2258
|
+
if (!isExpanded) return;
|
|
2259
|
+
isExpanded = false;
|
|
2260
|
+
hideBtnTooltip();
|
|
2261
|
+
if (expandedEscListener) {
|
|
2262
|
+
document.removeEventListener("keydown", expandedEscListener, true);
|
|
2263
|
+
expandedEscListener = null;
|
|
2264
|
+
}
|
|
2265
|
+
controls.style.pointerEvents = "none";
|
|
2266
|
+
controls.style.animation = "cancia-controls-out 0.15s cubic-bezier(0.4, 0, 1, 1) both";
|
|
2267
|
+
setTimeout(() => {
|
|
2268
|
+
controls.style.opacity = "0";
|
|
2269
|
+
bar.style.width = "44px";
|
|
2270
|
+
bar.style.borderRadius = "22px";
|
|
2271
|
+
bar.style.cursor = "pointer";
|
|
2272
|
+
icon.style.opacity = "1";
|
|
2273
|
+
icon.style.transform = "scale(1) rotate(0deg)";
|
|
2274
|
+
}, 100);
|
|
2275
|
+
}
|
|
2276
|
+
async function handleSave(btn) {
|
|
2277
|
+
if (btn) {
|
|
2278
|
+
btn.disabled = true;
|
|
2279
|
+
btn.style.opacity = "0.5";
|
|
2280
|
+
}
|
|
2281
|
+
try {
|
|
2282
|
+
await flushPending();
|
|
2283
|
+
flashPanelMessage("Saved", "success");
|
|
2284
|
+
} catch (err) {
|
|
2285
|
+
console.error(err);
|
|
2286
|
+
if (isAuthError(err)) {
|
|
2287
|
+
unmountToolbar();
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
if (btn) {
|
|
2291
|
+
btn.disabled = false;
|
|
2292
|
+
btn.style.opacity = "1";
|
|
2293
|
+
}
|
|
2294
|
+
flashPanelMessage("Save failed", "error");
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
async function handlePublish(btn) {
|
|
2298
|
+
btn.disabled = true;
|
|
2299
|
+
btn.style.opacity = "0.5";
|
|
2300
|
+
try {
|
|
2301
|
+
if (state.pending.size > 0) await flushPending();
|
|
2302
|
+
await triggerPublish();
|
|
2303
|
+
showToast("Published!", "success");
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
console.error(err);
|
|
2306
|
+
showToast("Publish failed", "error");
|
|
2307
|
+
} finally {
|
|
2308
|
+
btn.disabled = false;
|
|
2309
|
+
btn.style.opacity = "1";
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
function showToast(message, type) {
|
|
2313
|
+
const toast = document.createElement("div");
|
|
2314
|
+
const color = type === "success" ? "#4ade80" : "#f87171";
|
|
2315
|
+
toast.style.cssText = `
|
|
2316
|
+
position: fixed; bottom: 80px; right: 24px; z-index: 2147483647;
|
|
2317
|
+
display: flex; align-items: center; gap: 8px;
|
|
2318
|
+
background: rgba(12, 12, 14, 0.95);
|
|
2319
|
+
backdrop-filter: blur(16px);
|
|
2320
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
2321
|
+
border-radius: 10px; padding: 10px 14px;
|
|
2322
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2323
|
+
font-size: 13px; font-weight: 500; color: #f0f0f0;
|
|
2324
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
|
|
2325
|
+
pointer-events: none;
|
|
2326
|
+
animation: cancia-fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
2327
|
+
`;
|
|
2328
|
+
const dot = document.createElement("span");
|
|
2329
|
+
dot.style.cssText = `width: 7px; height: 7px; border-radius: 50%; background: ${color}; flex-shrink: 0;`;
|
|
2330
|
+
const label = document.createElement("span");
|
|
2331
|
+
label.textContent = message;
|
|
2332
|
+
toast.appendChild(dot);
|
|
2333
|
+
toast.appendChild(label);
|
|
2334
|
+
document.body.appendChild(toast);
|
|
2335
|
+
setTimeout(() => {
|
|
2336
|
+
toast.style.transition = "opacity 0.25s cubic-bezier(0.4,0,1,1), transform 0.25s cubic-bezier(0.4,0,1,1)";
|
|
2337
|
+
toast.style.opacity = "0";
|
|
2338
|
+
toast.style.transform = "translateY(4px)";
|
|
2339
|
+
setTimeout(() => toast.remove(), 300);
|
|
2340
|
+
}, 2e3);
|
|
2341
|
+
}
|
|
2342
|
+
function makeIconButton(svg, title, onClick) {
|
|
2343
|
+
const btn = document.createElement("button");
|
|
2344
|
+
btn.style.cssText = `
|
|
2345
|
+
display: flex; align-items: center; justify-content: center;
|
|
2346
|
+
width: 34px; height: 34px; border-radius: 50%;
|
|
2347
|
+
border: none; background: transparent;
|
|
2348
|
+
cursor: pointer; color: rgba(255,255,255,0.85); flex-shrink: 0; padding: 0;
|
|
2349
|
+
transition: color 0.15s, background 0.15s, transform 0.1s cubic-bezier(0.2,0,0,1);
|
|
2350
|
+
`;
|
|
2351
|
+
btn.innerHTML = svg;
|
|
2352
|
+
const svgEl = btn.querySelector("svg");
|
|
2353
|
+
if (svgEl) {
|
|
2354
|
+
svgEl.style.cssText = "display:block;flex-shrink:0;overflow:visible;margin:auto;";
|
|
2355
|
+
svgEl.setAttribute("stroke-width", "1.5");
|
|
2356
|
+
}
|
|
2357
|
+
btn.dataset.canciaTooltip = title;
|
|
2358
|
+
btn.addEventListener("mouseenter", () => {
|
|
2359
|
+
if (!btn.disabled) {
|
|
2360
|
+
btn.style.background = "rgba(255,255,255,0.1)";
|
|
2361
|
+
showBtnTooltip(btn, btn.dataset.canciaTooltip ?? title);
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
btn.addEventListener("mouseleave", () => {
|
|
2365
|
+
const activeColor = state.config?.accentColor ?? "#6366f1";
|
|
2366
|
+
if (!btn.style.background.includes(activeColor.slice(1, 7))) {
|
|
2367
|
+
btn.style.background = "transparent";
|
|
2368
|
+
}
|
|
2369
|
+
hideBtnTooltip();
|
|
2370
|
+
});
|
|
2371
|
+
btn.addEventListener("click", (e) => {
|
|
2372
|
+
e.stopPropagation();
|
|
2373
|
+
hideBtnTooltip();
|
|
2374
|
+
onClick();
|
|
2375
|
+
});
|
|
2376
|
+
return btn;
|
|
2377
|
+
}
|
|
2378
|
+
function makeDivider() {
|
|
2379
|
+
const d = document.createElement("span");
|
|
2380
|
+
d.style.cssText = `width: 1px; height: 14px; background: rgba(255,255,255,0.08); flex-shrink: 0; margin: 0 1px;`;
|
|
2381
|
+
return d;
|
|
2382
|
+
}
|
|
2383
|
+
function showPendingPanel(count) {
|
|
2384
|
+
if (!pendingPanelEl) {
|
|
2385
|
+
pendingPanelEl = document.createElement("div");
|
|
2386
|
+
pendingPanelEl.style.cssText = `
|
|
2387
|
+
position: fixed;
|
|
2388
|
+
bottom: 80px;
|
|
2389
|
+
right: 24px;
|
|
2390
|
+
z-index: 2147483646;
|
|
2391
|
+
display: flex;
|
|
2392
|
+
align-items: center;
|
|
2393
|
+
justify-content: space-between;
|
|
2394
|
+
gap: 12px;
|
|
2395
|
+
background: rgba(12, 12, 14, 0.92);
|
|
2396
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
2397
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
2398
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
2399
|
+
border-radius: 12px;
|
|
2400
|
+
padding: 0;
|
|
2401
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3), 0 8px 32px rgba(0,0,0,0.4);
|
|
2402
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2403
|
+
animation: cancia-fade-in 0.25s cubic-bezier(0.16,1,0.3,1) both;
|
|
2404
|
+
width: max-content;
|
|
2405
|
+
overflow: hidden;
|
|
2406
|
+
`;
|
|
2407
|
+
const label2 = document.createElement("span");
|
|
2408
|
+
label2.dataset.canciaPendingLabel = "1";
|
|
2409
|
+
label2.style.cssText = `font-size: 12px; font-weight: 500; color: rgba(255,255,255,0.5); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`;
|
|
2410
|
+
const saveBtn = document.createElement("button");
|
|
2411
|
+
saveBtn.dataset.canciaSaveBtn = "1";
|
|
2412
|
+
saveBtn.title = "Save changes";
|
|
2413
|
+
saveBtn.style.cssText = `
|
|
2414
|
+
padding: 5px 10px; border-radius: 7px; border: none; cursor: pointer;
|
|
2415
|
+
background: #fff; color: #0c0c0e;
|
|
2416
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2417
|
+
font-size: 12px; font-weight: 600; letter-spacing: 0.01em;
|
|
2418
|
+
transition: opacity 0.15s, transform 0.1s cubic-bezier(0.2,0,0,1);
|
|
2419
|
+
flex-shrink: 0;
|
|
2420
|
+
`;
|
|
2421
|
+
saveBtn.textContent = "Save";
|
|
2422
|
+
saveBtn.addEventListener("mouseenter", () => {
|
|
2423
|
+
saveBtn.style.opacity = "0.85";
|
|
2424
|
+
});
|
|
2425
|
+
saveBtn.addEventListener("mouseleave", () => {
|
|
2426
|
+
saveBtn.style.opacity = "1";
|
|
2427
|
+
});
|
|
2428
|
+
saveBtn.addEventListener("click", (e) => {
|
|
2429
|
+
e.stopPropagation();
|
|
2430
|
+
handleSave(saveBtn);
|
|
2431
|
+
});
|
|
2432
|
+
const undoBtn = document.createElement("button");
|
|
2433
|
+
undoBtn.title = "Discard changes";
|
|
2434
|
+
undoBtn.style.cssText = `
|
|
2435
|
+
padding: 5px 10px; border-radius: 7px; border: 1px solid rgba(255,255,255,0.1); cursor: pointer;
|
|
2436
|
+
background: transparent; color: rgba(255,255,255,0.5);
|
|
2437
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2438
|
+
font-size: 12px; font-weight: 500; letter-spacing: 0.01em;
|
|
2439
|
+
transition: color 0.15s, border-color 0.15s;
|
|
2440
|
+
flex-shrink: 0;
|
|
2441
|
+
`;
|
|
2442
|
+
undoBtn.textContent = "Discard";
|
|
2443
|
+
undoBtn.addEventListener("mouseenter", () => {
|
|
2444
|
+
undoBtn.style.color = "rgba(255,255,255,0.8)";
|
|
2445
|
+
undoBtn.style.borderColor = "rgba(255,255,255,0.2)";
|
|
2446
|
+
});
|
|
2447
|
+
undoBtn.addEventListener("mouseleave", () => {
|
|
2448
|
+
undoBtn.style.color = "rgba(255,255,255,0.5)";
|
|
2449
|
+
undoBtn.style.borderColor = "rgba(255,255,255,0.1)";
|
|
2450
|
+
});
|
|
2451
|
+
undoBtn.addEventListener("click", (e) => {
|
|
2452
|
+
e.stopPropagation();
|
|
2453
|
+
revertPending();
|
|
2454
|
+
closePopup();
|
|
2455
|
+
hidePendingPanel();
|
|
2456
|
+
});
|
|
2457
|
+
const slot = document.createElement("div");
|
|
2458
|
+
slot.dataset.canciaPanelSlot = "1";
|
|
2459
|
+
slot.style.cssText = `display:grid;place-items:center;overflow:hidden;`;
|
|
2460
|
+
const row = document.createElement("div");
|
|
2461
|
+
row.dataset.canciaPanelRow = "1";
|
|
2462
|
+
row.style.cssText = `
|
|
2463
|
+
grid-area: 1/1; display: flex; align-items: center; gap: 6px;
|
|
2464
|
+
padding: 7px 7px 7px 12px;
|
|
2465
|
+
transition: transform 200ms cubic-bezier(0.785,0.135,0.15,0.86);
|
|
2466
|
+
`;
|
|
2467
|
+
row.appendChild(label2);
|
|
2468
|
+
row.appendChild(undoBtn);
|
|
2469
|
+
row.appendChild(saveBtn);
|
|
2470
|
+
const flash = document.createElement("div");
|
|
2471
|
+
flash.dataset.canciaPanelFlash = "1";
|
|
2472
|
+
flash.style.cssText = `
|
|
2473
|
+
grid-area: 1/1; display: flex; align-items: center; gap: 7px;
|
|
2474
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2475
|
+
font-size: 12px; font-weight: 500; color: rgba(255,255,255,0.8); white-space: nowrap;
|
|
2476
|
+
padding: 7px 12px;
|
|
2477
|
+
transform: translateY(-150%);
|
|
2478
|
+
transition: transform 200ms cubic-bezier(0.785,0.135,0.15,0.86);
|
|
2479
|
+
`;
|
|
2480
|
+
slot.appendChild(row);
|
|
2481
|
+
slot.appendChild(flash);
|
|
2482
|
+
pendingPanelEl.appendChild(slot);
|
|
2483
|
+
document.body.appendChild(pendingPanelEl);
|
|
2484
|
+
}
|
|
2485
|
+
const label = pendingPanelEl.querySelector("[data-cancia-pending-label]");
|
|
2486
|
+
if (label) label.textContent = `${count} unsaved change${count === 1 ? "" : "s"}`;
|
|
2487
|
+
}
|
|
2488
|
+
function flashPanelMessage(message, type) {
|
|
2489
|
+
if (!pendingPanelEl) return;
|
|
2490
|
+
const color = type === "success" ? "#4ade80" : "#f87171";
|
|
2491
|
+
const row = pendingPanelEl.querySelector("[data-cancia-panel-row]");
|
|
2492
|
+
const flash = pendingPanelEl.querySelector("[data-cancia-panel-flash]");
|
|
2493
|
+
if (!row || !flash) return;
|
|
2494
|
+
flash.innerHTML = `<span style="width:7px;height:7px;border-radius:50%;background:${color};flex-shrink:0;display:block;"></span>${message}`;
|
|
2495
|
+
row.style.transform = "translateY(150%)";
|
|
2496
|
+
flash.style.transform = "translateY(0)";
|
|
2497
|
+
setTimeout(() => hidePendingPanel(), 1600);
|
|
2498
|
+
}
|
|
2499
|
+
function hidePendingPanel() {
|
|
2500
|
+
if (!pendingPanelEl) return;
|
|
2501
|
+
const panel = pendingPanelEl;
|
|
2502
|
+
pendingPanelEl = null;
|
|
2503
|
+
panel.style.transition = "opacity 0.2s cubic-bezier(0.4,0,1,1), transform 0.2s cubic-bezier(0.4,0,1,1)";
|
|
2504
|
+
panel.style.opacity = "0";
|
|
2505
|
+
panel.style.transform = "translateY(4px)";
|
|
2506
|
+
setTimeout(() => panel.remove(), 220);
|
|
2507
|
+
}
|
|
2508
|
+
function mountToolbar() {
|
|
2509
|
+
if (toolbarEl) return;
|
|
2510
|
+
isExpanded = false;
|
|
2511
|
+
toolbarEl = buildToolbar();
|
|
2512
|
+
document.body.appendChild(toolbarEl);
|
|
2513
|
+
}
|
|
2514
|
+
function unmountToolbar() {
|
|
2515
|
+
detachHighlight();
|
|
2516
|
+
closePopup();
|
|
2517
|
+
isExpanded = false;
|
|
2518
|
+
btnTooltipEl?.remove();
|
|
2519
|
+
btnTooltipEl = null;
|
|
2520
|
+
pendingPanelEl?.remove();
|
|
2521
|
+
pendingPanelEl = null;
|
|
2522
|
+
if (toolbarEl) {
|
|
2523
|
+
toolbarEl.style.animation = "cancia-exit 0.25s cubic-bezier(0.4, 0, 1, 1) both";
|
|
2524
|
+
setTimeout(() => {
|
|
2525
|
+
toolbarEl?.remove();
|
|
2526
|
+
toolbarEl = null;
|
|
2527
|
+
}, 260);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// src/index.ts
|
|
2532
|
+
var SESSION_KEY = "cancia_session";
|
|
2533
|
+
function getSessionToken() {
|
|
2534
|
+
try {
|
|
2535
|
+
return sessionStorage.getItem(SESSION_KEY);
|
|
2536
|
+
} catch {
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
function setSessionToken(token) {
|
|
2541
|
+
try {
|
|
2542
|
+
sessionStorage.setItem(SESSION_KEY, token);
|
|
2543
|
+
} catch {
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
function clearSession() {
|
|
2547
|
+
try {
|
|
2548
|
+
sessionStorage.removeItem(SESSION_KEY);
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
async function handleMagicUrl() {
|
|
2553
|
+
const params = new URLSearchParams(window.location.search);
|
|
2554
|
+
const token = params.get("cancia");
|
|
2555
|
+
if (!token) return null;
|
|
2556
|
+
params.delete("cancia");
|
|
2557
|
+
const newSearch = params.toString();
|
|
2558
|
+
const cleanUrl = window.location.pathname + (newSearch ? `?${newSearch}` : "") + window.location.hash;
|
|
2559
|
+
window.history.replaceState(null, "", cleanUrl);
|
|
2560
|
+
try {
|
|
2561
|
+
const res = await fetch("/api/cancia/auth", {
|
|
2562
|
+
method: "POST",
|
|
2563
|
+
headers: { "Content-Type": "application/json" },
|
|
2564
|
+
body: JSON.stringify({ token })
|
|
2565
|
+
});
|
|
2566
|
+
if (res.ok) {
|
|
2567
|
+
setSessionToken(token);
|
|
2568
|
+
return token;
|
|
2569
|
+
}
|
|
2570
|
+
} catch {
|
|
2571
|
+
}
|
|
2572
|
+
console.warn("Cancia: magic link token is invalid or expired.");
|
|
2573
|
+
return null;
|
|
2574
|
+
}
|
|
2575
|
+
async function init(config) {
|
|
2576
|
+
state.config = config;
|
|
2577
|
+
state.activeLang = config.languages[0];
|
|
2578
|
+
if (window.__CANCIA_DATA__) {
|
|
2579
|
+
state.cmsData = window.__CANCIA_DATA__;
|
|
2580
|
+
}
|
|
2581
|
+
try {
|
|
2582
|
+
const fresh = await fetchContent();
|
|
2583
|
+
state.cmsData = fresh;
|
|
2584
|
+
} catch (err) {
|
|
2585
|
+
if (!config.public && err instanceof Error && err.message.includes("401")) {
|
|
2586
|
+
clearSession();
|
|
2587
|
+
console.warn("Cancia: session expired or invalid, toolbar not mounted.");
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
console.warn("Cancia: could not fetch CMS content, using preloaded data.");
|
|
2591
|
+
}
|
|
2592
|
+
applyOverlay();
|
|
2593
|
+
try {
|
|
2594
|
+
state.schemas = await fetchSchemas();
|
|
2595
|
+
} catch {
|
|
2596
|
+
state.schemas = {};
|
|
2597
|
+
}
|
|
2598
|
+
mountToolbar();
|
|
2599
|
+
}
|
|
2600
|
+
async function tryAutoInit() {
|
|
2601
|
+
if (!window.__CANCIA__) return;
|
|
2602
|
+
if (window.__CANCIA__.public) {
|
|
2603
|
+
init(window.__CANCIA__);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
const token = await handleMagicUrl() ?? getSessionToken();
|
|
2607
|
+
if (!token) return;
|
|
2608
|
+
state.sessionToken = token;
|
|
2609
|
+
state.onLogout = () => {
|
|
2610
|
+
clearSession();
|
|
2611
|
+
unmountToolbar();
|
|
2612
|
+
};
|
|
2613
|
+
init(window.__CANCIA__);
|
|
2614
|
+
}
|
|
2615
|
+
if (typeof document !== "undefined") {
|
|
2616
|
+
if (document.readyState === "loading") {
|
|
2617
|
+
document.addEventListener("DOMContentLoaded", tryAutoInit);
|
|
2618
|
+
} else {
|
|
2619
|
+
tryAutoInit();
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
export {
|
|
2623
|
+
clearSession,
|
|
2624
|
+
unmountToolbar as destroy,
|
|
2625
|
+
getValue,
|
|
2626
|
+
init,
|
|
2627
|
+
onPendingChange
|
|
2628
|
+
};
|
|
2629
|
+
//# sourceMappingURL=cancia.js.map
|