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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starmark",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Browse and edit markdown content in local Astro sites",
5
5
  "keywords": [
6
6
  "cms",
package/public/app.js CHANGED
@@ -29,6 +29,7 @@ const frontmatterEditor = createFrontmatterEditor(frontmatterEditorRoot, {
29
29
  saveCurrentFile();
30
30
  }
31
31
  },
32
+ getProjectPath: () => currentProjectPath,
32
33
  });
33
34
  const projectsSection = document.getElementById("projects-section");
34
35
  const projectList = document.getElementById("project-list");
@@ -1,4 +1,5 @@
1
1
  import { icons } from "./icons.js";
2
+ import { createMediaDialog } from "./media-browser.js";
2
3
  import {
3
4
  defaultValueForType,
4
5
  inferValueType,
@@ -31,7 +32,24 @@ function coerceScalar(type, rawValue) {
31
32
  }
32
33
  }
33
34
 
34
- export function createFrontmatterEditor(root, { onChange } = {}) {
35
+ function isBannerImageSrcField(fieldPath) {
36
+ return (
37
+ fieldPath.length >= 2 &&
38
+ fieldPath[fieldPath.length - 2] === "bannerImage" &&
39
+ fieldPath[fieldPath.length - 1] === "src"
40
+ );
41
+ }
42
+
43
+ function isDateField(fieldPath) {
44
+ const key = fieldPath[fieldPath.length - 1];
45
+ return key === "pubDate" || key === "modDate";
46
+ }
47
+
48
+ function isIsoDateString(value) {
49
+ return /^\d{4}-\d{2}-\d{2}$/.test(String(value));
50
+ }
51
+
52
+ export function createFrontmatterEditor(root, { onChange, getProjectPath } = {}) {
35
53
  let data = {};
36
54
  let parseError = null;
37
55
  let rawFallback = "";
@@ -41,6 +59,32 @@ export function createFrontmatterEditor(root, { onChange } = {}) {
41
59
  container.className = "frontmatter-form";
42
60
  root.replaceChildren(container);
43
61
 
62
+ let mediaPickerTarget = null;
63
+
64
+ const mediaPickerContext = {
65
+ openMediaPicker: null,
66
+ setTarget(callback) {
67
+ mediaPickerTarget = callback;
68
+ },
69
+ };
70
+
71
+ if (getProjectPath) {
72
+ const { dialog, openMediaDialog } = createMediaDialog({
73
+ getProjectPath,
74
+ title: "Choose image",
75
+ selectMode: "path",
76
+ dialogId: "media-dialog-frontmatter",
77
+ onSelect(image) {
78
+ mediaPickerTarget?.(image.webPath);
79
+ },
80
+ onClose() {
81
+ mediaPickerTarget = null;
82
+ },
83
+ });
84
+ document.body.append(dialog);
85
+ mediaPickerContext.openMediaPicker = openMediaDialog;
86
+ }
87
+
44
88
  function emitChange() {
45
89
  if (silent || !onChange) {
46
90
  return;
@@ -106,7 +150,7 @@ export function createFrontmatterEditor(root, { onChange } = {}) {
106
150
  return;
107
151
  }
108
152
 
109
- container.appendChild(renderObjectEditor(data, refresh, touchData, 0));
153
+ container.appendChild(renderObjectEditor(data, refresh, touchData, 0, [], mediaPickerContext));
110
154
  container.appendChild(renderAddFieldButton(data, refresh));
111
155
  }
112
156
 
@@ -196,7 +240,7 @@ function renderAddFieldButton(objectValue, onRefresh, label = "Add field") {
196
240
  return footer;
197
241
  }
198
242
 
199
- function renderObjectEditor(objectValue, onRefresh, onValueChange, depth) {
243
+ function renderObjectEditor(objectValue, onRefresh, onValueChange, depth, fieldPath, mediaPickerContext) {
200
244
  const section = document.createElement("div");
201
245
  section.className = "frontmatter-node frontmatter-object";
202
246
  if (depth > 0) {
@@ -213,13 +257,33 @@ function renderObjectEditor(objectValue, onRefresh, onValueChange, depth) {
213
257
  }
214
258
 
215
259
  for (const [key, value] of entries) {
216
- section.appendChild(renderFieldRow(key, value, objectValue, onRefresh, onValueChange, depth));
260
+ section.appendChild(
261
+ renderFieldRow(
262
+ key,
263
+ value,
264
+ objectValue,
265
+ onRefresh,
266
+ onValueChange,
267
+ depth,
268
+ [...fieldPath, key],
269
+ mediaPickerContext,
270
+ ),
271
+ );
217
272
  }
218
273
 
219
274
  return section;
220
275
  }
221
276
 
222
- function renderFieldRow(key, value, parentObject, onRefresh, onValueChange, depth) {
277
+ function renderFieldRow(
278
+ key,
279
+ value,
280
+ parentObject,
281
+ onRefresh,
282
+ onValueChange,
283
+ depth,
284
+ fieldPath,
285
+ mediaPickerContext,
286
+ ) {
223
287
  const field = document.createElement("div");
224
288
  field.className = "frontmatter-field";
225
289
 
@@ -287,7 +351,15 @@ function renderFieldRow(key, value, parentObject, onRefresh, onValueChange, dept
287
351
  const valueHost = document.createElement("div");
288
352
  valueHost.className = "frontmatter-value-host";
289
353
  valueHost.appendChild(
290
- renderScalarEditor(value, currentType, parentObject, key, onValueChange),
354
+ renderScalarEditor(
355
+ value,
356
+ currentType,
357
+ parentObject,
358
+ key,
359
+ onValueChange,
360
+ fieldPath,
361
+ mediaPickerContext,
362
+ ),
291
363
  );
292
364
  row.append(keyInput, typeSelect, valueHost, removeButton);
293
365
  }
@@ -298,7 +370,17 @@ function renderFieldRow(key, value, parentObject, onRefresh, onValueChange, dept
298
370
  const children = document.createElement("div");
299
371
  children.className = "frontmatter-children";
300
372
  children.appendChild(
301
- renderValueEditor(value, currentType, parentObject, key, onRefresh, onValueChange, depth + 1),
373
+ renderValueEditor(
374
+ value,
375
+ currentType,
376
+ parentObject,
377
+ key,
378
+ onRefresh,
379
+ onValueChange,
380
+ depth + 1,
381
+ fieldPath,
382
+ mediaPickerContext,
383
+ ),
302
384
  );
303
385
  field.appendChild(children);
304
386
  }
@@ -314,6 +396,8 @@ function renderValueEditor(
314
396
  onRefresh,
315
397
  onValueChange,
316
398
  depth,
399
+ fieldPath,
400
+ mediaPickerContext,
317
401
  ) {
318
402
  if (type === "array") {
319
403
  const arrayValue = Array.isArray(value) ? value : [];
@@ -321,7 +405,14 @@ function renderValueEditor(
321
405
  parentContainer[parentKey] = arrayValue;
322
406
  }
323
407
 
324
- return renderArrayEditor(parentContainer[parentKey], onRefresh, onValueChange, depth);
408
+ return renderArrayEditor(
409
+ parentContainer[parentKey],
410
+ onRefresh,
411
+ onValueChange,
412
+ depth,
413
+ fieldPath,
414
+ mediaPickerContext,
415
+ );
325
416
  }
326
417
 
327
418
  if (type === "object") {
@@ -335,13 +426,36 @@ function renderValueEditor(
335
426
  parentContainer[parentKey] = objectValue;
336
427
  }
337
428
 
338
- return renderNestedObjectEditor(parentContainer[parentKey], onRefresh, onValueChange, depth);
429
+ return renderNestedObjectEditor(
430
+ parentContainer[parentKey],
431
+ onRefresh,
432
+ onValueChange,
433
+ depth,
434
+ fieldPath,
435
+ mediaPickerContext,
436
+ );
339
437
  }
340
438
 
341
- return renderScalarEditor(value, type, parentContainer, parentKey, onValueChange);
439
+ return renderScalarEditor(
440
+ value,
441
+ type,
442
+ parentContainer,
443
+ parentKey,
444
+ onValueChange,
445
+ fieldPath,
446
+ mediaPickerContext,
447
+ );
342
448
  }
343
449
 
344
- function renderScalarEditor(value, type, parentContainer, parentKey, onValueChange) {
450
+ function renderScalarEditor(
451
+ value,
452
+ type,
453
+ parentContainer,
454
+ parentKey,
455
+ onValueChange,
456
+ fieldPath = [],
457
+ mediaPickerContext = null,
458
+ ) {
345
459
  const wrapper = document.createElement("div");
346
460
  wrapper.className = "frontmatter-scalar";
347
461
 
@@ -363,33 +477,82 @@ function renderScalarEditor(value, type, parentContainer, parentKey, onValueChan
363
477
  return wrapper;
364
478
  }
365
479
 
480
+ const stringValue =
481
+ type === "null" ? "" : value === null || value === undefined ? "" : String(value);
482
+ const useDateInput =
483
+ type === "string" &&
484
+ isDateField(fieldPath) &&
485
+ (stringValue === "" || isIsoDateString(stringValue));
486
+
366
487
  const input = document.createElement("input");
367
- input.type = type === "number" ? "number" : "text";
488
+ input.type = useDateInput ? "date" : type === "number" ? "number" : "text";
368
489
  input.className = "frontmatter-value-input";
369
- input.value =
370
- type === "null" ? "" : value === null || value === undefined ? "" : String(value);
371
- input.placeholder = type === "null" ? "null" : "value";
490
+ input.value = stringValue;
491
+ input.placeholder = type === "null" ? "null" : useDateInput ? "YYYY-MM-DD" : "value";
372
492
  input.spellcheck = false;
373
493
 
494
+ if (useDateInput) {
495
+ input.setAttribute("aria-label", `${fieldPath[fieldPath.length - 1]} date`);
496
+ }
497
+
374
498
  input.addEventListener("input", () => {
375
499
  parentContainer[parentKey] = coerceScalar(type, input.value);
376
500
  onValueChange();
377
501
  });
378
502
 
379
503
  wrapper.appendChild(input);
504
+
505
+ if (
506
+ type === "string" &&
507
+ isBannerImageSrcField(fieldPath) &&
508
+ mediaPickerContext?.openMediaPicker
509
+ ) {
510
+ const pickButton = document.createElement("button");
511
+ pickButton.type = "button";
512
+ pickButton.className = "frontmatter-media-btn";
513
+ pickButton.innerHTML = icons.image;
514
+ pickButton.setAttribute("aria-label", "Choose image from media library");
515
+ pickButton.title = "Choose image";
516
+ pickButton.addEventListener("click", () => {
517
+ mediaPickerContext.setTarget((webPath) => {
518
+ input.value = webPath;
519
+ parentContainer[parentKey] = webPath;
520
+ onValueChange();
521
+ });
522
+ mediaPickerContext.openMediaPicker();
523
+ });
524
+ wrapper.appendChild(pickButton);
525
+ }
526
+
380
527
  return wrapper;
381
528
  }
382
529
 
383
- function renderNestedObjectEditor(objectValue, onRefresh, onValueChange, depth) {
530
+ function renderNestedObjectEditor(
531
+ objectValue,
532
+ onRefresh,
533
+ onValueChange,
534
+ depth,
535
+ fieldPath,
536
+ mediaPickerContext,
537
+ ) {
384
538
  const wrapper = document.createElement("div");
385
539
  wrapper.className = "frontmatter-nested";
386
540
 
387
- wrapper.appendChild(renderObjectEditor(objectValue, onRefresh, onValueChange, depth));
541
+ wrapper.appendChild(
542
+ renderObjectEditor(objectValue, onRefresh, onValueChange, depth, fieldPath, mediaPickerContext),
543
+ );
388
544
  wrapper.appendChild(renderAddFieldButton(objectValue, onRefresh));
389
545
  return wrapper;
390
546
  }
391
547
 
392
- function renderArrayEditor(arrayValue, onRefresh, onValueChange, depth) {
548
+ function renderArrayEditor(
549
+ arrayValue,
550
+ onRefresh,
551
+ onValueChange,
552
+ depth,
553
+ fieldPath,
554
+ mediaPickerContext,
555
+ ) {
393
556
  const wrapper = document.createElement("div");
394
557
  wrapper.className = "frontmatter-array";
395
558
 
@@ -401,7 +564,18 @@ function renderArrayEditor(arrayValue, onRefresh, onValueChange, depth) {
401
564
  }
402
565
 
403
566
  arrayValue.forEach((item, index) => {
404
- wrapper.appendChild(renderArrayItemRow(item, index, arrayValue, onRefresh, onValueChange, depth));
567
+ wrapper.appendChild(
568
+ renderArrayItemRow(
569
+ item,
570
+ index,
571
+ arrayValue,
572
+ onRefresh,
573
+ onValueChange,
574
+ depth,
575
+ fieldPath,
576
+ mediaPickerContext,
577
+ ),
578
+ );
405
579
  });
406
580
 
407
581
  const footer = document.createElement("div");
@@ -421,7 +595,16 @@ function renderArrayEditor(arrayValue, onRefresh, onValueChange, depth) {
421
595
  return wrapper;
422
596
  }
423
597
 
424
- function renderArrayItemRow(item, index, parentArray, onRefresh, onValueChange, depth) {
598
+ function renderArrayItemRow(
599
+ item,
600
+ index,
601
+ parentArray,
602
+ onRefresh,
603
+ onValueChange,
604
+ depth,
605
+ fieldPath,
606
+ mediaPickerContext,
607
+ ) {
425
608
  const entry = document.createElement("div");
426
609
  entry.className = "frontmatter-array-entry";
427
610
 
@@ -470,7 +653,15 @@ function renderArrayItemRow(item, index, parentArray, onRefresh, onValueChange,
470
653
  const valueHost = document.createElement("div");
471
654
  valueHost.className = "frontmatter-value-host";
472
655
  valueHost.appendChild(
473
- renderScalarEditor(item, currentType, parentArray, index, onValueChange),
656
+ renderScalarEditor(
657
+ item,
658
+ currentType,
659
+ parentArray,
660
+ index,
661
+ onValueChange,
662
+ [...fieldPath, String(index)],
663
+ mediaPickerContext,
664
+ ),
474
665
  );
475
666
  row.append(marker, typeSelect, valueHost, removeButton);
476
667
  }
@@ -481,7 +672,17 @@ function renderArrayItemRow(item, index, parentArray, onRefresh, onValueChange,
481
672
  const children = document.createElement("div");
482
673
  children.className = "frontmatter-children";
483
674
  children.appendChild(
484
- renderValueEditor(item, currentType, parentArray, index, onRefresh, onValueChange, depth + 1),
675
+ renderValueEditor(
676
+ item,
677
+ currentType,
678
+ parentArray,
679
+ index,
680
+ onRefresh,
681
+ onValueChange,
682
+ depth + 1,
683
+ [...fieldPath, String(index)],
684
+ mediaPickerContext,
685
+ ),
485
686
  );
486
687
  entry.appendChild(children);
487
688
  }