@nuasite/cms 0.39.1 → 0.40.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/dist/editor.js +14575 -13938
- package/package.json +1 -1
- package/src/build-processor.ts +1 -1
- package/src/collection-scanner.ts +49 -2
- package/src/dev-middleware.ts +1 -1
- package/src/editor/components/attribute-editor.tsx +0 -1
- package/src/editor/components/bg-image-overlay.tsx +7 -8
- package/src/editor/components/block-editor.tsx +12 -12
- package/src/editor/components/collections-browser.tsx +10 -10
- package/src/editor/components/create-page-modal.tsx +18 -18
- package/src/editor/components/delete-page-dialog.tsx +4 -3
- package/src/editor/components/field-utils.ts +54 -0
- package/src/editor/components/fields.tsx +254 -72
- package/src/editor/components/frontmatter-fields.tsx +135 -54
- package/src/editor/components/frontmatter-sidebar.tsx +55 -58
- package/src/editor/components/link-edit-popover.tsx +10 -5
- package/src/editor/components/markdown-editor-overlay.tsx +100 -39
- package/src/editor/components/markdown-inline-editor.tsx +58 -26
- package/src/editor/components/mdx-block-view.tsx +4 -4
- package/src/editor/components/mdx-component-picker.tsx +2 -2
- package/src/editor/components/media-library.tsx +19 -18
- package/src/editor/components/modal-shell.tsx +16 -3
- package/src/editor/components/prop-editor.tsx +15 -18
- package/src/editor/components/redirects-manager.tsx +42 -35
- package/src/editor/components/reference-picker.tsx +5 -4
- package/src/editor/components/seo-editor.tsx +36 -27
- package/src/editor/components/toolbar.tsx +50 -33
- package/src/editor/dom.ts +13 -2
- package/src/editor/editor.ts +7 -6
- package/src/editor/hooks/useBlockEditorHandlers.ts +7 -6
- package/src/editor/index.tsx +7 -6
- package/src/editor/signals.ts +44 -13
- package/src/editor/strings.ts +123 -0
- package/src/editor/styles.css +75 -2
- package/src/editor/types.ts +8 -0
- package/src/index.ts +6 -0
- package/src/source-finder/image-finder.ts +1 -1
- package/src/source-finder/search-index.ts +12 -4
- package/src/source-finder/snippet-utils.ts +4 -1
- package/src/types.ts +4 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentChildren } from 'preact'
|
|
2
2
|
import { useEffect, useState } from 'preact/hooks'
|
|
3
|
+
import { cn } from '../lib/cn'
|
|
3
4
|
import { buildAstroUploadContext, getCollectionEntryOptions } from '../manifest'
|
|
4
5
|
import { renameMarkdownPage } from '../markdown-api'
|
|
5
6
|
import {
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
updateMarkdownFrontmatter,
|
|
12
13
|
updateMarkdownPageMeta,
|
|
13
14
|
} from '../signals'
|
|
15
|
+
import { STRINGS } from '../strings'
|
|
14
16
|
import type { CollectionDefinition, FieldDefinition, MarkdownPageEntry } from '../types'
|
|
15
17
|
import { ColorField, ComboBoxField, ImageField, MultiSelectField, NumberField, TextField, ToggleField } from './fields'
|
|
16
18
|
import { groupFields } from './frontmatter-sidebar'
|
|
@@ -19,6 +21,37 @@ function isArrayOfObjects(value: unknown[]): value is Record<string, unknown>[]
|
|
|
19
21
|
return value.length > 0 && typeof value[0] === 'object' && value[0] !== null
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/** Checkbox below a URL field that toggles opening the link in a new tab. */
|
|
25
|
+
function OpenInNewTabToggle({ field }: { field: FieldDefinition }) {
|
|
26
|
+
const fm = markdownEditorState.value.currentPage?.frontmatter ?? {}
|
|
27
|
+
const targetKey = `${field.name}OpenInNewTab`
|
|
28
|
+
const isChecked = fm[targetKey] === true
|
|
29
|
+
return (
|
|
30
|
+
<label class="flex items-center gap-2 mt-2 text-xs text-white/70 cursor-pointer w-fit" data-cms-ui>
|
|
31
|
+
<input
|
|
32
|
+
type="checkbox"
|
|
33
|
+
checked={isChecked}
|
|
34
|
+
onChange={(e) => updateMarkdownFrontmatter({ [targetKey]: (e.target as HTMLInputElement).checked })}
|
|
35
|
+
class="sr-only peer"
|
|
36
|
+
data-cms-ui
|
|
37
|
+
/>
|
|
38
|
+
<span
|
|
39
|
+
class={cn(
|
|
40
|
+
'w-4 h-4 rounded border flex items-center justify-center shrink-0 transition-colors',
|
|
41
|
+
isChecked ? 'bg-cms-primary border-cms-primary' : 'border-white/30 bg-white/5',
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{isChecked && (
|
|
45
|
+
<svg class="w-3 h-3 text-cms-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
46
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
|
47
|
+
</svg>
|
|
48
|
+
)}
|
|
49
|
+
</span>
|
|
50
|
+
Open in new tab
|
|
51
|
+
</label>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
22
55
|
function FieldGroupHeader({ group, children }: { group: string | null; children: ComponentChildren }) {
|
|
23
56
|
return (
|
|
24
57
|
<>
|
|
@@ -66,9 +99,21 @@ export function FrontmatterField({
|
|
|
66
99
|
type="checkbox"
|
|
67
100
|
checked={value}
|
|
68
101
|
onChange={(e) => onChange((e.target as HTMLInputElement).checked)}
|
|
69
|
-
class="
|
|
102
|
+
class="sr-only peer"
|
|
70
103
|
data-cms-ui
|
|
71
104
|
/>
|
|
105
|
+
<span
|
|
106
|
+
class={cn(
|
|
107
|
+
'w-4 h-4 rounded border flex items-center justify-center shrink-0 transition-colors',
|
|
108
|
+
value ? 'bg-cms-primary border-cms-primary' : 'border-white/30 bg-white/5',
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{value && (
|
|
112
|
+
<svg class="w-3 h-3 text-cms-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
113
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
|
114
|
+
</svg>
|
|
115
|
+
)}
|
|
116
|
+
</span>
|
|
72
117
|
{label}
|
|
73
118
|
</label>
|
|
74
119
|
)
|
|
@@ -83,7 +128,7 @@ export function FrontmatterField({
|
|
|
83
128
|
type="date"
|
|
84
129
|
value={typeof value === 'string' ? value.split('T')[0] : ''}
|
|
85
130
|
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
86
|
-
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white focus:outline-none focus:border-
|
|
131
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white focus:outline-none focus:border-white/40"
|
|
87
132
|
data-cms-ui
|
|
88
133
|
/>
|
|
89
134
|
</div>
|
|
@@ -119,7 +164,7 @@ export function FrontmatterField({
|
|
|
119
164
|
onChange(arrayValue)
|
|
120
165
|
}}
|
|
121
166
|
placeholder={`Enter ${label.toLowerCase()} separated by commas`}
|
|
122
|
-
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-
|
|
167
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40"
|
|
123
168
|
data-cms-ui
|
|
124
169
|
/>
|
|
125
170
|
</div>
|
|
@@ -150,7 +195,7 @@ export function FrontmatterField({
|
|
|
150
195
|
value={typeof value === 'string' ? value : ''}
|
|
151
196
|
onChange={(e) => onChange((e.target as HTMLTextAreaElement).value)}
|
|
152
197
|
rows={3}
|
|
153
|
-
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-
|
|
198
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40 resize-none"
|
|
154
199
|
data-cms-ui
|
|
155
200
|
/>
|
|
156
201
|
</div>
|
|
@@ -165,7 +210,7 @@ export function FrontmatterField({
|
|
|
165
210
|
type="text"
|
|
166
211
|
value={typeof value === 'string' ? value : String(value ?? '')}
|
|
167
212
|
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
168
|
-
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-
|
|
213
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40"
|
|
169
214
|
data-cms-ui
|
|
170
215
|
/>
|
|
171
216
|
</div>
|
|
@@ -190,38 +235,53 @@ export function CreateModeFrontmatter({
|
|
|
190
235
|
onSlugManualEdit,
|
|
191
236
|
}: CreateModeFrontmatterProps) {
|
|
192
237
|
const allFields = fields ?? collectionDefinition.fields
|
|
238
|
+
const urlFieldNames = new Set(allFields.filter((f) => f.type === 'url' || /link|href|url/i.test(f.name)).map((f) => f.name))
|
|
239
|
+
const isOpenInNewTabSibling = (name: string) => {
|
|
240
|
+
if (!name.endsWith('OpenInNewTab')) return false
|
|
241
|
+
const base = name.slice(0, -'OpenInNewTab'.length)
|
|
242
|
+
return urlFieldNames.has(base)
|
|
243
|
+
}
|
|
193
244
|
// In create mode, skip complex fields (arrays, objects) — they can be edited after creation
|
|
194
|
-
|
|
245
|
+
// Draft is always rendered in the sidebar — never inline in the header.
|
|
246
|
+
// `*OpenInNewTab` siblings are handled by the OpenInNewTabToggle next to the URL field.
|
|
247
|
+
const displayFields = allFields.filter(f => f.type !== 'array' && f.type !== 'object' && f.name !== 'draft' && !isOpenInNewTabSibling(f.name))
|
|
195
248
|
const groups = groupFields(displayFields)
|
|
196
249
|
|
|
197
250
|
return (
|
|
198
251
|
<div class="space-y-4">
|
|
199
252
|
{/* Slug field */}
|
|
200
253
|
<div>
|
|
201
|
-
<
|
|
202
|
-
URL Slug
|
|
254
|
+
<div class="flex items-center gap-1.5 mb-1.5">
|
|
255
|
+
<label class="block text-xs font-medium text-white/70">URL Slug</label>
|
|
256
|
+
<span class="relative group/tt inline-flex" data-cms-ui>
|
|
257
|
+
<svg class="w-3.5 h-3.5 text-white/40 hover:text-white/70 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
258
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
259
|
+
</svg>
|
|
260
|
+
<span class="absolute left-0 top-full mt-1 w-72 p-2 bg-black/90 text-white text-xs rounded-cms-sm opacity-0 invisible group-hover/tt:opacity-100 group-hover/tt:visible transition-all z-50 pointer-events-none whitespace-normal break-all">
|
|
261
|
+
Will be saved to: src/content/{collectionDefinition.name}/{page.slug || 'your-slug'}.{collectionDefinition.fileExtension}
|
|
262
|
+
</span>
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
<label class="flex items-center gap-1 px-3 py-2 bg-white/10 border border-white/20 rounded-cms-sm focus-within:border-white/40 focus-within:ring-1 focus-within:ring-white/10">
|
|
266
|
+
<span class="text-white/40 text-sm shrink-0">/</span>
|
|
267
|
+
<input
|
|
268
|
+
type="text"
|
|
269
|
+
value={page.slug}
|
|
270
|
+
onInput={(e) => {
|
|
271
|
+
onSlugManualEdit()
|
|
272
|
+
const slug = (e.target as HTMLInputElement).value
|
|
273
|
+
markdownEditorState.value = {
|
|
274
|
+
...markdownEditorState.value,
|
|
275
|
+
currentPage: markdownEditorState.value.currentPage
|
|
276
|
+
? { ...markdownEditorState.value.currentPage, slug }
|
|
277
|
+
: null,
|
|
278
|
+
}
|
|
279
|
+
}}
|
|
280
|
+
placeholder="url-friendly-slug"
|
|
281
|
+
class="flex-1 bg-transparent text-sm text-white placeholder-white/40 focus:outline-none"
|
|
282
|
+
data-cms-ui
|
|
283
|
+
/>
|
|
203
284
|
</label>
|
|
204
|
-
<input
|
|
205
|
-
type="text"
|
|
206
|
-
value={page.slug}
|
|
207
|
-
onInput={(e) => {
|
|
208
|
-
onSlugManualEdit()
|
|
209
|
-
const slug = (e.target as HTMLInputElement).value
|
|
210
|
-
markdownEditorState.value = {
|
|
211
|
-
...markdownEditorState.value,
|
|
212
|
-
currentPage: markdownEditorState.value.currentPage
|
|
213
|
-
? { ...markdownEditorState.value.currentPage, slug }
|
|
214
|
-
: null,
|
|
215
|
-
}
|
|
216
|
-
}}
|
|
217
|
-
placeholder="url-friendly-slug"
|
|
218
|
-
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-cms-sm text-sm text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10"
|
|
219
|
-
data-cms-ui
|
|
220
|
-
/>
|
|
221
|
-
<p class="mt-1 text-xs text-white/40">
|
|
222
|
-
Will be saved to: src/content/{collectionDefinition.name}/
|
|
223
|
-
{page.slug || 'your-slug'}.{collectionDefinition.fileExtension}
|
|
224
|
-
</p>
|
|
225
285
|
</div>
|
|
226
286
|
|
|
227
287
|
{/* Schema fields */}
|
|
@@ -271,13 +331,13 @@ function SlugField({ page }: { page: MarkdownPageEntry }) {
|
|
|
271
331
|
if (result.success && result.newSlug && result.newFilePath) {
|
|
272
332
|
updateMarkdownPageMeta({ slug: result.newSlug, filePath: result.newFilePath })
|
|
273
333
|
setLocalSlug(result.newSlug)
|
|
274
|
-
showToast(
|
|
334
|
+
showToast(STRINGS.slug.updated, 'success')
|
|
275
335
|
} else {
|
|
276
|
-
showToast(result.error ||
|
|
336
|
+
showToast(result.error || STRINGS.slug.renameFailed, 'error')
|
|
277
337
|
setLocalSlug(page.slug)
|
|
278
338
|
}
|
|
279
339
|
} catch {
|
|
280
|
-
showToast(
|
|
340
|
+
showToast(STRINGS.slug.renameFailed, 'error')
|
|
281
341
|
setLocalSlug(page.slug)
|
|
282
342
|
} finally {
|
|
283
343
|
setIsRenaming(false)
|
|
@@ -289,7 +349,14 @@ function SlugField({ page }: { page: MarkdownPageEntry }) {
|
|
|
289
349
|
<label class="block text-xs font-medium text-white/70 mb-1.5">
|
|
290
350
|
URL Slug
|
|
291
351
|
</label>
|
|
292
|
-
<
|
|
352
|
+
<label
|
|
353
|
+
class={cn(
|
|
354
|
+
'flex items-center gap-1 px-3 py-2 bg-white/10 border rounded-cms-sm text-sm text-white focus-within:border-white/40',
|
|
355
|
+
isDirty ? 'border-white/30' : 'border-white/20',
|
|
356
|
+
isRenaming && 'opacity-60',
|
|
357
|
+
)}
|
|
358
|
+
>
|
|
359
|
+
<span class="text-white/40 text-sm shrink-0">/</span>
|
|
293
360
|
<input
|
|
294
361
|
type="text"
|
|
295
362
|
value={localSlug}
|
|
@@ -301,13 +368,11 @@ function SlugField({ page }: { page: MarkdownPageEntry }) {
|
|
|
301
368
|
;(e.target as HTMLInputElement).blur()
|
|
302
369
|
}
|
|
303
370
|
}}
|
|
304
|
-
class=
|
|
305
|
-
isDirty ? 'border-cms-primary' : 'border-white/20'
|
|
306
|
-
}`}
|
|
371
|
+
class="flex-1 bg-transparent focus:outline-none"
|
|
307
372
|
disabled={isRenaming}
|
|
308
373
|
data-cms-ui
|
|
309
374
|
/>
|
|
310
|
-
</
|
|
375
|
+
</label>
|
|
311
376
|
</div>
|
|
312
377
|
)
|
|
313
378
|
}
|
|
@@ -323,14 +388,22 @@ export function EditModeFrontmatter({
|
|
|
323
388
|
collectionDefinition,
|
|
324
389
|
fields,
|
|
325
390
|
}: EditModeFrontmatterProps) {
|
|
326
|
-
const
|
|
391
|
+
const allFields = fields ?? collectionDefinition?.fields ?? []
|
|
392
|
+
const urlFieldNames = new Set(allFields.filter((f) => f.type === 'url' || /link|href|url/i.test(f.name)).map((f) => f.name))
|
|
393
|
+
const isOpenInNewTabSibling = (name: string) => {
|
|
394
|
+
if (!name.endsWith('OpenInNewTab')) return false
|
|
395
|
+
const base = name.slice(0, -'OpenInNewTab'.length)
|
|
396
|
+
return urlFieldNames.has(base)
|
|
397
|
+
}
|
|
398
|
+
const displayFields = allFields.filter((f) => f.name !== 'draft' && !isOpenInNewTabSibling(f.name))
|
|
327
399
|
// Collect schema field names for filtering extra keys
|
|
328
400
|
const schemaFieldNames = new Set(
|
|
329
401
|
collectionDefinition?.fields.map((f) => f.name) ?? [],
|
|
330
402
|
)
|
|
331
|
-
// Frontmatter keys not covered by the schema (user-added fields)
|
|
403
|
+
// Frontmatter keys not covered by the schema (user-added fields). Draft and the
|
|
404
|
+
// `*OpenInNewTab` siblings are rendered separately, so exclude them here.
|
|
332
405
|
const extraKeys = Object.keys(page.frontmatter).filter(
|
|
333
|
-
(key) => !schemaFieldNames.has(key),
|
|
406
|
+
(key) => !schemaFieldNames.has(key) && key !== 'draft' && !isOpenInNewTabSibling(key),
|
|
334
407
|
)
|
|
335
408
|
const groups = groupFields(displayFields)
|
|
336
409
|
|
|
@@ -409,19 +482,29 @@ export function SchemaFrontmatterField({
|
|
|
409
482
|
switch (field.type) {
|
|
410
483
|
case 'text':
|
|
411
484
|
case 'url':
|
|
412
|
-
case 'email':
|
|
485
|
+
case 'email': {
|
|
486
|
+
const isLinkLike = field.type === 'url'
|
|
487
|
+
|| /link|href|url/i.test(field.name)
|
|
488
|
+
const linkTooltip = isLinkLike
|
|
489
|
+
? 'Use https://... for external links, or /path for internal pages.'
|
|
490
|
+
: undefined
|
|
413
491
|
return (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
492
|
+
<>
|
|
493
|
+
<TextField
|
|
494
|
+
label={label}
|
|
495
|
+
value={(value as string) ?? ''}
|
|
496
|
+
placeholder={hints?.placeholder ?? getPlaceholder(field)}
|
|
497
|
+
maxLength={hints?.maxLength as number | undefined}
|
|
498
|
+
minLength={hints?.minLength as number | undefined}
|
|
499
|
+
onChange={(v) => onChange(v)}
|
|
500
|
+
inputType={field.type === 'text' ? undefined : field.type}
|
|
501
|
+
required={field.required}
|
|
502
|
+
tooltip={linkTooltip}
|
|
503
|
+
/>
|
|
504
|
+
{field.type === 'url' && <OpenInNewTabToggle field={field} />}
|
|
505
|
+
</>
|
|
424
506
|
)
|
|
507
|
+
}
|
|
425
508
|
|
|
426
509
|
case 'image': {
|
|
427
510
|
const astroContext = buildAstroUploadContext(field, collection, entrySlug)
|
|
@@ -429,14 +512,12 @@ export function SchemaFrontmatterField({
|
|
|
429
512
|
<ImageField
|
|
430
513
|
label={label}
|
|
431
514
|
value={(value as string) ?? ''}
|
|
432
|
-
placeholder={getPlaceholder(field)}
|
|
433
515
|
onChange={(v) => onChange(v)}
|
|
434
516
|
onBrowse={() => {
|
|
435
517
|
openMediaLibraryWithCallback((url: string) => {
|
|
436
518
|
onChange(url)
|
|
437
519
|
}, astroContext)
|
|
438
520
|
}}
|
|
439
|
-
required={field.required}
|
|
440
521
|
/>
|
|
441
522
|
)
|
|
442
523
|
}
|
|
@@ -822,7 +903,7 @@ function ObjectFields({ label, value, onChange, schemaFields, extraKeys }: Objec
|
|
|
822
903
|
}
|
|
823
904
|
}}
|
|
824
905
|
placeholder="New field name..."
|
|
825
|
-
class="flex-1 px-2 py-1 text-xs bg-white/5 border border-white/10 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/
|
|
906
|
+
class="flex-1 px-2 py-1 text-xs bg-white/5 border border-white/10 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40"
|
|
826
907
|
data-cms-ui
|
|
827
908
|
/>
|
|
828
909
|
<button
|
|
@@ -2,51 +2,10 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
|
2
2
|
import { cn } from '../lib/cn'
|
|
3
3
|
import { updateMarkdownFrontmatter } from '../signals'
|
|
4
4
|
import type { CollectionDefinition, FieldDefinition, MarkdownPageEntry } from '../types'
|
|
5
|
+
import { groupFields, partitionFields } from './field-utils'
|
|
5
6
|
import { formatFieldLabel, FrontmatterField, SchemaFrontmatterField } from './frontmatter-fields'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
// Field Utilities
|
|
9
|
-
// ============================================================================
|
|
10
|
-
|
|
11
|
-
export function partitionFields(fields: FieldDefinition[]): { sidebar: FieldDefinition[]; header: FieldDefinition[] } {
|
|
12
|
-
const sidebar: FieldDefinition[] = []
|
|
13
|
-
const header: FieldDefinition[] = []
|
|
14
|
-
for (const field of fields) {
|
|
15
|
-
if (field.hidden) continue
|
|
16
|
-
if (field.position === 'sidebar') {
|
|
17
|
-
sidebar.push(field)
|
|
18
|
-
} else {
|
|
19
|
-
header.push(field)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return { sidebar, header }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface FieldGroup {
|
|
26
|
-
group: string | null
|
|
27
|
-
fields: FieldDefinition[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function groupFields(fields: FieldDefinition[]): FieldGroup[] {
|
|
31
|
-
const groups: FieldGroup[] = []
|
|
32
|
-
const groupMap = new Map<string | null, FieldDefinition[]>()
|
|
33
|
-
const order: (string | null)[] = []
|
|
34
|
-
|
|
35
|
-
for (const field of fields) {
|
|
36
|
-
const key = field.group ?? null
|
|
37
|
-
if (!groupMap.has(key)) {
|
|
38
|
-
groupMap.set(key, [])
|
|
39
|
-
order.push(key)
|
|
40
|
-
}
|
|
41
|
-
groupMap.get(key)!.push(field)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
for (const key of order) {
|
|
45
|
-
groups.push({ group: key, fields: groupMap.get(key)! })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return groups
|
|
49
|
-
}
|
|
8
|
+
export { groupFields, partitionFields } from './field-utils'
|
|
50
9
|
|
|
51
10
|
// ============================================================================
|
|
52
11
|
// Group Header
|
|
@@ -94,9 +53,11 @@ interface FrontmatterSidebarProps {
|
|
|
94
53
|
|
|
95
54
|
export function FrontmatterSidebar({ fields, page, collectionDefinition }: FrontmatterSidebarProps) {
|
|
96
55
|
const [state, setState] = useState(loadSidebarState)
|
|
56
|
+
const [isAnimating, setIsAnimating] = useState(false)
|
|
97
57
|
const isResizing = useRef(false)
|
|
98
58
|
const startX = useRef(0)
|
|
99
59
|
const startWidth = useRef(0)
|
|
60
|
+
const animationTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
100
61
|
|
|
101
62
|
const { width, collapsed } = state
|
|
102
63
|
|
|
@@ -108,8 +69,17 @@ export function FrontmatterSidebar({ fields, page, collectionDefinition }: Front
|
|
|
108
69
|
})
|
|
109
70
|
}, [])
|
|
110
71
|
|
|
72
|
+
const toggleCollapsed = useCallback(() => {
|
|
73
|
+
if (animationTimeout.current) clearTimeout(animationTimeout.current)
|
|
74
|
+
setIsAnimating(true)
|
|
75
|
+
updateState({ collapsed: !collapsed })
|
|
76
|
+
animationTimeout.current = setTimeout(() => setIsAnimating(false), 300)
|
|
77
|
+
}, [collapsed, updateState])
|
|
78
|
+
|
|
111
79
|
const handleMouseDown = useCallback((e: MouseEvent) => {
|
|
112
80
|
e.preventDefault()
|
|
81
|
+
if (animationTimeout.current) clearTimeout(animationTimeout.current)
|
|
82
|
+
setIsAnimating(false)
|
|
113
83
|
isResizing.current = true
|
|
114
84
|
startX.current = e.clientX
|
|
115
85
|
startWidth.current = width
|
|
@@ -151,7 +121,11 @@ export function FrontmatterSidebar({ fields, page, collectionDefinition }: Front
|
|
|
151
121
|
|
|
152
122
|
return (
|
|
153
123
|
<div
|
|
154
|
-
class={cn(
|
|
124
|
+
class={cn(
|
|
125
|
+
'relative shrink-0 border-l border-white/10 bg-white/5 flex overflow-hidden',
|
|
126
|
+
isAnimating && 'transition-[width] duration-300 ease-out',
|
|
127
|
+
collapsed && 'w-8',
|
|
128
|
+
)}
|
|
155
129
|
style={collapsed ? undefined : { width: `${width}px` }}
|
|
156
130
|
data-cms-ui
|
|
157
131
|
>
|
|
@@ -166,27 +140,50 @@ export function FrontmatterSidebar({ fields, page, collectionDefinition }: Front
|
|
|
166
140
|
{/* Collapse toggle */}
|
|
167
141
|
<button
|
|
168
142
|
type="button"
|
|
169
|
-
onClick={
|
|
170
|
-
class="absolute top-2 left-
|
|
143
|
+
onClick={toggleCollapsed}
|
|
144
|
+
class="absolute top-2 left-1.5 -translate-x-1/2 z-20 w-5 h-5 rounded-cms-xs bg-white/10 hover:bg-white/20 border border-white/20 hover:border-white/40 flex items-center justify-center text-white/50 hover:text-white transition-colors"
|
|
171
145
|
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
172
146
|
data-cms-ui
|
|
173
147
|
>
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
148
|
+
<span class="relative w-3 h-3 inline-block">
|
|
149
|
+
<svg
|
|
150
|
+
class={cn(
|
|
151
|
+
'absolute inset-0 w-3 h-3 transition-opacity duration-200 ease-out',
|
|
152
|
+
collapsed ? 'opacity-0' : 'opacity-100',
|
|
153
|
+
)}
|
|
154
|
+
viewBox="0 0 24 24"
|
|
155
|
+
fill="none"
|
|
156
|
+
stroke="currentColor"
|
|
157
|
+
stroke-width="2"
|
|
158
|
+
stroke-linecap="round"
|
|
159
|
+
stroke-linejoin="round"
|
|
160
|
+
>
|
|
161
|
+
<path d="M3 5v14" />
|
|
162
|
+
<path d="M21 12H7" />
|
|
163
|
+
<path d="m15 18 6-6-6-6" />
|
|
164
|
+
</svg>
|
|
165
|
+
<svg
|
|
166
|
+
class={cn(
|
|
167
|
+
'absolute inset-0 w-3 h-3 transition-opacity duration-200 ease-out',
|
|
168
|
+
collapsed ? 'opacity-100' : 'opacity-0',
|
|
169
|
+
)}
|
|
170
|
+
viewBox="0 0 24 24"
|
|
171
|
+
fill="none"
|
|
172
|
+
stroke="currentColor"
|
|
173
|
+
stroke-width="2"
|
|
174
|
+
stroke-linecap="round"
|
|
175
|
+
stroke-linejoin="round"
|
|
176
|
+
>
|
|
177
|
+
<path d="M3 19V5" />
|
|
178
|
+
<path d="m13 6-6 6 6 6" />
|
|
179
|
+
<path d="M7 12h14" />
|
|
180
|
+
</svg>
|
|
181
|
+
</span>
|
|
185
182
|
</button>
|
|
186
183
|
|
|
187
184
|
{/* Sidebar content */}
|
|
188
185
|
{!collapsed && (
|
|
189
|
-
<div class="flex-1 overflow-y-auto p-
|
|
186
|
+
<div class="flex-1 overflow-y-auto p-3 pt-10 space-y-3 min-w-0" style={{ scrollbarGutter: 'stable' }}>
|
|
190
187
|
{groups.map((group, gi) => (
|
|
191
188
|
<div key={gi} data-cms-ui>
|
|
192
189
|
{group.group && <GroupHeader label={group.group} />}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import { cn } from '../lib/cn'
|
|
2
3
|
import { HighlightMatch } from './fields'
|
|
3
4
|
import { PrimaryButton } from './modal-shell'
|
|
4
5
|
|
|
@@ -131,7 +132,10 @@ export function LinkEditPopover({ initialUrl, suggestions, onApply, onRemove, on
|
|
|
131
132
|
>
|
|
132
133
|
<form
|
|
133
134
|
onSubmit={handleSubmit}
|
|
134
|
-
class={
|
|
135
|
+
class={cn(
|
|
136
|
+
'flex items-center gap-2',
|
|
137
|
+
inline ? 'py-1.5' : 'px-4 py-2 bg-cms-dark border-b border-white/10',
|
|
138
|
+
)}
|
|
135
139
|
>
|
|
136
140
|
<svg class="w-4 h-4 text-white/40 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
137
141
|
<path
|
|
@@ -152,7 +156,7 @@ export function LinkEditPopover({ initialUrl, suggestions, onApply, onRemove, on
|
|
|
152
156
|
onBlur={handleBlur}
|
|
153
157
|
onKeyDown={handleKeyDown}
|
|
154
158
|
autocomplete="off"
|
|
155
|
-
class="w-full bg-white/5 border border-white/10 rounded-cms-sm px-2.5 py-1.5 text-[13px] text-white placeholder:text-white/30 outline-none focus:border-
|
|
159
|
+
class="w-full bg-white/5 border border-white/10 rounded-cms-sm px-2.5 py-1.5 text-[13px] text-white placeholder:text-white/30 outline-none focus:border-white/40 transition-colors"
|
|
156
160
|
data-cms-ui
|
|
157
161
|
/>
|
|
158
162
|
{showDropdown && (
|
|
@@ -169,11 +173,12 @@ export function LinkEditPopover({ initialUrl, suggestions, onApply, onRemove, on
|
|
|
169
173
|
e.preventDefault()
|
|
170
174
|
selectOption(opt.value)
|
|
171
175
|
}}
|
|
172
|
-
class={
|
|
176
|
+
class={cn(
|
|
177
|
+
'w-full text-left px-3 py-2 text-xs transition-colors cursor-pointer',
|
|
173
178
|
i === highlightedIndex
|
|
174
179
|
? 'bg-white/15 text-white'
|
|
175
|
-
: 'text-white/70 hover:bg-white/10 hover:text-white'
|
|
176
|
-
}
|
|
180
|
+
: 'text-white/70 hover:bg-white/10 hover:text-white',
|
|
181
|
+
)}
|
|
177
182
|
data-cms-ui
|
|
178
183
|
>
|
|
179
184
|
<span class="block truncate font-medium">
|