@imjp/writenex-astro 1.3.6 → 1.5.0

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.
Files changed (54) hide show
  1. package/dist/chunk-E4PQLKAH.js +282 -0
  2. package/dist/chunk-E4PQLKAH.js.map +1 -0
  3. package/dist/{chunk-NSW7AIVF.js → chunk-EEUN46Q2.js} +3 -3
  4. package/dist/{chunk-YBCPOLMY.js → chunk-P5KMSHFP.js} +2 -1
  5. package/dist/{chunk-YBCPOLMY.js.map → chunk-P5KMSHFP.js.map} +1 -1
  6. package/dist/{chunk-JMNCPNQX.js → chunk-XVQNYPOI.js} +65 -3
  7. package/dist/chunk-XVQNYPOI.js.map +1 -0
  8. package/dist/chunk-YRSIZLHE.js +1 -0
  9. package/dist/{chunk-N37EPLKG.js → chunk-ZWUGHWHD.js} +79 -10
  10. package/dist/chunk-ZWUGHWHD.js.map +1 -0
  11. package/dist/client/index.css +1 -1
  12. package/dist/client/index.css.map +1 -1
  13. package/dist/client/index.js +114 -114
  14. package/dist/client/index.js.map +1 -1
  15. package/dist/config/index.d.ts +3 -2
  16. package/dist/config/index.js +11 -3
  17. package/dist/{config-CliL0CoN.d.ts → config-B7t8CjL1.d.ts} +38 -60
  18. package/dist/{content-TuL3GT66.d.ts → content-CwcgR8P6.d.ts} +1 -1
  19. package/dist/discovery/index.d.ts +2 -2
  20. package/dist/discovery/index.js +2 -2
  21. package/dist/fields/index.d.ts +16 -0
  22. package/dist/fields/index.js +13 -0
  23. package/dist/fields-DUSm13nm.d.ts +223 -0
  24. package/dist/filesystem/index.d.ts +2 -2
  25. package/dist/filesystem/index.js +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.js +13 -5
  28. package/dist/index.js.map +1 -1
  29. package/dist/{loader-53VVP2IN.js → loader-B5WZCVBC.js} +3 -3
  30. package/dist/loader-B5WZCVBC.js.map +1 -0
  31. package/dist/{schema-DDJyoVkj.d.ts → schema-nLMfZ9-o.d.ts} +79 -53
  32. package/dist/server/index.d.ts +2 -2
  33. package/dist/server/index.js +3 -3
  34. package/package.json +5 -1
  35. package/src/client/components/FrontmatterForm/FrontmatterForm.css +104 -0
  36. package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +452 -26
  37. package/src/config/defaults.ts +1 -0
  38. package/src/config/index.ts +3 -0
  39. package/src/config/schema.ts +114 -73
  40. package/src/discovery/schema.ts +120 -1
  41. package/src/fields/collection.ts +67 -0
  42. package/src/fields/fields.ts +150 -0
  43. package/src/fields/index.ts +42 -0
  44. package/src/fields/resolve.ts +203 -0
  45. package/src/fields/types.ts +179 -0
  46. package/src/index.ts +3 -0
  47. package/src/types/config.ts +87 -63
  48. package/src/types/index.ts +3 -0
  49. package/dist/chunk-JMNCPNQX.js.map +0 -1
  50. package/dist/chunk-KIKIPIFA.js +0 -1
  51. package/dist/chunk-N37EPLKG.js.map +0 -1
  52. /package/dist/{chunk-NSW7AIVF.js.map → chunk-EEUN46Q2.js.map} +0 -0
  53. /package/dist/{chunk-KIKIPIFA.js.map → chunk-YRSIZLHE.js.map} +0 -0
  54. /package/dist/{loader-53VVP2IN.js.map → fields/index.js.map} +0 -0
@@ -413,7 +413,6 @@ function DynamicField({
413
413
  }): React.ReactElement {
414
414
  const fieldId = `fm-${name}`;
415
415
  const label = formatFieldLabel(name);
416
- const enumOptions = parseEnumFromDescription(field.description);
417
416
 
418
417
  switch (field.type) {
419
418
  case "boolean":
@@ -428,6 +427,7 @@ function DynamicField({
428
427
  );
429
428
 
430
429
  case "number":
430
+ case "integer":
431
431
  return (
432
432
  <NumberField
433
433
  id={fieldId}
@@ -436,6 +436,7 @@ function DynamicField({
436
436
  onChange={onChange}
437
437
  disabled={disabled}
438
438
  required={field.required}
439
+ step={field.type === "integer" ? 1 : "any"}
439
440
  />
440
441
  );
441
442
 
@@ -451,6 +452,18 @@ function DynamicField({
451
452
  />
452
453
  );
453
454
 
455
+ case "datetime":
456
+ return (
457
+ <DatetimeField
458
+ id={fieldId}
459
+ label={label}
460
+ value={value}
461
+ onChange={onChange}
462
+ disabled={disabled}
463
+ required={field.required}
464
+ />
465
+ );
466
+
454
467
  case "array":
455
468
  return (
456
469
  <ArrayField
@@ -464,6 +477,32 @@ function DynamicField({
464
477
  />
465
478
  );
466
479
 
480
+ case "multiselect":
481
+ return (
482
+ <MultiselectField
483
+ id={fieldId}
484
+ label={label}
485
+ value={value as string[] | undefined}
486
+ options={field.options || []}
487
+ onChange={onChange}
488
+ disabled={disabled}
489
+ required={field.required}
490
+ />
491
+ );
492
+
493
+ case "select":
494
+ return (
495
+ <SelectField
496
+ id={fieldId}
497
+ label={label}
498
+ value={String(value ?? "")}
499
+ options={field.options || []}
500
+ onChange={onChange}
501
+ disabled={disabled}
502
+ required={field.required}
503
+ />
504
+ );
505
+
467
506
  case "image":
468
507
  return (
469
508
  <ImageField
@@ -481,24 +520,95 @@ function DynamicField({
481
520
  />
482
521
  );
483
522
 
523
+ case "file":
524
+ return (
525
+ <FileField
526
+ id={fieldId}
527
+ label={label}
528
+ value={value as string | undefined}
529
+ onChange={onChange}
530
+ disabled={disabled}
531
+ required={field.required}
532
+ />
533
+ );
534
+
535
+ case "slug":
536
+ return (
537
+ <SlugField
538
+ id={fieldId}
539
+ label={label}
540
+ value={String(value ?? "")}
541
+ onChange={onChange}
542
+ disabled={disabled}
543
+ required={field.required}
544
+ />
545
+ );
546
+
547
+ case "url":
548
+ return (
549
+ <UrlField
550
+ id={fieldId}
551
+ label={label}
552
+ value={String(value ?? "")}
553
+ onChange={onChange}
554
+ disabled={disabled}
555
+ required={field.required}
556
+ />
557
+ );
558
+
559
+ case "object":
560
+ return (
561
+ <ObjectField
562
+ id={fieldId}
563
+ label={label}
564
+ value={value as Record<string, unknown> | undefined}
565
+ fields={field.fields}
566
+ onChange={onChange}
567
+ disabled={disabled}
568
+ required={field.required}
569
+ />
570
+ );
571
+
572
+ case "relationship":
573
+ return (
574
+ <RelationshipField
575
+ id={fieldId}
576
+ label={label}
577
+ value={value as string | string[] | undefined}
578
+ collection={field.collection}
579
+ onChange={onChange}
580
+ disabled={disabled}
581
+ required={field.required}
582
+ />
583
+ );
584
+
585
+ case "markdoc":
586
+ case "mdx":
587
+ return (
588
+ <RichTextField
589
+ id={fieldId}
590
+ label={label}
591
+ value={String(value ?? "")}
592
+ onChange={onChange}
593
+ disabled={disabled}
594
+ required={field.required}
595
+ />
596
+ );
597
+
598
+ case "ignored":
599
+ case "empty":
600
+ case "empty-content":
601
+ case "empty-document":
602
+ case "child":
603
+ return <></>;
604
+
484
605
  case "string":
485
606
  default:
486
- if (enumOptions.length > 0) {
487
- return (
488
- <SelectField
489
- id={fieldId}
490
- label={label}
491
- value={String(value ?? "")}
492
- options={enumOptions}
493
- onChange={onChange}
494
- disabled={disabled}
495
- required={field.required}
496
- />
497
- );
498
- }
499
-
500
607
  const isMultiline =
501
- name === "description" || name === "excerpt" || name === "summary";
608
+ field.multiline ||
609
+ name === "description" ||
610
+ name === "excerpt" ||
611
+ name === "summary";
502
612
 
503
613
  return (
504
614
  <StringField
@@ -574,9 +684,11 @@ function NumberField({
574
684
  onChange,
575
685
  disabled,
576
686
  required,
687
+ step,
577
688
  }: BaseFieldProps & {
578
689
  value: number | undefined;
579
690
  onChange: (value: number | undefined) => void;
691
+ step?: string | number;
580
692
  }): React.ReactElement {
581
693
  return (
582
694
  <div className="wn-frontmatter-field">
@@ -588,6 +700,7 @@ function NumberField({
588
700
  id={id}
589
701
  type="number"
590
702
  value={value ?? ""}
703
+ step={step ?? "any"}
591
704
  onChange={(e) => {
592
705
  const val = e.target.value;
593
706
  onChange(val === "" ? undefined : Number(val));
@@ -869,6 +982,329 @@ function ImageField({
869
982
  );
870
983
  }
871
984
 
985
+ function DatetimeField({
986
+ id,
987
+ label,
988
+ value,
989
+ onChange,
990
+ disabled,
991
+ required,
992
+ }: BaseFieldProps & {
993
+ value: unknown;
994
+ onChange: (value: string | undefined) => void;
995
+ }): React.ReactElement {
996
+ const datetimeValue = formatDatetimeForInput(value);
997
+
998
+ return (
999
+ <div className="wn-frontmatter-field">
1000
+ <label htmlFor={id} className="wn-frontmatter-label">
1001
+ {label}
1002
+ {required && <span className="wn-frontmatter-required">*</span>}
1003
+ </label>
1004
+ <input
1005
+ id={id}
1006
+ type="datetime-local"
1007
+ value={datetimeValue}
1008
+ onChange={(e) => onChange(e.target.value || undefined)}
1009
+ disabled={disabled}
1010
+ className="wn-frontmatter-input"
1011
+ />
1012
+ </div>
1013
+ );
1014
+ }
1015
+
1016
+ function MultiselectField({
1017
+ id,
1018
+ label,
1019
+ value,
1020
+ options,
1021
+ onChange,
1022
+ disabled,
1023
+ required,
1024
+ }: BaseFieldProps & {
1025
+ value: string[] | undefined;
1026
+ options: string[];
1027
+ onChange: (value: string[]) => void;
1028
+ }): React.ReactElement {
1029
+ const selectedValues = value || [];
1030
+
1031
+ const handleToggle = (option: string) => {
1032
+ if (selectedValues.includes(option)) {
1033
+ onChange(selectedValues.filter((v) => v !== option));
1034
+ } else {
1035
+ onChange([...selectedValues, option]);
1036
+ }
1037
+ };
1038
+
1039
+ return (
1040
+ <div className="wn-frontmatter-field">
1041
+ <label className="wn-frontmatter-label">
1042
+ {label}
1043
+ {required && <span className="wn-frontmatter-required">*</span>}
1044
+ </label>
1045
+ <div className="wn-frontmatter-checkbox-group">
1046
+ {options.map((option) => (
1047
+ <label key={option} className="wn-frontmatter-checkbox-label">
1048
+ <input
1049
+ type="checkbox"
1050
+ checked={selectedValues.includes(option)}
1051
+ onChange={() => handleToggle(option)}
1052
+ disabled={disabled}
1053
+ className="wn-frontmatter-checkbox"
1054
+ />
1055
+ <span>{option}</span>
1056
+ </label>
1057
+ ))}
1058
+ </div>
1059
+ </div>
1060
+ );
1061
+ }
1062
+
1063
+ function FileField({
1064
+ id,
1065
+ label,
1066
+ value,
1067
+ onChange,
1068
+ disabled,
1069
+ required,
1070
+ }: BaseFieldProps & {
1071
+ value: string | undefined;
1072
+ onChange: (value: string | undefined) => void;
1073
+ }): React.ReactElement {
1074
+ return (
1075
+ <div className="wn-frontmatter-field">
1076
+ <label htmlFor={id} className="wn-frontmatter-label">
1077
+ {label}
1078
+ {required && <span className="wn-frontmatter-required">*</span>}
1079
+ </label>
1080
+ <input
1081
+ id={id}
1082
+ type="text"
1083
+ value={value ?? ""}
1084
+ onChange={(e) => onChange(e.target.value || undefined)}
1085
+ disabled={disabled}
1086
+ placeholder="./files/document.pdf"
1087
+ className="wn-frontmatter-input"
1088
+ />
1089
+ </div>
1090
+ );
1091
+ }
1092
+
1093
+ function SlugField({
1094
+ id,
1095
+ label,
1096
+ value,
1097
+ onChange,
1098
+ disabled,
1099
+ required,
1100
+ }: BaseFieldProps & {
1101
+ value: string;
1102
+ onChange: (value: string) => void;
1103
+ }): React.ReactElement {
1104
+ return (
1105
+ <div className="wn-frontmatter-field">
1106
+ <label htmlFor={id} className="wn-frontmatter-label">
1107
+ {label}
1108
+ {required && <span className="wn-frontmatter-required">*</span>}
1109
+ </label>
1110
+ <input
1111
+ id={id}
1112
+ type="text"
1113
+ value={value}
1114
+ onChange={(e) => onChange(e.target.value)}
1115
+ disabled={disabled}
1116
+ placeholder="enter-slug-here"
1117
+ className="wn-frontmatter-input"
1118
+ />
1119
+ </div>
1120
+ );
1121
+ }
1122
+
1123
+ function UrlField({
1124
+ id,
1125
+ label,
1126
+ value,
1127
+ onChange,
1128
+ disabled,
1129
+ required,
1130
+ }: BaseFieldProps & {
1131
+ value: string;
1132
+ onChange: (value: string) => void;
1133
+ }): React.ReactElement {
1134
+ return (
1135
+ <div className="wn-frontmatter-field">
1136
+ <label htmlFor={id} className="wn-frontmatter-label">
1137
+ {label}
1138
+ {required && <span className="wn-frontmatter-required">*</span>}
1139
+ </label>
1140
+ <input
1141
+ id={id}
1142
+ type="url"
1143
+ value={value}
1144
+ onChange={(e) => onChange(e.target.value)}
1145
+ disabled={disabled}
1146
+ placeholder="https://example.com"
1147
+ className="wn-frontmatter-input"
1148
+ />
1149
+ </div>
1150
+ );
1151
+ }
1152
+
1153
+ function ObjectField({
1154
+ id,
1155
+ label,
1156
+ value,
1157
+ fields,
1158
+ onChange,
1159
+ disabled,
1160
+ required,
1161
+ }: BaseFieldProps & {
1162
+ value: Record<string, unknown> | undefined;
1163
+ fields?: Record<string, SchemaField>;
1164
+ onChange: (value: Record<string, unknown>) => void;
1165
+ }): React.ReactElement {
1166
+ const objectValue = value || {};
1167
+ const [isExpanded, setIsExpanded] = useState(true);
1168
+
1169
+ const handleFieldChange = (fieldName: string, fieldValue: unknown) => {
1170
+ onChange({ ...objectValue, [fieldName]: fieldValue });
1171
+ };
1172
+
1173
+ if (!fields || Object.keys(fields).length === 0) {
1174
+ return (
1175
+ <div className="wn-frontmatter-field">
1176
+ <label className="wn-frontmatter-label">
1177
+ {label}
1178
+ {required && <span className="wn-frontmatter-required">*</span>}
1179
+ </label>
1180
+ <span className="wn-frontmatter-hint">No sub-fields defined</span>
1181
+ </div>
1182
+ );
1183
+ }
1184
+
1185
+ return (
1186
+ <div className="wn-frontmatter-field-group">
1187
+ <button
1188
+ type="button"
1189
+ className="wn-frontmatter-group-toggle"
1190
+ onClick={() => setIsExpanded(!isExpanded)}
1191
+ disabled={disabled}
1192
+ >
1193
+ <span className="wn-frontmatter-label">
1194
+ {label}
1195
+ {required && <span className="wn-frontmatter-required">*</span>}
1196
+ </span>
1197
+ <span>{isExpanded ? "[-]" : "[+]"}</span>
1198
+ </button>
1199
+ {isExpanded && (
1200
+ <div className="wn-frontmatter-group-content">
1201
+ {Object.entries(fields).map(([fieldName, fieldDef]) => (
1202
+ <DynamicField
1203
+ key={fieldName}
1204
+ name={fieldName}
1205
+ field={fieldDef}
1206
+ value={objectValue[fieldName]}
1207
+ onChange={(fieldValue) =>
1208
+ handleFieldChange(fieldName, fieldValue)
1209
+ }
1210
+ disabled={disabled}
1211
+ />
1212
+ ))}
1213
+ </div>
1214
+ )}
1215
+ </div>
1216
+ );
1217
+ }
1218
+
1219
+ function RelationshipField({
1220
+ id,
1221
+ label,
1222
+ value,
1223
+ collection,
1224
+ onChange,
1225
+ disabled,
1226
+ required,
1227
+ }: BaseFieldProps & {
1228
+ value: string | string[] | undefined;
1229
+ collection?: string;
1230
+ onChange: (value: string | string[]) => void;
1231
+ }): React.ReactElement {
1232
+ return (
1233
+ <div className="wn-frontmatter-field">
1234
+ <label htmlFor={id} className="wn-frontmatter-label">
1235
+ {label}
1236
+ {required && <span className="wn-frontmatter-required">*</span>}
1237
+ </label>
1238
+ <input
1239
+ id={id}
1240
+ type="text"
1241
+ value={Array.isArray(value) ? value.join(", ") : (value ?? "")}
1242
+ onChange={(e) => onChange(e.target.value)}
1243
+ disabled={disabled}
1244
+ placeholder={collection ? `Reference to ${collection}` : "Reference"}
1245
+ className="wn-frontmatter-input"
1246
+ />
1247
+ {collection && (
1248
+ <span className="wn-frontmatter-hint">References: {collection}</span>
1249
+ )}
1250
+ </div>
1251
+ );
1252
+ }
1253
+
1254
+ function RichTextField({
1255
+ id,
1256
+ label,
1257
+ value,
1258
+ onChange,
1259
+ disabled,
1260
+ required,
1261
+ }: BaseFieldProps & {
1262
+ value: string;
1263
+ onChange: (value: string) => void;
1264
+ }): React.ReactElement {
1265
+ return (
1266
+ <div className="wn-frontmatter-field wn-frontmatter-field--full">
1267
+ <label htmlFor={id} className="wn-frontmatter-label">
1268
+ {label}
1269
+ {required && <span className="wn-frontmatter-required">*</span>}
1270
+ </label>
1271
+ <textarea
1272
+ id={id}
1273
+ value={value}
1274
+ onChange={(e) => onChange(e.target.value)}
1275
+ disabled={disabled}
1276
+ placeholder={`Enter ${label.toLowerCase()}`}
1277
+ rows={8}
1278
+ className="wn-frontmatter-textarea wn-frontmatter-textarea--rich"
1279
+ />
1280
+ </div>
1281
+ );
1282
+ }
1283
+
1284
+ function formatDatetimeForInput(value: unknown): string {
1285
+ if (!value) return "";
1286
+
1287
+ if (typeof value === "string") {
1288
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)) {
1289
+ return value.slice(0, 16);
1290
+ }
1291
+ try {
1292
+ const date = new Date(value);
1293
+ if (!isNaN(date.getTime())) {
1294
+ return date.toISOString().slice(0, 16) ?? "";
1295
+ }
1296
+ } catch {
1297
+ return "";
1298
+ }
1299
+ }
1300
+
1301
+ if (value instanceof Date) {
1302
+ return value.toISOString().slice(0, 16) ?? "";
1303
+ }
1304
+
1305
+ return "";
1306
+ }
1307
+
872
1308
  // Utilities
873
1309
 
874
1310
  function formatFieldLabel(name: string): string {
@@ -902,13 +1338,3 @@ function formatDateForInput(value: unknown): string {
902
1338
 
903
1339
  return "";
904
1340
  }
905
-
906
- function parseEnumFromDescription(description?: string): string[] {
907
- if (!description) return [];
908
- const match = description.match(/^Options:\s*(.+)$/i);
909
- if (!match || !match[1]) return [];
910
- return match[1]
911
- .split(",")
912
- .map((s) => s.trim())
913
- .filter(Boolean);
914
- }
@@ -93,6 +93,7 @@ export function applyConfigDefaults(
93
93
  ): Required<WritenexConfig> {
94
94
  return {
95
95
  collections: (config.collections ?? []).map(applyCollectionDefaults),
96
+ singletons: (config.singletons ?? []).map(applyCollectionDefaults),
96
97
  images: config.images
97
98
  ? { ...DEFAULT_IMAGE_CONFIG, ...config.images }
98
99
  : DEFAULT_IMAGE_CONFIG,
@@ -7,6 +7,9 @@
7
7
  * @module @writenex/astro/config
8
8
  */
9
9
 
10
+ export type { FieldDefinition, FieldKind, ValidationOptions } from "@/fields";
11
+ // Fields API
12
+ export { collection, fields, singleton } from "@/fields";
10
13
  // Defaults and constants
11
14
  export {
12
15
  applyCollectionDefaults,