starmark 1.0.2 → 1.0.3
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/package.json +1 -1
- package/public/app.js +1 -0
- package/public/frontmatter-editor.js +223 -22
- package/public/media-browser.js +464 -0
- package/public/styles.css +54 -5
- package/public/tools/31-image.js +25 -446
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { icons } from "./icons.js";
|
|
2
|
+
|
|
3
|
+
const IMAGE_UPLOAD_ACCEPT = ".png,.jpg,.jpeg,.gif,.webp,.svg,.avif,.ico,image/*";
|
|
4
|
+
const IMAGE_UPLOAD_EXTENSIONS = new Set([
|
|
5
|
+
".png",
|
|
6
|
+
".jpg",
|
|
7
|
+
".jpeg",
|
|
8
|
+
".gif",
|
|
9
|
+
".webp",
|
|
10
|
+
".svg",
|
|
11
|
+
".avif",
|
|
12
|
+
".ico",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function createMediaDialog({
|
|
16
|
+
getProjectPath,
|
|
17
|
+
title = "Media",
|
|
18
|
+
selectMode = "path",
|
|
19
|
+
onSelect,
|
|
20
|
+
onInsert,
|
|
21
|
+
onOpen,
|
|
22
|
+
onClose,
|
|
23
|
+
dialogId = "media-dialog",
|
|
24
|
+
}) {
|
|
25
|
+
let pendingImage = null;
|
|
26
|
+
|
|
27
|
+
const dialog = document.createElement("dialog");
|
|
28
|
+
dialog.id = dialogId;
|
|
29
|
+
dialog.className = "media-dialog";
|
|
30
|
+
dialog.innerHTML = `
|
|
31
|
+
<div class="dialog-header">
|
|
32
|
+
<h2></h2>
|
|
33
|
+
<button type="button" class="dialog-close media-dialog-close" aria-label="Close">×</button>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="media-browser">
|
|
36
|
+
<div class="media-toolbar">
|
|
37
|
+
<button type="button" class="media-up-btn" disabled aria-label="Go up one folder">
|
|
38
|
+
${icons.arrowUp}
|
|
39
|
+
Up
|
|
40
|
+
</button>
|
|
41
|
+
<nav class="media-breadcrumb" aria-label="Media path"></nav>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="media-search">
|
|
44
|
+
<input
|
|
45
|
+
type="search"
|
|
46
|
+
class="media-search-input"
|
|
47
|
+
placeholder="Search by file name"
|
|
48
|
+
autocomplete="off"
|
|
49
|
+
spellcheck="false"
|
|
50
|
+
aria-label="Search images by file name"
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
<p class="media-status" hidden></p>
|
|
54
|
+
<div class="media-grid"></div>
|
|
55
|
+
</div>
|
|
56
|
+
<form class="image-form" hidden aria-hidden="true">
|
|
57
|
+
<p class="image-selected-name"></p>
|
|
58
|
+
<div class="link-field">
|
|
59
|
+
<label for="${dialogId}-image-alt">Alt text</label>
|
|
60
|
+
<input id="${dialogId}-image-alt" type="text" autocomplete="off" spellcheck="true" />
|
|
61
|
+
</div>
|
|
62
|
+
<label class="image-checkbox-field" for="${dialogId}-image-lazy">
|
|
63
|
+
<input id="${dialogId}-image-lazy" type="checkbox" checked />
|
|
64
|
+
Lazy load
|
|
65
|
+
</label>
|
|
66
|
+
<div class="link-field">
|
|
67
|
+
<label for="${dialogId}-image-caption">Caption</label>
|
|
68
|
+
<input id="${dialogId}-image-caption" type="text" autocomplete="off" spellcheck="true" />
|
|
69
|
+
</div>
|
|
70
|
+
<div class="link-form-actions image-form-actions">
|
|
71
|
+
<button type="button" class="image-back-btn">Back</button>
|
|
72
|
+
<button type="submit" class="primary">Done</button>
|
|
73
|
+
</div>
|
|
74
|
+
</form>
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
dialog.querySelector(".dialog-header h2").textContent = title;
|
|
78
|
+
|
|
79
|
+
const closeBtn = dialog.querySelector(".media-dialog-close");
|
|
80
|
+
const mediaUpBtn = dialog.querySelector(".media-up-btn");
|
|
81
|
+
const mediaBreadcrumb = dialog.querySelector(".media-breadcrumb");
|
|
82
|
+
const mediaSearchInput = dialog.querySelector(".media-search-input");
|
|
83
|
+
const mediaStatus = dialog.querySelector(".media-status");
|
|
84
|
+
const mediaGrid = dialog.querySelector(".media-grid");
|
|
85
|
+
const mediaBrowserView = dialog.querySelector(".media-browser");
|
|
86
|
+
const imageForm = dialog.querySelector(".image-form");
|
|
87
|
+
const imageSelectedName = dialog.querySelector(".image-selected-name");
|
|
88
|
+
const imageAltInput = dialog.querySelector(`#${dialogId}-image-alt`);
|
|
89
|
+
const imageLazyInput = dialog.querySelector(`#${dialogId}-image-lazy`);
|
|
90
|
+
const imageCaptionInput = dialog.querySelector(`#${dialogId}-image-caption`);
|
|
91
|
+
const imageBackBtn = dialog.querySelector(".image-back-btn");
|
|
92
|
+
|
|
93
|
+
let currentMediaDir = "img";
|
|
94
|
+
let isMediaUploading = false;
|
|
95
|
+
|
|
96
|
+
function formatMediaDirLabel(relativeDir) {
|
|
97
|
+
return relativeDir ? relativeDir.replace(/\//g, " / ") : "public";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getMediaFileUrl(relativePath) {
|
|
101
|
+
return `/api/media/file?project=${encodeURIComponent(getProjectPath())}&path=${encodeURIComponent(relativePath)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getImageSearchLabel(image, searchRootDir) {
|
|
105
|
+
if (image.dir === searchRootDir) {
|
|
106
|
+
return image.name;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const fullPath = image.dir ? `${image.dir}/${image.name}` : image.name;
|
|
110
|
+
const prefix = searchRootDir ? `${searchRootDir}/` : "";
|
|
111
|
+
|
|
112
|
+
if (searchRootDir && fullPath.startsWith(prefix)) {
|
|
113
|
+
return fullPath.slice(prefix.length);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return fullPath;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function navigateToMediaDirectory(relativeDir) {
|
|
120
|
+
mediaSearchInput.value = "";
|
|
121
|
+
loadMediaDirectory(relativeDir);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isImageUploadFile(file) {
|
|
125
|
+
const dotIndex = file.name.lastIndexOf(".");
|
|
126
|
+
if (dotIndex === -1) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const extension = file.name.slice(dotIndex).toLowerCase();
|
|
131
|
+
return IMAGE_UPLOAD_EXTENSIONS.has(extension);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createMediaAddItem() {
|
|
135
|
+
const label = document.createElement("label");
|
|
136
|
+
label.className = "media-item-btn media-add-btn";
|
|
137
|
+
|
|
138
|
+
const input = document.createElement("input");
|
|
139
|
+
input.type = "file";
|
|
140
|
+
input.className = "media-upload-input";
|
|
141
|
+
input.accept = IMAGE_UPLOAD_ACCEPT;
|
|
142
|
+
input.disabled = isMediaUploading;
|
|
143
|
+
|
|
144
|
+
const preview = document.createElement("span");
|
|
145
|
+
preview.className = "media-item-preview media-add-preview";
|
|
146
|
+
preview.innerHTML = icons.plus;
|
|
147
|
+
preview.setAttribute("aria-hidden", "true");
|
|
148
|
+
|
|
149
|
+
const name = document.createElement("span");
|
|
150
|
+
name.className = "media-item-name";
|
|
151
|
+
name.textContent = "Add";
|
|
152
|
+
|
|
153
|
+
label.append(input, preview, name);
|
|
154
|
+
label.title = "Upload image";
|
|
155
|
+
|
|
156
|
+
input.addEventListener("change", async () => {
|
|
157
|
+
const file = input.files?.[0];
|
|
158
|
+
input.value = "";
|
|
159
|
+
|
|
160
|
+
if (!file) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!isImageUploadFile(file)) {
|
|
165
|
+
setMediaStatus("Only image files are supported.", { isError: true });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
isMediaUploading = true;
|
|
170
|
+
input.disabled = true;
|
|
171
|
+
setMediaStatus("Uploading…");
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const params = new URLSearchParams({
|
|
175
|
+
project: getProjectPath(),
|
|
176
|
+
dir: currentMediaDir,
|
|
177
|
+
filename: file.name,
|
|
178
|
+
});
|
|
179
|
+
const response = await fetch(`/api/media/upload?${params.toString()}`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: {
|
|
182
|
+
"Content-Type": file.type || "application/octet-stream",
|
|
183
|
+
},
|
|
184
|
+
body: file,
|
|
185
|
+
});
|
|
186
|
+
const data = await response.json();
|
|
187
|
+
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
setMediaStatus(data.error ?? "Could not upload image", { isError: true });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await loadMediaDirectory(currentMediaDir);
|
|
194
|
+
} catch {
|
|
195
|
+
setMediaStatus("Could not upload image", { isError: true });
|
|
196
|
+
} finally {
|
|
197
|
+
isMediaUploading = false;
|
|
198
|
+
input.disabled = false;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return label;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function setMediaStatus(message, { isError = false } = {}) {
|
|
206
|
+
mediaStatus.hidden = !message;
|
|
207
|
+
mediaStatus.textContent = message ?? "";
|
|
208
|
+
mediaStatus.classList.toggle("is-error", Boolean(isError && message));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function renderMediaBreadcrumb(currentDir) {
|
|
212
|
+
mediaBreadcrumb.replaceChildren();
|
|
213
|
+
|
|
214
|
+
const segments = currentDir ? currentDir.split("/") : [];
|
|
215
|
+
const crumbs = [{ label: "public", dir: "" }];
|
|
216
|
+
|
|
217
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
218
|
+
crumbs.push({
|
|
219
|
+
label: segments[index],
|
|
220
|
+
dir: segments.slice(0, index + 1).join("/"),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
crumbs.forEach((crumb, index) => {
|
|
225
|
+
if (index > 0) {
|
|
226
|
+
const separator = document.createElement("span");
|
|
227
|
+
separator.className = "media-breadcrumb-separator";
|
|
228
|
+
separator.textContent = "/";
|
|
229
|
+
separator.setAttribute("aria-hidden", "true");
|
|
230
|
+
mediaBreadcrumb.append(separator);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const button = document.createElement("button");
|
|
234
|
+
button.type = "button";
|
|
235
|
+
button.textContent = crumb.label;
|
|
236
|
+
button.addEventListener("click", () => {
|
|
237
|
+
navigateToMediaDirectory(crumb.dir);
|
|
238
|
+
});
|
|
239
|
+
mediaBreadcrumb.append(button);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function showMediaBrowserView() {
|
|
244
|
+
pendingImage = null;
|
|
245
|
+
mediaBrowserView.hidden = false;
|
|
246
|
+
mediaBrowserView.setAttribute("aria-hidden", "false");
|
|
247
|
+
imageForm.hidden = true;
|
|
248
|
+
imageForm.setAttribute("aria-hidden", "true");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function showMediaDetailsView(image) {
|
|
252
|
+
pendingImage = image;
|
|
253
|
+
imageSelectedName.textContent = image.name;
|
|
254
|
+
imageAltInput.value = filenameToAlt(image.name);
|
|
255
|
+
imageLazyInput.checked = true;
|
|
256
|
+
imageCaptionInput.value = "";
|
|
257
|
+
mediaBrowserView.hidden = true;
|
|
258
|
+
mediaBrowserView.setAttribute("aria-hidden", "true");
|
|
259
|
+
imageForm.hidden = false;
|
|
260
|
+
imageForm.setAttribute("aria-hidden", "false");
|
|
261
|
+
imageAltInput.focus();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function handleImageSelection(image) {
|
|
265
|
+
if (selectMode === "path") {
|
|
266
|
+
onSelect?.(image);
|
|
267
|
+
dialog.close();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
showMediaDetailsView(image);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function renderMediaDirectory(data) {
|
|
275
|
+
const isSearching = Boolean(data.searchQuery);
|
|
276
|
+
|
|
277
|
+
mediaUpBtn.disabled = data.parentDir === null;
|
|
278
|
+
mediaUpBtn.onclick = () => {
|
|
279
|
+
if (data.parentDir !== null) {
|
|
280
|
+
navigateToMediaDirectory(data.parentDir === "" ? "" : data.parentDir);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
renderMediaBreadcrumb(data.currentDir);
|
|
285
|
+
mediaGrid.replaceChildren();
|
|
286
|
+
|
|
287
|
+
if (!isSearching) {
|
|
288
|
+
for (const folder of data.folders) {
|
|
289
|
+
const button = document.createElement("button");
|
|
290
|
+
button.type = "button";
|
|
291
|
+
button.className = "media-item-btn media-folder-btn";
|
|
292
|
+
button.title = folder.name;
|
|
293
|
+
|
|
294
|
+
const preview = document.createElement("span");
|
|
295
|
+
preview.className = "media-item-preview media-folder-preview";
|
|
296
|
+
preview.innerHTML = icons.folder;
|
|
297
|
+
preview.setAttribute("aria-hidden", "true");
|
|
298
|
+
|
|
299
|
+
const label = document.createElement("span");
|
|
300
|
+
label.className = "media-item-name";
|
|
301
|
+
label.textContent = folder.name;
|
|
302
|
+
|
|
303
|
+
button.append(preview, label);
|
|
304
|
+
button.addEventListener("click", () => {
|
|
305
|
+
navigateToMediaDirectory(folder.dir);
|
|
306
|
+
});
|
|
307
|
+
mediaGrid.append(button);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
mediaGrid.append(createMediaAddItem());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const image of data.images) {
|
|
314
|
+
const relativePath = image.webPath.replace(/^\//, "");
|
|
315
|
+
const displayName = isSearching
|
|
316
|
+
? getImageSearchLabel(image, data.currentDir)
|
|
317
|
+
: image.name;
|
|
318
|
+
const button = document.createElement("button");
|
|
319
|
+
button.type = "button";
|
|
320
|
+
button.className = "media-item-btn media-image-btn";
|
|
321
|
+
button.title = displayName;
|
|
322
|
+
|
|
323
|
+
const preview = document.createElement("img");
|
|
324
|
+
preview.className = "media-item-preview media-image-preview";
|
|
325
|
+
preview.src = getMediaFileUrl(relativePath);
|
|
326
|
+
preview.alt = "";
|
|
327
|
+
preview.loading = "lazy";
|
|
328
|
+
|
|
329
|
+
const label = document.createElement("span");
|
|
330
|
+
label.className = "media-item-name";
|
|
331
|
+
label.textContent = displayName;
|
|
332
|
+
|
|
333
|
+
button.append(preview, label);
|
|
334
|
+
button.addEventListener("click", () => {
|
|
335
|
+
handleImageSelection(image);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
mediaGrid.append(button);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (data.folders.length === 0 && data.images.length === 0) {
|
|
342
|
+
if (isSearching) {
|
|
343
|
+
setMediaStatus(
|
|
344
|
+
`No images matching "${data.searchQuery}" in ${formatMediaDirLabel(data.currentDir)}.`,
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
setMediaStatus("");
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
setMediaStatus("");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function loadMediaDirectory(relativeDir = currentMediaDir) {
|
|
355
|
+
currentMediaDir = relativeDir;
|
|
356
|
+
const searchQuery = mediaSearchInput.value.trim();
|
|
357
|
+
|
|
358
|
+
if (!getProjectPath()) {
|
|
359
|
+
setMediaStatus("Open and scan a project first.", { isError: true });
|
|
360
|
+
mediaGrid.replaceChildren();
|
|
361
|
+
mediaUpBtn.disabled = true;
|
|
362
|
+
mediaBreadcrumb.replaceChildren();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
setMediaStatus(searchQuery ? "Searching…" : "Loading…");
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const params = new URLSearchParams({
|
|
370
|
+
project: getProjectPath(),
|
|
371
|
+
dir: relativeDir,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (searchQuery) {
|
|
375
|
+
params.set("q", searchQuery);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const response = await fetch(`/api/media?${params.toString()}`);
|
|
379
|
+
const data = await response.json();
|
|
380
|
+
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
setMediaStatus(data.error ?? "Could not load media", { isError: true });
|
|
383
|
+
mediaGrid.replaceChildren();
|
|
384
|
+
mediaUpBtn.disabled = true;
|
|
385
|
+
mediaBreadcrumb.replaceChildren();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
renderMediaDirectory(data);
|
|
390
|
+
} catch {
|
|
391
|
+
setMediaStatus("Could not load media", { isError: true });
|
|
392
|
+
mediaGrid.replaceChildren();
|
|
393
|
+
mediaUpBtn.disabled = true;
|
|
394
|
+
mediaBreadcrumb.replaceChildren();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function openMediaDialog() {
|
|
399
|
+
onOpen?.();
|
|
400
|
+
showMediaBrowserView();
|
|
401
|
+
mediaSearchInput.value = "";
|
|
402
|
+
currentMediaDir = "img";
|
|
403
|
+
dialog.showModal();
|
|
404
|
+
loadMediaDirectory("img");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
closeBtn.addEventListener("click", () => {
|
|
408
|
+
dialog.close();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
dialog.addEventListener("click", (event) => {
|
|
412
|
+
if (event.target === dialog) {
|
|
413
|
+
dialog.close();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
dialog.addEventListener("close", () => {
|
|
418
|
+
onClose?.();
|
|
419
|
+
showMediaBrowserView();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
imageForm.addEventListener("submit", (event) => {
|
|
423
|
+
event.preventDefault();
|
|
424
|
+
|
|
425
|
+
if (!pendingImage) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const handled = onInsert?.(pendingImage, {
|
|
430
|
+
alt: imageAltInput.value,
|
|
431
|
+
lazyLoad: imageLazyInput.checked,
|
|
432
|
+
caption: imageCaptionInput.value,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
if (handled !== false) {
|
|
436
|
+
dialog.close();
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
imageBackBtn.addEventListener("click", () => {
|
|
441
|
+
showMediaBrowserView();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
mediaSearchInput.addEventListener("input", () => {
|
|
445
|
+
loadMediaDirectory(currentMediaDir);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
imageAltInput.addEventListener("keydown", (event) => {
|
|
449
|
+
if (event.key === "Enter") {
|
|
450
|
+
event.preventDefault();
|
|
451
|
+
imageCaptionInput.focus();
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return { dialog, openMediaDialog };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function filenameToAlt(filename) {
|
|
459
|
+
const base = filename.replace(/\.[^.]+$/, "");
|
|
460
|
+
return base
|
|
461
|
+
.replace(/[-_]+/g, " ")
|
|
462
|
+
.replace(/\s+/g, " ")
|
|
463
|
+
.trim();
|
|
464
|
+
}
|
package/public/styles.css
CHANGED
|
@@ -849,17 +849,32 @@ details[open] > .tree-folder-header .tree-folder-chevron {
|
|
|
849
849
|
align-items: start;
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
-
.frontmatter-row--compact
|
|
853
|
-
|
|
854
|
-
|
|
852
|
+
.frontmatter-row--compact > .frontmatter-key-input {
|
|
853
|
+
grid-column: 1;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.frontmatter-row--compact > .frontmatter-type-select {
|
|
857
|
+
grid-column: 2;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.frontmatter-row--compact > .frontmatter-remove-btn {
|
|
861
|
+
grid-column: 4;
|
|
855
862
|
}
|
|
856
863
|
|
|
857
864
|
.frontmatter-array-item {
|
|
858
865
|
grid-template-columns: 1rem 6.5rem minmax(0, 1.6fr) 2rem;
|
|
859
866
|
}
|
|
860
867
|
|
|
861
|
-
.frontmatter-array-item.frontmatter-row--compact {
|
|
862
|
-
grid-
|
|
868
|
+
.frontmatter-array-item.frontmatter-row--compact > .frontmatter-array-marker {
|
|
869
|
+
grid-column: 1;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.frontmatter-array-item.frontmatter-row--compact > .frontmatter-type-select {
|
|
873
|
+
grid-column: 2;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.frontmatter-array-item.frontmatter-row--compact > .frontmatter-remove-btn {
|
|
877
|
+
grid-column: 4;
|
|
863
878
|
}
|
|
864
879
|
|
|
865
880
|
.frontmatter-children {
|
|
@@ -883,9 +898,43 @@ details[open] > .tree-folder-header .tree-folder-chevron {
|
|
|
883
898
|
}
|
|
884
899
|
|
|
885
900
|
.frontmatter-scalar {
|
|
901
|
+
display: flex;
|
|
902
|
+
align-items: center;
|
|
903
|
+
gap: 0.35rem;
|
|
886
904
|
width: 100%;
|
|
887
905
|
}
|
|
888
906
|
|
|
907
|
+
.frontmatter-scalar .frontmatter-value-input {
|
|
908
|
+
flex: 1;
|
|
909
|
+
min-width: 0;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.frontmatter-media-btn {
|
|
913
|
+
display: inline-flex;
|
|
914
|
+
align-items: center;
|
|
915
|
+
justify-content: center;
|
|
916
|
+
flex-shrink: 0;
|
|
917
|
+
width: 2rem;
|
|
918
|
+
height: 2.15rem;
|
|
919
|
+
padding: 0;
|
|
920
|
+
border: 1px solid transparent;
|
|
921
|
+
border-radius: 8px;
|
|
922
|
+
background: transparent;
|
|
923
|
+
color: var(--muted);
|
|
924
|
+
cursor: pointer;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
.frontmatter-media-btn:hover:not(:disabled) {
|
|
928
|
+
border-color: var(--border);
|
|
929
|
+
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
|
930
|
+
color: var(--accent);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.frontmatter-media-btn svg {
|
|
934
|
+
display: block;
|
|
935
|
+
flex-shrink: 0;
|
|
936
|
+
}
|
|
937
|
+
|
|
889
938
|
.frontmatter-boolean-label {
|
|
890
939
|
display: inline-flex;
|
|
891
940
|
align-items: center;
|