@imjp/writenex-astro 0.1.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.
- package/README.md +539 -0
- package/dist/chunk-5PM6EQE5.js +151 -0
- package/dist/chunk-5PM6EQE5.js.map +1 -0
- package/dist/chunk-7XU5X6CW.js +1331 -0
- package/dist/chunk-7XU5X6CW.js.map +1 -0
- package/dist/chunk-AAOQHQPU.js +574 -0
- package/dist/chunk-AAOQHQPU.js.map +1 -0
- package/dist/chunk-CF2XXJFF.js +1410 -0
- package/dist/chunk-CF2XXJFF.js.map +1 -0
- package/dist/chunk-CRPZUUDU.js +52 -0
- package/dist/chunk-CRPZUUDU.js.map +1 -0
- package/dist/chunk-CYLDJ3HZ.js +310 -0
- package/dist/chunk-CYLDJ3HZ.js.map +1 -0
- package/dist/chunk-KIKIPIFA.js +1 -0
- package/dist/chunk-KIKIPIFA.js.map +1 -0
- package/dist/chunk-XNTQTTJU.js +145 -0
- package/dist/chunk-XNTQTTJU.js.map +1 -0
- package/dist/client/index.css +2 -0
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.js +375 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.css +584 -0
- package/dist/client/variables.css +304 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config-BmEdBDo_.d.ts +220 -0
- package/dist/content-BWR52vD-.d.ts +64 -0
- package/dist/discovery/index.d.ts +310 -0
- package/dist/discovery/index.js +38 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/errors-C0iYiDTv.d.ts +107 -0
- package/dist/filesystem/index.d.ts +1292 -0
- package/dist/filesystem/index.js +203 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/image-FP7w5ZIs.d.ts +47 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-55LWCXHA.js +12 -0
- package/dist/loader-55LWCXHA.js.map +1 -0
- package/dist/loader-CrdnaAWR.d.ts +327 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +37 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +94 -0
- package/src/client/App.tsx +900 -0
- package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
- package/src/client/components/ConfigPanel/index.ts +6 -0
- package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
- package/src/client/components/CreateContentModal/index.ts +7 -0
- package/src/client/components/Editor/Editor.css +885 -0
- package/src/client/components/Editor/Editor.tsx +484 -0
- package/src/client/components/Editor/ImageDialog.css +344 -0
- package/src/client/components/Editor/ImageDialog.tsx +367 -0
- package/src/client/components/Editor/LinkDialog.css +326 -0
- package/src/client/components/Editor/LinkDialog.tsx +332 -0
- package/src/client/components/Editor/index.ts +6 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
- package/src/client/components/FrontmatterForm/index.ts +7 -0
- package/src/client/components/Header/Header.css +300 -0
- package/src/client/components/Header/Header.tsx +300 -0
- package/src/client/components/Header/index.ts +7 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
- package/src/client/components/KeyboardShortcuts/index.ts +6 -0
- package/src/client/components/LazyEditor.tsx +75 -0
- package/src/client/components/LiveRegion/LiveRegion.css +19 -0
- package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
- package/src/client/components/LiveRegion/index.ts +7 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
- package/src/client/components/SearchReplace/index.ts +7 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
- package/src/client/components/SelectCollectionModal/index.ts +7 -0
- package/src/client/components/Sidebar/Sidebar.css +570 -0
- package/src/client/components/Sidebar/Sidebar.tsx +617 -0
- package/src/client/components/Sidebar/index.ts +7 -0
- package/src/client/components/SkipLink/SkipLink.css +51 -0
- package/src/client/components/SkipLink/SkipLink.tsx +67 -0
- package/src/client/components/SkipLink/index.ts +7 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
- package/src/client/components/UnsavedChangesModal/index.ts +1 -0
- package/src/client/components/VersionHistory/DiffViewer.css +430 -0
- package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
- package/src/client/components/VersionHistory/VersionActions.css +318 -0
- package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
- package/src/client/components/VersionHistory/index.ts +9 -0
- package/src/client/context/ApiContext.tsx +154 -0
- package/src/client/context/ThemeContext.tsx +172 -0
- package/src/client/hooks/useAnnounce.ts +201 -0
- package/src/client/hooks/useApi.ts +374 -0
- package/src/client/hooks/useArrowNavigation.ts +286 -0
- package/src/client/hooks/useAutosave.ts +241 -0
- package/src/client/hooks/useFocusTrap.ts +178 -0
- package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
- package/src/client/hooks/useSearch.ts +206 -0
- package/src/client/hooks/useVersionHistory.ts +451 -0
- package/src/client/index.tsx +70 -0
- package/src/client/styles.css +584 -0
- package/src/client/utils/focus.ts +57 -0
- package/src/client/utils/openInEditor.ts +130 -0
- package/src/client/variables.css +304 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +32 -0
- package/src/config/loader.ts +174 -0
- package/src/config/schema.ts +161 -0
- package/src/core/constants.ts +39 -0
- package/src/core/errors.ts +739 -0
- package/src/core/index.ts +11 -0
- package/src/discovery/collections.ts +216 -0
- package/src/discovery/index.ts +33 -0
- package/src/discovery/patterns.ts +702 -0
- package/src/discovery/schema.ts +453 -0
- package/src/filesystem/images.ts +798 -0
- package/src/filesystem/index.ts +107 -0
- package/src/filesystem/reader.ts +452 -0
- package/src/filesystem/version-config.ts +390 -0
- package/src/filesystem/versions.ts +1339 -0
- package/src/filesystem/watcher.ts +226 -0
- package/src/filesystem/writer.ts +540 -0
- package/src/index.ts +61 -0
- package/src/integration.ts +228 -0
- package/src/server/assets.ts +254 -0
- package/src/server/cache.ts +355 -0
- package/src/server/index.ts +33 -0
- package/src/server/middleware.ts +209 -0
- package/src/server/routes.ts +1428 -0
- package/src/types/api.ts +61 -0
- package/src/types/config.ts +134 -0
- package/src/types/content.ts +64 -0
- package/src/types/image.ts +48 -0
- package/src/types/index.ts +58 -0
- package/src/types/version.ts +117 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Frontmatter form panel component for editing content metadata
|
|
3
|
+
*
|
|
4
|
+
* This component provides a collapsible panel on the right side for editing
|
|
5
|
+
* frontmatter fields. It supports schema-aware dynamic fields when a collection
|
|
6
|
+
* schema is available, or falls back to basic fields.
|
|
7
|
+
*
|
|
8
|
+
* @module @writenex/astro/client/components/FrontmatterForm
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useState } from "react";
|
|
12
|
+
import { X, Info, AlertCircle } from "lucide-react";
|
|
13
|
+
import type { CollectionSchema, SchemaField } from "../../../types";
|
|
14
|
+
import "./FrontmatterForm.css";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Props for the FrontmatterForm component
|
|
18
|
+
*/
|
|
19
|
+
interface FrontmatterFormProps {
|
|
20
|
+
/** Whether the panel is open */
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
/** Callback to close the panel */
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
/** Current frontmatter data */
|
|
25
|
+
frontmatter: Record<string, unknown> | null;
|
|
26
|
+
/** Collection schema for dynamic field generation */
|
|
27
|
+
schema?: CollectionSchema;
|
|
28
|
+
/** Callback when frontmatter changes */
|
|
29
|
+
onChange: (frontmatter: Record<string, unknown>) => void;
|
|
30
|
+
/** Whether the form is disabled */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Callback for image upload */
|
|
33
|
+
onImageUpload?: (file: File, fieldName: string) => Promise<string | null>;
|
|
34
|
+
/** Current collection name for image preview URLs */
|
|
35
|
+
collection?: string;
|
|
36
|
+
/** Current content ID for image preview URLs */
|
|
37
|
+
contentId?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Frontmatter form panel for editing content metadata
|
|
42
|
+
*
|
|
43
|
+
* @component
|
|
44
|
+
*/
|
|
45
|
+
export function FrontmatterForm({
|
|
46
|
+
isOpen,
|
|
47
|
+
onClose,
|
|
48
|
+
frontmatter,
|
|
49
|
+
schema,
|
|
50
|
+
onChange,
|
|
51
|
+
disabled = false,
|
|
52
|
+
onImageUpload,
|
|
53
|
+
collection,
|
|
54
|
+
contentId,
|
|
55
|
+
}: FrontmatterFormProps): React.ReactElement {
|
|
56
|
+
const handleFieldChange = useCallback(
|
|
57
|
+
(field: string, value: unknown) => {
|
|
58
|
+
if (!frontmatter) return;
|
|
59
|
+
onChange({ ...frontmatter, [field]: value });
|
|
60
|
+
},
|
|
61
|
+
[frontmatter, onChange]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const panelClassName = [
|
|
65
|
+
"wn-frontmatter-panel",
|
|
66
|
+
isOpen ? "wn-frontmatter-panel--open" : "wn-frontmatter-panel--closed",
|
|
67
|
+
]
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(" ");
|
|
70
|
+
|
|
71
|
+
const hasSchema = schema && Object.keys(schema).length > 0;
|
|
72
|
+
const fieldCount = hasSchema ? Object.keys(schema).length : 0;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<aside
|
|
76
|
+
className={panelClassName}
|
|
77
|
+
role="complementary"
|
|
78
|
+
aria-label="Frontmatter editor"
|
|
79
|
+
aria-hidden={!isOpen}
|
|
80
|
+
>
|
|
81
|
+
<div className="wn-frontmatter-panel-inner">
|
|
82
|
+
{/* Header */}
|
|
83
|
+
<div className="wn-frontmatter-header">
|
|
84
|
+
<h2 className="wn-frontmatter-title">
|
|
85
|
+
<Info size={14} />
|
|
86
|
+
Frontmatter
|
|
87
|
+
{frontmatter && (
|
|
88
|
+
<span className="wn-frontmatter-badge">
|
|
89
|
+
{hasSchema ? `${fieldCount} fields` : "Basic"}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
</h2>
|
|
93
|
+
<button
|
|
94
|
+
className="wn-frontmatter-close"
|
|
95
|
+
onClick={onClose}
|
|
96
|
+
title="Close panel"
|
|
97
|
+
aria-label="Close frontmatter panel"
|
|
98
|
+
>
|
|
99
|
+
<X size={12} />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Content */}
|
|
104
|
+
<div className="wn-frontmatter-content">
|
|
105
|
+
{!frontmatter ? (
|
|
106
|
+
<EmptyState />
|
|
107
|
+
) : hasSchema ? (
|
|
108
|
+
<SchemaFields
|
|
109
|
+
frontmatter={frontmatter}
|
|
110
|
+
schema={schema}
|
|
111
|
+
onChange={handleFieldChange}
|
|
112
|
+
disabled={disabled}
|
|
113
|
+
onImageUpload={onImageUpload}
|
|
114
|
+
collection={collection}
|
|
115
|
+
contentId={contentId}
|
|
116
|
+
/>
|
|
117
|
+
) : (
|
|
118
|
+
<BasicFields
|
|
119
|
+
frontmatter={frontmatter}
|
|
120
|
+
onChange={handleFieldChange}
|
|
121
|
+
disabled={disabled}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</aside>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Empty state when no content is selected
|
|
132
|
+
*/
|
|
133
|
+
function EmptyState(): React.ReactElement {
|
|
134
|
+
return (
|
|
135
|
+
<div className="wn-frontmatter-empty">
|
|
136
|
+
<div className="wn-frontmatter-empty-icon">
|
|
137
|
+
<AlertCircle size={32} strokeWidth={1.5} />
|
|
138
|
+
</div>
|
|
139
|
+
<p className="wn-frontmatter-empty-text">No content selected</p>
|
|
140
|
+
<p className="wn-frontmatter-empty-hint">
|
|
141
|
+
Select a content item from the sidebar to edit its frontmatter
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Priority order for common frontmatter fields.
|
|
149
|
+
* Lower number = higher priority (appears first).
|
|
150
|
+
*/
|
|
151
|
+
const FIELD_PRIORITY: Record<string, number> = {
|
|
152
|
+
title: 1,
|
|
153
|
+
name: 2,
|
|
154
|
+
description: 10,
|
|
155
|
+
excerpt: 11,
|
|
156
|
+
summary: 12,
|
|
157
|
+
date: 20,
|
|
158
|
+
pubDate: 21,
|
|
159
|
+
publishDate: 22,
|
|
160
|
+
updatedDate: 23,
|
|
161
|
+
modifiedDate: 24,
|
|
162
|
+
author: 30,
|
|
163
|
+
authors: 31,
|
|
164
|
+
category: 40,
|
|
165
|
+
categories: 41,
|
|
166
|
+
tags: 42,
|
|
167
|
+
image: 50,
|
|
168
|
+
hero: 51,
|
|
169
|
+
heroImage: 52,
|
|
170
|
+
heroAlt: 53,
|
|
171
|
+
cover: 54,
|
|
172
|
+
coverImage: 55,
|
|
173
|
+
thumbnail: 56,
|
|
174
|
+
draft: 90,
|
|
175
|
+
featured: 91,
|
|
176
|
+
published: 92,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get sort priority for a field name.
|
|
181
|
+
* Fields not in priority list get a default value of 100.
|
|
182
|
+
*/
|
|
183
|
+
function getFieldPriority(fieldName: string): number {
|
|
184
|
+
return FIELD_PRIORITY[fieldName] ?? 100;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Schema-aware dynamic fields
|
|
189
|
+
*/
|
|
190
|
+
function SchemaFields({
|
|
191
|
+
frontmatter,
|
|
192
|
+
schema,
|
|
193
|
+
onChange,
|
|
194
|
+
disabled,
|
|
195
|
+
onImageUpload,
|
|
196
|
+
collection,
|
|
197
|
+
contentId,
|
|
198
|
+
}: {
|
|
199
|
+
frontmatter: Record<string, unknown>;
|
|
200
|
+
schema: CollectionSchema;
|
|
201
|
+
onChange: (field: string, value: unknown) => void;
|
|
202
|
+
disabled: boolean;
|
|
203
|
+
onImageUpload?: (file: File, fieldName: string) => Promise<string | null>;
|
|
204
|
+
collection?: string;
|
|
205
|
+
contentId?: string;
|
|
206
|
+
}): React.ReactElement {
|
|
207
|
+
const sortedFields = Object.entries(schema).sort(
|
|
208
|
+
([aKey, aField], [bKey, bField]) => {
|
|
209
|
+
const aPriority = getFieldPriority(aKey);
|
|
210
|
+
const bPriority = getFieldPriority(bKey);
|
|
211
|
+
|
|
212
|
+
// Sort by priority first
|
|
213
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
214
|
+
|
|
215
|
+
// Then by required status
|
|
216
|
+
if (aField.required && !bField.required) return -1;
|
|
217
|
+
if (!aField.required && bField.required) return 1;
|
|
218
|
+
|
|
219
|
+
// Finally alphabetically
|
|
220
|
+
return aKey.localeCompare(bKey);
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div className="wn-frontmatter-fields">
|
|
226
|
+
{sortedFields.map(([fieldName, fieldDef]) => (
|
|
227
|
+
<DynamicField
|
|
228
|
+
key={fieldName}
|
|
229
|
+
name={fieldName}
|
|
230
|
+
field={fieldDef}
|
|
231
|
+
value={frontmatter[fieldName]}
|
|
232
|
+
onChange={(value) => onChange(fieldName, value)}
|
|
233
|
+
disabled={disabled}
|
|
234
|
+
onImageUpload={onImageUpload}
|
|
235
|
+
collection={collection}
|
|
236
|
+
contentId={contentId}
|
|
237
|
+
/>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Basic fallback fields when no schema is available
|
|
245
|
+
*/
|
|
246
|
+
function BasicFields({
|
|
247
|
+
frontmatter,
|
|
248
|
+
onChange,
|
|
249
|
+
disabled,
|
|
250
|
+
}: {
|
|
251
|
+
frontmatter: Record<string, unknown>;
|
|
252
|
+
onChange: (field: string, value: unknown) => void;
|
|
253
|
+
disabled: boolean;
|
|
254
|
+
}): React.ReactElement {
|
|
255
|
+
const handleTagsChange = (tagsString: string) => {
|
|
256
|
+
const tags = tagsString
|
|
257
|
+
.split(",")
|
|
258
|
+
.map((t) => t.trim())
|
|
259
|
+
.filter(Boolean);
|
|
260
|
+
onChange("tags", tags);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const title = String(frontmatter.title ?? "");
|
|
264
|
+
const description = String(frontmatter.description ?? "");
|
|
265
|
+
const pubDate = formatDateForInput(frontmatter.pubDate);
|
|
266
|
+
const updatedDate = formatDateForInput(frontmatter.updatedDate);
|
|
267
|
+
const draft = Boolean(frontmatter.draft);
|
|
268
|
+
const tags = Array.isArray(frontmatter.tags)
|
|
269
|
+
? frontmatter.tags.join(", ")
|
|
270
|
+
: "";
|
|
271
|
+
const heroImage = String(frontmatter.heroImage ?? "");
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<div className="wn-frontmatter-fields">
|
|
275
|
+
{/* Title */}
|
|
276
|
+
<div className="wn-frontmatter-field">
|
|
277
|
+
<label className="wn-frontmatter-label" htmlFor="fm-title">
|
|
278
|
+
Title<span className="wn-frontmatter-required">*</span>
|
|
279
|
+
</label>
|
|
280
|
+
<input
|
|
281
|
+
id="fm-title"
|
|
282
|
+
type="text"
|
|
283
|
+
className="wn-frontmatter-input"
|
|
284
|
+
value={title}
|
|
285
|
+
onChange={(e) => onChange("title", e.target.value)}
|
|
286
|
+
disabled={disabled}
|
|
287
|
+
placeholder="Enter title"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Description */}
|
|
292
|
+
<div className="wn-frontmatter-field">
|
|
293
|
+
<label className="wn-frontmatter-label" htmlFor="fm-description">
|
|
294
|
+
Description
|
|
295
|
+
</label>
|
|
296
|
+
<textarea
|
|
297
|
+
id="fm-description"
|
|
298
|
+
className="wn-frontmatter-textarea"
|
|
299
|
+
value={description}
|
|
300
|
+
onChange={(e) => onChange("description", e.target.value)}
|
|
301
|
+
disabled={disabled}
|
|
302
|
+
placeholder="Brief description"
|
|
303
|
+
rows={2}
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{/* Dates */}
|
|
308
|
+
<div className="wn-frontmatter-field--row">
|
|
309
|
+
<div className="wn-frontmatter-field">
|
|
310
|
+
<label className="wn-frontmatter-label" htmlFor="fm-pubDate">
|
|
311
|
+
Publish Date
|
|
312
|
+
</label>
|
|
313
|
+
<input
|
|
314
|
+
id="fm-pubDate"
|
|
315
|
+
type="date"
|
|
316
|
+
className="wn-frontmatter-input"
|
|
317
|
+
value={pubDate}
|
|
318
|
+
onChange={(e) => onChange("pubDate", e.target.value)}
|
|
319
|
+
disabled={disabled}
|
|
320
|
+
/>
|
|
321
|
+
</div>
|
|
322
|
+
<div className="wn-frontmatter-field">
|
|
323
|
+
<label className="wn-frontmatter-label" htmlFor="fm-updatedDate">
|
|
324
|
+
Updated Date
|
|
325
|
+
</label>
|
|
326
|
+
<input
|
|
327
|
+
id="fm-updatedDate"
|
|
328
|
+
type="date"
|
|
329
|
+
className="wn-frontmatter-input"
|
|
330
|
+
value={updatedDate}
|
|
331
|
+
onChange={(e) =>
|
|
332
|
+
onChange("updatedDate", e.target.value || undefined)
|
|
333
|
+
}
|
|
334
|
+
disabled={disabled}
|
|
335
|
+
/>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* Tags */}
|
|
340
|
+
<div className="wn-frontmatter-field">
|
|
341
|
+
<label className="wn-frontmatter-label" htmlFor="fm-tags">
|
|
342
|
+
Tags
|
|
343
|
+
</label>
|
|
344
|
+
<input
|
|
345
|
+
id="fm-tags"
|
|
346
|
+
type="text"
|
|
347
|
+
className="wn-frontmatter-input"
|
|
348
|
+
value={tags}
|
|
349
|
+
onChange={(e) => handleTagsChange(e.target.value)}
|
|
350
|
+
disabled={disabled}
|
|
351
|
+
placeholder="tag1, tag2, tag3"
|
|
352
|
+
/>
|
|
353
|
+
<span className="wn-frontmatter-hint">Separate with commas</span>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{/* Hero Image */}
|
|
357
|
+
<div className="wn-frontmatter-field">
|
|
358
|
+
<label className="wn-frontmatter-label" htmlFor="fm-heroImage">
|
|
359
|
+
Hero Image
|
|
360
|
+
</label>
|
|
361
|
+
<input
|
|
362
|
+
id="fm-heroImage"
|
|
363
|
+
type="text"
|
|
364
|
+
className="wn-frontmatter-input"
|
|
365
|
+
value={heroImage}
|
|
366
|
+
onChange={(e) => onChange("heroImage", e.target.value || undefined)}
|
|
367
|
+
disabled={disabled}
|
|
368
|
+
placeholder="./images/hero.jpg"
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<div className="wn-frontmatter-divider" />
|
|
373
|
+
|
|
374
|
+
{/* Draft */}
|
|
375
|
+
<div className="wn-frontmatter-checkbox-field">
|
|
376
|
+
<label className="wn-frontmatter-checkbox-label">
|
|
377
|
+
<input
|
|
378
|
+
type="checkbox"
|
|
379
|
+
className="wn-frontmatter-checkbox"
|
|
380
|
+
checked={draft}
|
|
381
|
+
onChange={(e) => onChange("draft", e.target.checked)}
|
|
382
|
+
disabled={disabled}
|
|
383
|
+
/>
|
|
384
|
+
<span>Draft</span>
|
|
385
|
+
</label>
|
|
386
|
+
<span className="wn-frontmatter-checkbox-hint">Not published</span>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Dynamic field renderer based on schema type
|
|
394
|
+
*/
|
|
395
|
+
function DynamicField({
|
|
396
|
+
name,
|
|
397
|
+
field,
|
|
398
|
+
value,
|
|
399
|
+
onChange,
|
|
400
|
+
disabled,
|
|
401
|
+
onImageUpload,
|
|
402
|
+
collection,
|
|
403
|
+
contentId,
|
|
404
|
+
}: {
|
|
405
|
+
name: string;
|
|
406
|
+
field: SchemaField;
|
|
407
|
+
value: unknown;
|
|
408
|
+
onChange: (value: unknown) => void;
|
|
409
|
+
disabled: boolean;
|
|
410
|
+
onImageUpload?: (file: File, fieldName: string) => Promise<string | null>;
|
|
411
|
+
collection?: string;
|
|
412
|
+
contentId?: string;
|
|
413
|
+
}): React.ReactElement {
|
|
414
|
+
const fieldId = `fm-${name}`;
|
|
415
|
+
const label = formatFieldLabel(name);
|
|
416
|
+
const enumOptions = parseEnumFromDescription(field.description);
|
|
417
|
+
|
|
418
|
+
switch (field.type) {
|
|
419
|
+
case "boolean":
|
|
420
|
+
return (
|
|
421
|
+
<BooleanField
|
|
422
|
+
id={fieldId}
|
|
423
|
+
label={label}
|
|
424
|
+
value={Boolean(value ?? field.default)}
|
|
425
|
+
onChange={onChange}
|
|
426
|
+
disabled={disabled}
|
|
427
|
+
/>
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
case "number":
|
|
431
|
+
return (
|
|
432
|
+
<NumberField
|
|
433
|
+
id={fieldId}
|
|
434
|
+
label={label}
|
|
435
|
+
value={value as number | undefined}
|
|
436
|
+
onChange={onChange}
|
|
437
|
+
disabled={disabled}
|
|
438
|
+
required={field.required}
|
|
439
|
+
/>
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
case "date":
|
|
443
|
+
return (
|
|
444
|
+
<DateField
|
|
445
|
+
id={fieldId}
|
|
446
|
+
label={label}
|
|
447
|
+
value={value}
|
|
448
|
+
onChange={onChange}
|
|
449
|
+
disabled={disabled}
|
|
450
|
+
required={field.required}
|
|
451
|
+
/>
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
case "array":
|
|
455
|
+
return (
|
|
456
|
+
<ArrayField
|
|
457
|
+
id={fieldId}
|
|
458
|
+
label={label}
|
|
459
|
+
value={value as unknown[] | undefined}
|
|
460
|
+
itemType={field.items}
|
|
461
|
+
onChange={onChange}
|
|
462
|
+
disabled={disabled}
|
|
463
|
+
required={field.required}
|
|
464
|
+
/>
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
case "image":
|
|
468
|
+
return (
|
|
469
|
+
<ImageField
|
|
470
|
+
id={fieldId}
|
|
471
|
+
label={label}
|
|
472
|
+
value={value as string | undefined}
|
|
473
|
+
onChange={onChange}
|
|
474
|
+
disabled={disabled}
|
|
475
|
+
required={field.required}
|
|
476
|
+
onUpload={
|
|
477
|
+
onImageUpload ? (file) => onImageUpload(file, name) : undefined
|
|
478
|
+
}
|
|
479
|
+
collection={collection}
|
|
480
|
+
contentId={contentId}
|
|
481
|
+
/>
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
case "string":
|
|
485
|
+
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
|
+
const isMultiline =
|
|
501
|
+
name === "description" || name === "excerpt" || name === "summary";
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<StringField
|
|
505
|
+
id={fieldId}
|
|
506
|
+
label={label}
|
|
507
|
+
value={String(value ?? "")}
|
|
508
|
+
onChange={onChange}
|
|
509
|
+
disabled={disabled}
|
|
510
|
+
required={field.required}
|
|
511
|
+
multiline={isMultiline}
|
|
512
|
+
/>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Field Components
|
|
518
|
+
|
|
519
|
+
interface BaseFieldProps {
|
|
520
|
+
id: string;
|
|
521
|
+
label: string;
|
|
522
|
+
disabled: boolean;
|
|
523
|
+
required?: boolean;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function StringField({
|
|
527
|
+
id,
|
|
528
|
+
label,
|
|
529
|
+
value,
|
|
530
|
+
onChange,
|
|
531
|
+
disabled,
|
|
532
|
+
required,
|
|
533
|
+
multiline,
|
|
534
|
+
}: BaseFieldProps & {
|
|
535
|
+
value: string;
|
|
536
|
+
onChange: (value: string) => void;
|
|
537
|
+
multiline?: boolean;
|
|
538
|
+
}): React.ReactElement {
|
|
539
|
+
return (
|
|
540
|
+
<div className="wn-frontmatter-field">
|
|
541
|
+
<label htmlFor={id} className="wn-frontmatter-label">
|
|
542
|
+
{label}
|
|
543
|
+
{required && <span className="wn-frontmatter-required">*</span>}
|
|
544
|
+
</label>
|
|
545
|
+
{multiline ? (
|
|
546
|
+
<textarea
|
|
547
|
+
id={id}
|
|
548
|
+
value={value}
|
|
549
|
+
onChange={(e) => onChange(e.target.value)}
|
|
550
|
+
disabled={disabled}
|
|
551
|
+
placeholder={`Enter ${label.toLowerCase()}`}
|
|
552
|
+
rows={2}
|
|
553
|
+
className="wn-frontmatter-textarea"
|
|
554
|
+
/>
|
|
555
|
+
) : (
|
|
556
|
+
<input
|
|
557
|
+
id={id}
|
|
558
|
+
type="text"
|
|
559
|
+
value={value}
|
|
560
|
+
onChange={(e) => onChange(e.target.value)}
|
|
561
|
+
disabled={disabled}
|
|
562
|
+
placeholder={`Enter ${label.toLowerCase()}`}
|
|
563
|
+
className="wn-frontmatter-input"
|
|
564
|
+
/>
|
|
565
|
+
)}
|
|
566
|
+
</div>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function NumberField({
|
|
571
|
+
id,
|
|
572
|
+
label,
|
|
573
|
+
value,
|
|
574
|
+
onChange,
|
|
575
|
+
disabled,
|
|
576
|
+
required,
|
|
577
|
+
}: BaseFieldProps & {
|
|
578
|
+
value: number | undefined;
|
|
579
|
+
onChange: (value: number | undefined) => void;
|
|
580
|
+
}): React.ReactElement {
|
|
581
|
+
return (
|
|
582
|
+
<div className="wn-frontmatter-field">
|
|
583
|
+
<label htmlFor={id} className="wn-frontmatter-label">
|
|
584
|
+
{label}
|
|
585
|
+
{required && <span className="wn-frontmatter-required">*</span>}
|
|
586
|
+
</label>
|
|
587
|
+
<input
|
|
588
|
+
id={id}
|
|
589
|
+
type="number"
|
|
590
|
+
value={value ?? ""}
|
|
591
|
+
onChange={(e) => {
|
|
592
|
+
const val = e.target.value;
|
|
593
|
+
onChange(val === "" ? undefined : Number(val));
|
|
594
|
+
}}
|
|
595
|
+
disabled={disabled}
|
|
596
|
+
placeholder="0"
|
|
597
|
+
className="wn-frontmatter-input"
|
|
598
|
+
/>
|
|
599
|
+
</div>
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function BooleanField({
|
|
604
|
+
id,
|
|
605
|
+
label,
|
|
606
|
+
value,
|
|
607
|
+
onChange,
|
|
608
|
+
disabled,
|
|
609
|
+
}: BaseFieldProps & {
|
|
610
|
+
value: boolean;
|
|
611
|
+
onChange: (value: boolean) => void;
|
|
612
|
+
}): React.ReactElement {
|
|
613
|
+
return (
|
|
614
|
+
<div className="wn-frontmatter-checkbox-field">
|
|
615
|
+
<label className="wn-frontmatter-checkbox-label">
|
|
616
|
+
<input
|
|
617
|
+
id={id}
|
|
618
|
+
type="checkbox"
|
|
619
|
+
checked={value}
|
|
620
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
621
|
+
disabled={disabled}
|
|
622
|
+
className="wn-frontmatter-checkbox"
|
|
623
|
+
/>
|
|
624
|
+
<span>{label}</span>
|
|
625
|
+
</label>
|
|
626
|
+
</div>
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function DateField({
|
|
631
|
+
id,
|
|
632
|
+
label,
|
|
633
|
+
value,
|
|
634
|
+
onChange,
|
|
635
|
+
disabled,
|
|
636
|
+
required,
|
|
637
|
+
}: BaseFieldProps & {
|
|
638
|
+
value: unknown;
|
|
639
|
+
onChange: (value: string | undefined) => void;
|
|
640
|
+
}): React.ReactElement {
|
|
641
|
+
const dateValue = formatDateForInput(value);
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<div className="wn-frontmatter-field">
|
|
645
|
+
<label htmlFor={id} className="wn-frontmatter-label">
|
|
646
|
+
{label}
|
|
647
|
+
{required && <span className="wn-frontmatter-required">*</span>}
|
|
648
|
+
</label>
|
|
649
|
+
<input
|
|
650
|
+
id={id}
|
|
651
|
+
type="date"
|
|
652
|
+
value={dateValue}
|
|
653
|
+
onChange={(e) => onChange(e.target.value || undefined)}
|
|
654
|
+
disabled={disabled}
|
|
655
|
+
className="wn-frontmatter-input"
|
|
656
|
+
/>
|
|
657
|
+
</div>
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function SelectField({
|
|
662
|
+
id,
|
|
663
|
+
label,
|
|
664
|
+
value,
|
|
665
|
+
options,
|
|
666
|
+
onChange,
|
|
667
|
+
disabled,
|
|
668
|
+
required,
|
|
669
|
+
}: BaseFieldProps & {
|
|
670
|
+
value: string;
|
|
671
|
+
options: string[];
|
|
672
|
+
onChange: (value: string) => void;
|
|
673
|
+
}): React.ReactElement {
|
|
674
|
+
return (
|
|
675
|
+
<div className="wn-frontmatter-field">
|
|
676
|
+
<label htmlFor={id} className="wn-frontmatter-label">
|
|
677
|
+
{label}
|
|
678
|
+
{required && <span className="wn-frontmatter-required">*</span>}
|
|
679
|
+
</label>
|
|
680
|
+
<select
|
|
681
|
+
id={id}
|
|
682
|
+
value={value}
|
|
683
|
+
onChange={(e) => onChange(e.target.value)}
|
|
684
|
+
disabled={disabled}
|
|
685
|
+
className="wn-frontmatter-select"
|
|
686
|
+
>
|
|
687
|
+
<option value="">Select {label.toLowerCase()}</option>
|
|
688
|
+
{options.map((opt) => (
|
|
689
|
+
<option key={opt} value={opt}>
|
|
690
|
+
{opt}
|
|
691
|
+
</option>
|
|
692
|
+
))}
|
|
693
|
+
</select>
|
|
694
|
+
</div>
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function ArrayField({
|
|
699
|
+
id,
|
|
700
|
+
label,
|
|
701
|
+
value,
|
|
702
|
+
itemType,
|
|
703
|
+
onChange,
|
|
704
|
+
disabled,
|
|
705
|
+
required,
|
|
706
|
+
}: BaseFieldProps & {
|
|
707
|
+
value: unknown[] | undefined;
|
|
708
|
+
itemType?: string;
|
|
709
|
+
onChange: (value: unknown[]) => void;
|
|
710
|
+
}): React.ReactElement {
|
|
711
|
+
const [inputValue, setInputValue] = useState("");
|
|
712
|
+
const items = Array.isArray(value) ? value : [];
|
|
713
|
+
|
|
714
|
+
const handleAdd = () => {
|
|
715
|
+
if (!inputValue.trim()) return;
|
|
716
|
+
let newItem: unknown = inputValue.trim();
|
|
717
|
+
if (itemType === "number") {
|
|
718
|
+
newItem = Number(newItem);
|
|
719
|
+
}
|
|
720
|
+
onChange([...items, newItem]);
|
|
721
|
+
setInputValue("");
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const handleRemove = (index: number) => {
|
|
725
|
+
onChange(items.filter((_, i) => i !== index));
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
729
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
730
|
+
e.preventDefault();
|
|
731
|
+
handleAdd();
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
return (
|
|
736
|
+
<div className="wn-frontmatter-field">
|
|
737
|
+
<label htmlFor={id} className="wn-frontmatter-label">
|
|
738
|
+
{label}
|
|
739
|
+
{required && <span className="wn-frontmatter-required">*</span>}
|
|
740
|
+
</label>
|
|
741
|
+
{items.length > 0 && (
|
|
742
|
+
<div className="wn-frontmatter-tags">
|
|
743
|
+
{items.map((item, index) => (
|
|
744
|
+
<span key={index} className="wn-frontmatter-tag">
|
|
745
|
+
{String(item)}
|
|
746
|
+
<button
|
|
747
|
+
type="button"
|
|
748
|
+
onClick={() => handleRemove(index)}
|
|
749
|
+
disabled={disabled}
|
|
750
|
+
className="wn-frontmatter-tag-remove"
|
|
751
|
+
>
|
|
752
|
+
<X size={10} />
|
|
753
|
+
</button>
|
|
754
|
+
</span>
|
|
755
|
+
))}
|
|
756
|
+
</div>
|
|
757
|
+
)}
|
|
758
|
+
<input
|
|
759
|
+
id={id}
|
|
760
|
+
type={itemType === "number" ? "number" : "text"}
|
|
761
|
+
value={inputValue}
|
|
762
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
763
|
+
onKeyDown={handleKeyDown}
|
|
764
|
+
onBlur={handleAdd}
|
|
765
|
+
disabled={disabled}
|
|
766
|
+
placeholder="Type and press Enter"
|
|
767
|
+
className="wn-frontmatter-input"
|
|
768
|
+
/>
|
|
769
|
+
</div>
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function ImageField({
|
|
774
|
+
id,
|
|
775
|
+
label,
|
|
776
|
+
value,
|
|
777
|
+
onChange,
|
|
778
|
+
disabled,
|
|
779
|
+
required,
|
|
780
|
+
onUpload,
|
|
781
|
+
collection,
|
|
782
|
+
contentId,
|
|
783
|
+
}: BaseFieldProps & {
|
|
784
|
+
value: string | undefined;
|
|
785
|
+
onChange: (value: string | undefined) => void;
|
|
786
|
+
onUpload?: (file: File) => Promise<string | null>;
|
|
787
|
+
collection?: string;
|
|
788
|
+
contentId?: string;
|
|
789
|
+
}): React.ReactElement {
|
|
790
|
+
const [uploading, setUploading] = useState(false);
|
|
791
|
+
const [previewError, setPreviewError] = useState(false);
|
|
792
|
+
|
|
793
|
+
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
794
|
+
const file = e.target.files?.[0];
|
|
795
|
+
if (!file || !onUpload) return;
|
|
796
|
+
|
|
797
|
+
setUploading(true);
|
|
798
|
+
setPreviewError(false);
|
|
799
|
+
try {
|
|
800
|
+
const path = await onUpload(file);
|
|
801
|
+
if (path) {
|
|
802
|
+
onChange(path);
|
|
803
|
+
}
|
|
804
|
+
} finally {
|
|
805
|
+
setUploading(false);
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const handleValueChange = (newValue: string) => {
|
|
810
|
+
setPreviewError(false);
|
|
811
|
+
onChange(newValue || undefined);
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
// Build preview URL from relative path
|
|
815
|
+
// URL format: /_writenex/api/images/:collection/:contentId/:imagePath
|
|
816
|
+
const getPreviewUrl = (): string | null => {
|
|
817
|
+
if (!value || !collection || !contentId) return null;
|
|
818
|
+
|
|
819
|
+
// Remove leading ./ from path if present
|
|
820
|
+
const imagePath = value.replace(/^\.\//, "");
|
|
821
|
+
return `/_writenex/api/images/${collection}/${contentId}/${imagePath}`;
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const previewUrl = getPreviewUrl();
|
|
825
|
+
const showPreview = previewUrl && !previewError;
|
|
826
|
+
|
|
827
|
+
return (
|
|
828
|
+
<div className="wn-frontmatter-field">
|
|
829
|
+
<label htmlFor={id} className="wn-frontmatter-label">
|
|
830
|
+
{label}
|
|
831
|
+
{required && <span className="wn-frontmatter-required">*</span>}
|
|
832
|
+
</label>
|
|
833
|
+
|
|
834
|
+
{/* Image Preview */}
|
|
835
|
+
{showPreview && (
|
|
836
|
+
<div className="wn-frontmatter-image-preview">
|
|
837
|
+
<img
|
|
838
|
+
src={previewUrl}
|
|
839
|
+
alt={`Preview for ${label}`}
|
|
840
|
+
onError={() => setPreviewError(true)}
|
|
841
|
+
/>
|
|
842
|
+
</div>
|
|
843
|
+
)}
|
|
844
|
+
|
|
845
|
+
<div className="wn-frontmatter-image-field">
|
|
846
|
+
<input
|
|
847
|
+
id={id}
|
|
848
|
+
type="text"
|
|
849
|
+
value={value ?? ""}
|
|
850
|
+
onChange={(e) => handleValueChange(e.target.value)}
|
|
851
|
+
disabled={disabled}
|
|
852
|
+
placeholder="./images/hero.jpg"
|
|
853
|
+
className="wn-frontmatter-input"
|
|
854
|
+
/>
|
|
855
|
+
{onUpload && (
|
|
856
|
+
<label className="wn-frontmatter-upload-btn">
|
|
857
|
+
<input
|
|
858
|
+
type="file"
|
|
859
|
+
accept="image/*"
|
|
860
|
+
onChange={handleFileChange}
|
|
861
|
+
disabled={disabled || uploading}
|
|
862
|
+
style={{ display: "none" }}
|
|
863
|
+
/>
|
|
864
|
+
{uploading ? "..." : "Upload"}
|
|
865
|
+
</label>
|
|
866
|
+
)}
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Utilities
|
|
873
|
+
|
|
874
|
+
function formatFieldLabel(name: string): string {
|
|
875
|
+
return name
|
|
876
|
+
.replace(/([A-Z])/g, " $1")
|
|
877
|
+
.replace(/[_-]/g, " ")
|
|
878
|
+
.replace(/^\w/, (c) => c.toUpperCase())
|
|
879
|
+
.trim();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function formatDateForInput(value: unknown): string {
|
|
883
|
+
if (!value) return "";
|
|
884
|
+
|
|
885
|
+
if (typeof value === "string") {
|
|
886
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
const date = new Date(value);
|
|
891
|
+
if (!isNaN(date.getTime())) {
|
|
892
|
+
return date.toISOString().split("T")[0] ?? "";
|
|
893
|
+
}
|
|
894
|
+
} catch {
|
|
895
|
+
return "";
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (value instanceof Date) {
|
|
900
|
+
return value.toISOString().split("T")[0] ?? "";
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return "";
|
|
904
|
+
}
|
|
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
|
+
}
|