@nuasite/cms 0.39.2 → 0.41.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 +15910 -15027
- package/package.json +1 -1
- package/src/collection-scanner.ts +127 -13
- package/src/content-config-ast.ts +91 -24
- 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 +516 -73
- package/src/editor/components/frontmatter-fields.tsx +188 -55
- package/src/editor/components/frontmatter-sidebar.tsx +56 -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/field-types.ts +15 -0
- package/src/index.ts +6 -0
- package/src/types.ts +7 -0
|
@@ -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
|
+
}
|