@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
package/package.json
CHANGED
package/src/build-processor.ts
CHANGED
|
@@ -382,7 +382,7 @@ async function processFile(
|
|
|
382
382
|
// The sourcePath from HTML attributes may point to a shared Image component
|
|
383
383
|
// rather than the file that actually uses the component with the src value
|
|
384
384
|
if (entry.imageMetadata?.src) {
|
|
385
|
-
const preferredLocation = entry.sourcePath
|
|
385
|
+
const preferredLocation = entry.sourcePath || entry.imageMetadata.srcOccurrence !== undefined
|
|
386
386
|
? {
|
|
387
387
|
file: entry.sourcePath,
|
|
388
388
|
line: entry.sourceLine,
|
|
@@ -50,6 +50,25 @@ const FREE_TEXT_FIELD_NAMES = new Set([
|
|
|
50
50
|
'caption',
|
|
51
51
|
])
|
|
52
52
|
|
|
53
|
+
/** Normalized names (lowercased, underscores/hyphens stripped) that mark a field as the publish toggle. */
|
|
54
|
+
const PUBLISH_TOGGLE_NAMES = new Set(['draft', 'isdraft', 'published', 'ispublished', 'unpublished'])
|
|
55
|
+
|
|
56
|
+
/** Normalized names that mark a field as the publish/release date anchor. */
|
|
57
|
+
const PUBLISH_DATE_NAMES = new Set([
|
|
58
|
+
'date',
|
|
59
|
+
'pubdate',
|
|
60
|
+
'publishdate',
|
|
61
|
+
'publisheddate',
|
|
62
|
+
'publishedate',
|
|
63
|
+
'publishedat',
|
|
64
|
+
'datepublished',
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
/** Normalize a field name for case- and separator-insensitive matching against the *_NAMES sets above. */
|
|
68
|
+
function normalizeFieldName(name: string): string {
|
|
69
|
+
return name.toLowerCase().replace(/[_-]/g, '')
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
/**
|
|
54
73
|
* Observed values for a single field across multiple files
|
|
55
74
|
*/
|
|
@@ -121,7 +140,7 @@ function assignFieldMetadata(
|
|
|
121
140
|
): void {
|
|
122
141
|
for (const field of fields) {
|
|
123
142
|
// Scanner defaults: well-known fields go to sidebar
|
|
124
|
-
if (SIDEBAR_FIELD_NAMES.has(field.name
|
|
143
|
+
if (SIDEBAR_FIELD_NAMES.has(normalizeFieldName(field.name)) || field.type === 'image' || field.type === 'boolean') {
|
|
125
144
|
field.position = 'sidebar'
|
|
126
145
|
} else {
|
|
127
146
|
field.position = 'header'
|
|
@@ -228,7 +247,7 @@ function mergeFieldObservations(observations: FieldObservation[]): FieldDefiniti
|
|
|
228
247
|
}
|
|
229
248
|
|
|
230
249
|
// For text fields, check if we should treat as select (limited unique values)
|
|
231
|
-
if (fieldType === 'text' && !FREE_TEXT_FIELD_NAMES.has(obs.name
|
|
250
|
+
if (fieldType === 'text' && !FREE_TEXT_FIELD_NAMES.has(normalizeFieldName(obs.name))) {
|
|
232
251
|
const uniqueValues = [...new Set(nonNullValues.map(v => String(v)))]
|
|
233
252
|
const uniqueRatio = uniqueValues.length / nonNullValues.length
|
|
234
253
|
// Only treat as select if unique values are limited AND not nearly all unique
|
|
@@ -583,6 +602,33 @@ function detectReferenceFieldsBySlugMatch(collections: Record<string, Collection
|
|
|
583
602
|
}
|
|
584
603
|
}
|
|
585
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Tag fields with semantic roles so the editor UI can position them without
|
|
607
|
+
* matching on Astro-specific field names. Detection lives here — the layer
|
|
608
|
+
* that already knows it's parsing Astro content collections.
|
|
609
|
+
*/
|
|
610
|
+
function assignSemanticRoles(collections: Record<string, CollectionDefinition>): void {
|
|
611
|
+
for (const def of Object.values(collections)) {
|
|
612
|
+
let toggle: FieldDefinition | undefined
|
|
613
|
+
let dateByName: FieldDefinition | undefined
|
|
614
|
+
let dateByType: FieldDefinition | undefined
|
|
615
|
+
for (const field of def.fields) {
|
|
616
|
+
if (field.hidden || field.role) continue
|
|
617
|
+
const normalized = normalizeFieldName(field.name)
|
|
618
|
+
if (!toggle && field.type === 'boolean' && PUBLISH_TOGGLE_NAMES.has(normalized)) {
|
|
619
|
+
toggle = field
|
|
620
|
+
} else if (!dateByName && PUBLISH_DATE_NAMES.has(normalized)) {
|
|
621
|
+
dateByName = field
|
|
622
|
+
} else if (!dateByType && (field.type === 'date' || field.type === 'datetime')) {
|
|
623
|
+
dateByType = field
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (toggle) toggle.role = 'publish-toggle'
|
|
627
|
+
const date = dateByName ?? dateByType
|
|
628
|
+
if (date) date.role = 'publish-date'
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
586
632
|
/** Suffixes that indicate a field is a derived href/url/slug companion */
|
|
587
633
|
const HREF_SUFFIXES = ['href', 'url', 'link', 'slug', 'path'] as const
|
|
588
634
|
|
|
@@ -749,6 +795,7 @@ export async function scanCollections(contentDir: string = 'src/content'): Promi
|
|
|
749
795
|
applyParsedConfig(collections, parsed)
|
|
750
796
|
detectReferenceFields(collections, parsed)
|
|
751
797
|
detectDerivedHrefFields(collections)
|
|
798
|
+
assignSemanticRoles(collections)
|
|
752
799
|
applyCollectionOrderBy(collections, parsed)
|
|
753
800
|
|
|
754
801
|
return collections
|
package/src/dev-middleware.ts
CHANGED
|
@@ -598,7 +598,7 @@ export async function enhanceManifestInBackground(
|
|
|
598
598
|
for (const entry of Object.values(enhanced)) {
|
|
599
599
|
if (entry.sourceSnippet || entry.sourcePath) continue
|
|
600
600
|
if (entry.imageMetadata?.src) {
|
|
601
|
-
const preferredLocation = entry.sourcePath
|
|
601
|
+
const preferredLocation = entry.sourcePath || entry.imageMetadata.srcOccurrence !== undefined
|
|
602
602
|
? {
|
|
603
603
|
file: entry.sourcePath,
|
|
604
604
|
line: entry.sourceLine,
|
|
@@ -329,7 +329,6 @@ function AttributeField({ attrName, currentAttr, originalAttr, pages, onUpdate,
|
|
|
329
329
|
<ImageField
|
|
330
330
|
label={config.label}
|
|
331
331
|
value={currentValue || undefined}
|
|
332
|
-
placeholder={config.placeholder}
|
|
333
332
|
onChange={(v) => onUpdate(v)}
|
|
334
333
|
onBrowse={onOpenMediaLibrary}
|
|
335
334
|
isDirty={isDirty}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
|
2
2
|
import { Z_INDEX } from '../constants'
|
|
3
3
|
import { isApplyingUndoRedo, recordChange } from '../history'
|
|
4
4
|
import { cn } from '../lib/cn'
|
|
@@ -80,14 +80,14 @@ const REPEAT_OPTIONS = [
|
|
|
80
80
|
* Shows a floating badge on hover and opens a right-side settings panel on click.
|
|
81
81
|
*/
|
|
82
82
|
export function BgImageOverlay({ visible, rect, element, cmsId }: BgImageOverlayProps) {
|
|
83
|
-
const
|
|
83
|
+
const panelOpen = signals.isBgImageOverlayOpen.value
|
|
84
84
|
// Capture target when panel opens so it stays stable when hover moves away
|
|
85
85
|
const panelTargetRef = useRef<{ cmsId: string; element: HTMLElement } | null>(null)
|
|
86
86
|
|
|
87
87
|
// Close panel when hovering a different bg-image element
|
|
88
88
|
useEffect(() => {
|
|
89
89
|
if (cmsId && panelTargetRef.current && cmsId !== panelTargetRef.current.cmsId) {
|
|
90
|
-
|
|
90
|
+
signals.isBgImageOverlayOpen.value = false
|
|
91
91
|
panelTargetRef.current = null
|
|
92
92
|
}
|
|
93
93
|
}, [cmsId])
|
|
@@ -99,7 +99,7 @@ export function BgImageOverlay({ visible, rect, element, cmsId }: BgImageOverlay
|
|
|
99
99
|
const handleClickOutside = (e: MouseEvent) => {
|
|
100
100
|
const target = e.target as HTMLElement
|
|
101
101
|
if (target.closest('[data-cms-ui]')) return
|
|
102
|
-
|
|
102
|
+
signals.isBgImageOverlayOpen.value = false
|
|
103
103
|
panelTargetRef.current = null
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -117,16 +117,16 @@ export function BgImageOverlay({ visible, rect, element, cmsId }: BgImageOverlay
|
|
|
117
117
|
e.preventDefault()
|
|
118
118
|
e.stopPropagation()
|
|
119
119
|
if (panelOpen) {
|
|
120
|
-
|
|
120
|
+
signals.isBgImageOverlayOpen.value = false
|
|
121
121
|
panelTargetRef.current = null
|
|
122
122
|
} else if (cmsId && element) {
|
|
123
|
-
|
|
123
|
+
signals.isBgImageOverlayOpen.value = true
|
|
124
124
|
panelTargetRef.current = { cmsId, element }
|
|
125
125
|
}
|
|
126
126
|
}, [panelOpen, cmsId, element])
|
|
127
127
|
|
|
128
128
|
const handleClose = useCallback(() => {
|
|
129
|
-
|
|
129
|
+
signals.isBgImageOverlayOpen.value = false
|
|
130
130
|
panelTargetRef.current = null
|
|
131
131
|
}, [])
|
|
132
132
|
|
|
@@ -243,7 +243,6 @@ export function BgImageOverlay({ visible, rect, element, cmsId }: BgImageOverlay
|
|
|
243
243
|
<ImageField
|
|
244
244
|
label="Image URL"
|
|
245
245
|
value={currentUrl || undefined}
|
|
246
|
-
placeholder="/assets/image.png"
|
|
247
246
|
onChange={handleImageUrlChange}
|
|
248
247
|
onBrowse={handleBrowse}
|
|
249
248
|
isDirty={isImageDirty}
|
|
@@ -374,13 +374,13 @@ export function BlockEditor({
|
|
|
374
374
|
<div class="flex gap-2">
|
|
375
375
|
<button
|
|
376
376
|
onClick={() => handleStartInsert('before')}
|
|
377
|
-
class="flex-1 py-2.5 px-3 bg-white/10 text-white/80 rounded-cms-
|
|
377
|
+
class="flex-1 py-2.5 px-3 bg-white/10 text-white/80 rounded-cms-sm cursor-pointer text-[13px] font-medium flex items-center justify-center gap-1.5 hover:bg-white/20 hover:text-white transition-colors"
|
|
378
378
|
>
|
|
379
379
|
<span class="text-base">↑</span> {isArrayItem ? 'Add item before' : 'Insert before'}
|
|
380
380
|
</button>
|
|
381
381
|
<button
|
|
382
382
|
onClick={() => handleStartInsert('after')}
|
|
383
|
-
class="flex-1 py-2.5 px-3 bg-white/10 text-white/80 rounded-cms-
|
|
383
|
+
class="flex-1 py-2.5 px-3 bg-white/10 text-white/80 rounded-cms-sm cursor-pointer text-[13px] font-medium flex items-center justify-center gap-1.5 hover:bg-white/20 hover:text-white transition-colors"
|
|
384
384
|
>
|
|
385
385
|
<span class="text-base">↓</span> {isArrayItem ? 'Add item after' : 'Insert after'}
|
|
386
386
|
</button>
|
|
@@ -388,20 +388,20 @@ export function BlockEditor({
|
|
|
388
388
|
<div class="flex gap-2 justify-between">
|
|
389
389
|
<button
|
|
390
390
|
onClick={() => setMode('confirm-remove')}
|
|
391
|
-
class="px-
|
|
391
|
+
class="px-5 py-2.5 bg-cms-error text-white rounded-cms-pill cursor-pointer hover:bg-red-600 transition-colors font-medium"
|
|
392
392
|
>
|
|
393
393
|
{isArrayItem ? 'Remove item' : 'Remove'}
|
|
394
394
|
</button>
|
|
395
395
|
<div class="flex gap-2">
|
|
396
396
|
<button
|
|
397
397
|
onClick={onClose}
|
|
398
|
-
class="px-
|
|
398
|
+
class="px-5 py-2.5 bg-white/10 text-white/80 rounded-cms-pill cursor-pointer hover:bg-white/20 hover:text-white transition-colors font-medium"
|
|
399
399
|
>
|
|
400
400
|
Cancel
|
|
401
401
|
</button>
|
|
402
402
|
<button
|
|
403
403
|
onClick={handleSave}
|
|
404
|
-
class="px-
|
|
404
|
+
class="px-5 py-2.5 bg-cms-primary text-cms-primary-text rounded-cms-pill cursor-pointer hover:bg-cms-primary-hover transition-all font-medium"
|
|
405
405
|
>
|
|
406
406
|
Save
|
|
407
407
|
</button>
|
|
@@ -413,7 +413,7 @@ export function BlockEditor({
|
|
|
413
413
|
: mode === 'confirm-remove'
|
|
414
414
|
? (
|
|
415
415
|
<div class="text-center p-5">
|
|
416
|
-
<div class="px-4 py-3 bg-red-500/10 border border-red-500/30 rounded-cms-
|
|
416
|
+
<div class="px-4 py-3 bg-red-500/10 border border-red-500/30 rounded-cms-sm mb-5 text-[13px] text-white">
|
|
417
417
|
{isArrayItem
|
|
418
418
|
? (
|
|
419
419
|
<>
|
|
@@ -429,7 +429,7 @@ export function BlockEditor({
|
|
|
429
429
|
<div class="flex gap-2 justify-end pt-4 border-t border-white/10 mt-4">
|
|
430
430
|
<button
|
|
431
431
|
onClick={handleBackToEdit}
|
|
432
|
-
class="px-
|
|
432
|
+
class="px-5 py-2.5 bg-white/10 text-white/80 rounded-cms-pill cursor-pointer hover:bg-white/20 hover:text-white transition-colors font-medium"
|
|
433
433
|
>
|
|
434
434
|
Cancel
|
|
435
435
|
</button>
|
|
@@ -440,7 +440,7 @@ export function BlockEditor({
|
|
|
440
440
|
onClose()
|
|
441
441
|
}
|
|
442
442
|
}}
|
|
443
|
-
class="px-
|
|
443
|
+
class="px-5 py-2.5 bg-cms-error text-white rounded-cms-pill cursor-pointer hover:bg-red-600 transition-colors font-medium"
|
|
444
444
|
>
|
|
445
445
|
{isArrayItem ? 'Confirm remove item' : 'Confirm remove'}
|
|
446
446
|
</button>
|
|
@@ -452,7 +452,7 @@ export function BlockEditor({
|
|
|
452
452
|
<div class="p-5">
|
|
453
453
|
{/* New component props */}
|
|
454
454
|
<div class="mb-5">
|
|
455
|
-
<div class="px-4 py-3 bg-white/10 rounded-cms-
|
|
455
|
+
<div class="px-4 py-3 bg-white/10 rounded-cms-sm mb-4 text-[13px] text-white">
|
|
456
456
|
{isArrayItem
|
|
457
457
|
? (
|
|
458
458
|
<>
|
|
@@ -478,13 +478,13 @@ export function BlockEditor({
|
|
|
478
478
|
<div class="flex gap-2 justify-end pt-4 border-t border-white/10 mt-4">
|
|
479
479
|
<button
|
|
480
480
|
onClick={() => isArrayItem ? handleBackToEdit() : setMode('insert-picker')}
|
|
481
|
-
class="px-
|
|
481
|
+
class="px-5 py-2.5 bg-white/10 text-white/80 rounded-cms-pill cursor-pointer hover:bg-white/20 hover:text-white transition-colors font-medium"
|
|
482
482
|
>
|
|
483
483
|
Back
|
|
484
484
|
</button>
|
|
485
485
|
<button
|
|
486
486
|
onClick={handleConfirmInsert}
|
|
487
|
-
class="px-
|
|
487
|
+
class="px-5 py-2.5 bg-cms-primary text-cms-primary-text rounded-cms-pill cursor-pointer hover:bg-cms-primary-hover transition-all font-medium"
|
|
488
488
|
>
|
|
489
489
|
{isArrayItem ? 'Add item' : 'Insert component'}
|
|
490
490
|
</button>
|
|
@@ -510,7 +510,7 @@ export function BlockEditor({
|
|
|
510
510
|
<div class="mt-5 pt-4 border-t border-white/10">
|
|
511
511
|
<button
|
|
512
512
|
onClick={handleBackToEdit}
|
|
513
|
-
class="w-full px-4 py-2
|
|
513
|
+
class="w-full px-4 py-2 bg-white/10 text-white/80 rounded-cms-pill cursor-pointer hover:bg-white/20 hover:text-white transition-colors font-medium"
|
|
514
514
|
>
|
|
515
515
|
Back to edit
|
|
516
516
|
</button>
|
|
@@ -97,7 +97,7 @@ export function CollectionsBrowser() {
|
|
|
97
97
|
|
|
98
98
|
return (
|
|
99
99
|
<ModalBackdrop onClose={handleClose} extraClass="flex flex-col max-h-[80vh]">
|
|
100
|
-
<div class="flex items-center justify-between
|
|
100
|
+
<div class="flex items-center justify-between px-5 py-4 border-b border-white/10 shrink-0">
|
|
101
101
|
<div class="flex items-center gap-3">
|
|
102
102
|
<button
|
|
103
103
|
type="button"
|
|
@@ -122,8 +122,8 @@ export function CollectionsBrowser() {
|
|
|
122
122
|
|
|
123
123
|
{entries.length > 0 && (
|
|
124
124
|
<div class="px-5 pt-4 pb-2 shrink-0">
|
|
125
|
-
<
|
|
126
|
-
<svg class="
|
|
125
|
+
<label class="flex items-center gap-2 px-3 py-2 bg-white/5 border border-white/10 rounded-cms-lg focus-within:border-white/40">
|
|
126
|
+
<svg class="w-4 h-4 text-white/30 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
127
127
|
<circle cx="11" cy="11" r="8" />
|
|
128
128
|
<path stroke-linecap="round" stroke-width="2" d="m21 21-4.3-4.3" />
|
|
129
129
|
</svg>
|
|
@@ -132,11 +132,11 @@ export function CollectionsBrowser() {
|
|
|
132
132
|
placeholder="Search..."
|
|
133
133
|
value={search}
|
|
134
134
|
onInput={(e) => setSearch((e.target as HTMLInputElement).value)}
|
|
135
|
-
class="
|
|
135
|
+
class="flex-1 bg-transparent text-sm text-white placeholder:text-white/30 focus:outline-none"
|
|
136
136
|
data-cms-ui
|
|
137
137
|
/>
|
|
138
|
-
</
|
|
139
|
-
<div class="text-white/30 text-xs mt-2">
|
|
138
|
+
</label>
|
|
139
|
+
<div class="text-white/30 text-xs mt-2 ml-4">
|
|
140
140
|
{search
|
|
141
141
|
? `${filteredEntries.length} of ${entries.length}`
|
|
142
142
|
: `${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`}
|
|
@@ -186,7 +186,7 @@ export function CollectionsBrowser() {
|
|
|
186
186
|
<button
|
|
187
187
|
type="button"
|
|
188
188
|
onClick={() => handleEntryClick(entry.slug, entry.sourcePath)}
|
|
189
|
-
class="w-full flex items-center gap-3 px-4 py-3 hover:bg-white/10 rounded-cms-
|
|
189
|
+
class="w-full flex items-center gap-3 px-4 py-3 hover:bg-white/10 rounded-cms-md transition-colors text-left group"
|
|
190
190
|
data-cms-ui
|
|
191
191
|
>
|
|
192
192
|
<div class="flex-1 min-w-0">
|
|
@@ -210,7 +210,7 @@ export function CollectionsBrowser() {
|
|
|
210
210
|
<TrashIcon />
|
|
211
211
|
</button>
|
|
212
212
|
<svg
|
|
213
|
-
class="w-4 h-4 text-white/20 group-hover:text-
|
|
213
|
+
class="w-4 h-4 text-white/20 group-hover:text-cms-primary shrink-0 transition-colors"
|
|
214
214
|
fill="none"
|
|
215
215
|
stroke="currentColor"
|
|
216
216
|
viewBox="0 0 24 24"
|
|
@@ -251,10 +251,10 @@ export function CollectionsBrowser() {
|
|
|
251
251
|
key={col.name}
|
|
252
252
|
type="button"
|
|
253
253
|
onClick={() => selectBrowserCollection(col.name)}
|
|
254
|
-
class="w-full flex items-center gap-4 p-4 bg-white/5 hover:bg-white/10 rounded-cms-lg border border-white/10 hover:border-white/20 transition-colors text-left"
|
|
254
|
+
class="group w-full flex items-center gap-4 p-4 bg-white/5 hover:bg-white/10 rounded-cms-lg border border-white/10 hover:border-white/20 transition-colors text-left cursor-pointer"
|
|
255
255
|
data-cms-ui
|
|
256
256
|
>
|
|
257
|
-
<div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-
|
|
257
|
+
<div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-sm flex items-center justify-center">
|
|
258
258
|
<CollectionIcon />
|
|
259
259
|
</div>
|
|
260
260
|
<div class="flex-1 min-w-0">
|
|
@@ -75,10 +75,10 @@ function ModeCard({ icon, title, description, onClick }: {
|
|
|
75
75
|
<button
|
|
76
76
|
type="button"
|
|
77
77
|
onClick={onClick}
|
|
78
|
-
class="w-full flex items-center gap-4 p-4 bg-white/5 hover:bg-white/10 rounded-cms-lg border border-white/10 hover:border-white/20 transition-colors text-left cursor-pointer"
|
|
78
|
+
class="group w-full flex items-center gap-4 p-4 bg-white/5 hover:bg-white/10 rounded-cms-lg border border-white/10 hover:border-white/20 transition-colors text-left cursor-pointer"
|
|
79
79
|
data-cms-ui
|
|
80
80
|
>
|
|
81
|
-
<div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-
|
|
81
|
+
<div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-sm flex items-center justify-center">
|
|
82
82
|
{icon}
|
|
83
83
|
</div>
|
|
84
84
|
<div class="flex-1 min-w-0">
|
|
@@ -222,25 +222,25 @@ function NewPageForm() {
|
|
|
222
222
|
onInput={(e) => form.handleTitleChange((e.target as HTMLInputElement).value)}
|
|
223
223
|
placeholder="My New Page"
|
|
224
224
|
required
|
|
225
|
-
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white placeholder:text-white/30 focus:outline-none focus:border-
|
|
225
|
+
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white placeholder:text-white/30 focus:outline-none focus:border-white/40"
|
|
226
226
|
autoFocus
|
|
227
227
|
data-cms-ui
|
|
228
228
|
/>
|
|
229
229
|
</Field>
|
|
230
230
|
|
|
231
231
|
<Field label="URL Path" error={form.slugError} checking={form.slugChecking}>
|
|
232
|
-
<
|
|
233
|
-
<span class="text-white/40 text-sm">/</span>
|
|
232
|
+
<label class="flex items-center gap-1 px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md focus-within:border-white/40">
|
|
233
|
+
<span class="text-white/40 text-sm shrink-0">/</span>
|
|
234
234
|
<input
|
|
235
235
|
type="text"
|
|
236
236
|
value={form.slug}
|
|
237
237
|
onInput={(e) => form.handleSlugChange((e.target as HTMLInputElement).value)}
|
|
238
238
|
placeholder="my-new-page"
|
|
239
239
|
required
|
|
240
|
-
class="flex-1
|
|
240
|
+
class="flex-1 bg-transparent text-white placeholder:text-white/30 focus:outline-none"
|
|
241
241
|
data-cms-ui
|
|
242
242
|
/>
|
|
243
|
-
</
|
|
243
|
+
</label>
|
|
244
244
|
</Field>
|
|
245
245
|
|
|
246
246
|
{layouts.length > 0 && (
|
|
@@ -248,7 +248,7 @@ function NewPageForm() {
|
|
|
248
248
|
<select
|
|
249
249
|
value={selectedLayout}
|
|
250
250
|
onChange={(e) => setSelectedLayout((e.target as HTMLSelectElement).value || undefined)}
|
|
251
|
-
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white focus:outline-none focus:border-
|
|
251
|
+
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white focus:outline-none focus:border-white/40"
|
|
252
252
|
data-cms-ui
|
|
253
253
|
>
|
|
254
254
|
{layouts.map((l) => <option key={l.path} value={l.path}>{l.name}</option>)}
|
|
@@ -325,7 +325,7 @@ function DuplicatePageForm() {
|
|
|
325
325
|
form.resetSlugManual()
|
|
326
326
|
}}
|
|
327
327
|
required
|
|
328
|
-
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white focus:outline-none focus:border-
|
|
328
|
+
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white focus:outline-none focus:border-white/40"
|
|
329
329
|
data-cms-ui
|
|
330
330
|
>
|
|
331
331
|
{pages.map((p) => (
|
|
@@ -342,24 +342,24 @@ function DuplicatePageForm() {
|
|
|
342
342
|
value={form.title}
|
|
343
343
|
onInput={(e) => form.handleTitleChange((e.target as HTMLInputElement).value)}
|
|
344
344
|
placeholder="Page title"
|
|
345
|
-
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white placeholder:text-white/30 focus:outline-none focus:border-
|
|
345
|
+
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white placeholder:text-white/30 focus:outline-none focus:border-white/40"
|
|
346
346
|
data-cms-ui
|
|
347
347
|
/>
|
|
348
348
|
</Field>
|
|
349
349
|
|
|
350
350
|
<Field label="New URL Path" error={form.slugError} checking={form.slugChecking}>
|
|
351
|
-
<
|
|
352
|
-
<span class="text-white/40 text-sm">/</span>
|
|
351
|
+
<label class="flex items-center gap-1 px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md focus-within:border-white/40">
|
|
352
|
+
<span class="text-white/40 text-sm shrink-0">/</span>
|
|
353
353
|
<input
|
|
354
354
|
type="text"
|
|
355
355
|
value={form.slug}
|
|
356
356
|
onInput={(e) => form.handleSlugChange((e.target as HTMLInputElement).value)}
|
|
357
357
|
placeholder="new-page-slug"
|
|
358
358
|
required
|
|
359
|
-
class="flex-1
|
|
359
|
+
class="flex-1 bg-transparent text-white placeholder:text-white/30 focus:outline-none"
|
|
360
360
|
data-cms-ui
|
|
361
361
|
/>
|
|
362
|
-
</
|
|
362
|
+
</label>
|
|
363
363
|
</Field>
|
|
364
364
|
|
|
365
365
|
<label class="flex items-center gap-2.5 cursor-pointer" data-cms-ui>
|
|
@@ -441,16 +441,16 @@ function CollectionPicker() {
|
|
|
441
441
|
key={col.name}
|
|
442
442
|
type="button"
|
|
443
443
|
onClick={() => handleSelectCollection(col.name)}
|
|
444
|
-
class="w-full flex items-center gap-4 p-4 bg-white/5 hover:bg-white/10 rounded-cms-lg border border-white/10 hover:border-white/20 transition-colors text-left cursor-pointer"
|
|
444
|
+
class="group w-full flex items-center gap-4 p-4 bg-white/5 hover:bg-white/10 rounded-cms-lg border border-white/10 hover:border-white/20 transition-colors text-left cursor-pointer"
|
|
445
445
|
data-cms-ui
|
|
446
446
|
>
|
|
447
|
-
<div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-
|
|
447
|
+
<div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-sm flex items-center justify-center">
|
|
448
448
|
<CollectionIcon />
|
|
449
449
|
</div>
|
|
450
450
|
<div class="flex-1 min-w-0">
|
|
451
451
|
<div class="text-white font-medium">{col.label}</div>
|
|
452
452
|
<div class="text-white/50 text-sm">
|
|
453
|
-
{col.entryCount} {col.entryCount === 1 ? 'entry' : 'entries'}
|
|
453
|
+
{col.entryCount} {col.entryCount === 1 ? 'entry' : 'entries'}
|
|
454
454
|
</div>
|
|
455
455
|
</div>
|
|
456
456
|
<ChevronRightIcon />
|
|
@@ -588,7 +588,7 @@ export function CollectionIcon() {
|
|
|
588
588
|
|
|
589
589
|
export function ChevronRightIcon() {
|
|
590
590
|
return (
|
|
591
|
-
<svg class="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
591
|
+
<svg class="w-5 h-5 text-white/40 group-hover:text-cms-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
592
592
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
593
593
|
</svg>
|
|
594
594
|
)
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
setDeletingPage,
|
|
11
11
|
showToast,
|
|
12
12
|
} from '../signals'
|
|
13
|
+
import { STRINGS } from '../strings'
|
|
13
14
|
import { CancelButton, ModalBackdrop, ModalFooter, ModalHeader } from './modal-shell'
|
|
14
15
|
|
|
15
16
|
export function DeletePageDialog() {
|
|
@@ -35,10 +36,10 @@ export function DeletePageDialog() {
|
|
|
35
36
|
|
|
36
37
|
if (result.success) {
|
|
37
38
|
resetDeletePageState()
|
|
38
|
-
showToast(
|
|
39
|
+
showToast(STRINGS.page.deleted, 'success')
|
|
39
40
|
window.location.href = currentState.createRedirect && currentState.redirectTo ? currentState.redirectTo : '/'
|
|
40
41
|
} else {
|
|
41
|
-
showToast(result.error ||
|
|
42
|
+
showToast(result.error || STRINGS.page.deleteFailed, 'error')
|
|
42
43
|
}
|
|
43
44
|
}, [])
|
|
44
45
|
|
|
@@ -75,7 +76,7 @@ export function DeletePageDialog() {
|
|
|
75
76
|
value={state.redirectTo}
|
|
76
77
|
onInput={(e) => setDeletePageRedirectTo((e.target as HTMLInputElement).value)}
|
|
77
78
|
placeholder="/"
|
|
78
|
-
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white placeholder:text-white/30 focus:outline-none focus:border-
|
|
79
|
+
class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-cms-md text-white placeholder:text-white/30 focus:outline-none focus:border-white/40"
|
|
79
80
|
data-cms-ui
|
|
80
81
|
/>
|
|
81
82
|
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FieldDefinition } from '../types'
|
|
2
|
+
|
|
3
|
+
export function partitionFields(fields: FieldDefinition[]): { sidebar: FieldDefinition[]; header: FieldDefinition[] } {
|
|
4
|
+
const sidebar: FieldDefinition[] = []
|
|
5
|
+
const header: FieldDefinition[] = []
|
|
6
|
+
let toggleField: FieldDefinition | null = null
|
|
7
|
+
for (const field of fields) {
|
|
8
|
+
if (field.hidden) continue
|
|
9
|
+
if (field.role === 'publish-toggle' && field.position !== 'header') {
|
|
10
|
+
toggleField = field
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
if (field.position === 'sidebar') {
|
|
14
|
+
sidebar.push(field)
|
|
15
|
+
} else {
|
|
16
|
+
header.push(field)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (toggleField) {
|
|
20
|
+
const dateIdx = sidebar.findIndex((f) => f.role === 'publish-date')
|
|
21
|
+
if (dateIdx >= 0) {
|
|
22
|
+
sidebar.splice(dateIdx, 0, toggleField)
|
|
23
|
+
} else {
|
|
24
|
+
sidebar.unshift(toggleField)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { sidebar, header }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FieldGroup {
|
|
31
|
+
group: string | null
|
|
32
|
+
fields: FieldDefinition[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function groupFields(fields: FieldDefinition[]): FieldGroup[] {
|
|
36
|
+
const groups: FieldGroup[] = []
|
|
37
|
+
const groupMap = new Map<string | null, FieldDefinition[]>()
|
|
38
|
+
const order: (string | null)[] = []
|
|
39
|
+
|
|
40
|
+
for (const field of fields) {
|
|
41
|
+
const key = field.group ?? null
|
|
42
|
+
if (!groupMap.has(key)) {
|
|
43
|
+
groupMap.set(key, [])
|
|
44
|
+
order.push(key)
|
|
45
|
+
}
|
|
46
|
+
groupMap.get(key)!.push(field)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const key of order) {
|
|
50
|
+
groups.push({ group: key, fields: groupMap.get(key)! })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return groups
|
|
54
|
+
}
|