@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
|
@@ -4,6 +4,7 @@ import { CMS_VERSION, Z_INDEX } from '../constants'
|
|
|
4
4
|
import { cn } from '../lib/cn'
|
|
5
5
|
import * as signals from '../signals'
|
|
6
6
|
import { showConfirmDialog } from '../signals'
|
|
7
|
+
import { STRINGS } from '../strings'
|
|
7
8
|
import type { CollectionDefinition } from '../types'
|
|
8
9
|
import { Spinner } from './spinner'
|
|
9
10
|
|
|
@@ -46,6 +47,9 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
46
47
|
const isSaving = signals.isSaving.value
|
|
47
48
|
const showEditableHighlights = signals.showEditableHighlights.value
|
|
48
49
|
const isPreviewingMarkdown = signals.isMarkdownPreview.value
|
|
50
|
+
const isSideModalOpen = signals.isColorEditorOpen.value
|
|
51
|
+
|| signals.isAttributeEditorOpen.value
|
|
52
|
+
|| signals.isBgImageOverlayOpen.value
|
|
49
53
|
const currentPageCollection = signals.currentPageCollection.value
|
|
50
54
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
|
51
55
|
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set())
|
|
@@ -53,15 +57,18 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
53
57
|
const versionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
54
58
|
|
|
55
59
|
if (isPreviewingMarkdown) return null
|
|
60
|
+
// Right-anchored side panels (color editor, attribute editor, background-image overlay)
|
|
61
|
+
// would visually overlap the right-anchored toolbar pill — hide while any of them is open.
|
|
62
|
+
if (isSideModalOpen) return null
|
|
56
63
|
|
|
57
64
|
const stopPropagation = (e: Event) => e.stopPropagation()
|
|
58
65
|
|
|
59
66
|
const handleDiscard = async () => {
|
|
60
67
|
const confirmed = await showConfirmDialog({
|
|
61
|
-
title:
|
|
62
|
-
message:
|
|
63
|
-
confirmLabel:
|
|
64
|
-
cancelLabel:
|
|
68
|
+
title: STRINGS.dialog.discardAll.title,
|
|
69
|
+
message: STRINGS.dialog.discardAll.message,
|
|
70
|
+
confirmLabel: STRINGS.dialog.discardAll.confirm,
|
|
71
|
+
cancelLabel: STRINGS.dialog.discardAll.cancel,
|
|
65
72
|
variant: 'danger',
|
|
66
73
|
})
|
|
67
74
|
if (confirmed) {
|
|
@@ -76,7 +83,7 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
76
83
|
const topLevelItems: MenuItem[] = []
|
|
77
84
|
if (callbacks.onSelectElement && signals.config.value.features?.selectElement) {
|
|
78
85
|
topLevelItems.push({
|
|
79
|
-
label:
|
|
86
|
+
label: STRINGS.toolbar.selectElement,
|
|
80
87
|
icon: (
|
|
81
88
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
82
89
|
<path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z" />
|
|
@@ -201,7 +208,7 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
201
208
|
class={cn(
|
|
202
209
|
'fixed bottom-4 sm:bottom-8 font-sans transition-all duration-300',
|
|
203
210
|
isToolbarOpen
|
|
204
|
-
? 'left-4 right-4 sm:
|
|
211
|
+
? 'left-4 right-4 sm:right-8 sm:left-auto sm:-translate-x-1/2'
|
|
205
212
|
: 'right-4 sm:right-8',
|
|
206
213
|
)}
|
|
207
214
|
data-cms-ui
|
|
@@ -213,9 +220,9 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
213
220
|
{isToolbarOpen && !showingOriginal && callbacks.onToggleHighlights && (
|
|
214
221
|
<ToolbarButton
|
|
215
222
|
onClick={() => callbacks.onToggleHighlights?.()}
|
|
216
|
-
class={'flex gap-2.5 bg-white/10 text-white/80 hover:bg-white/20 hover:text-white py-2! pr-1.5!'}
|
|
223
|
+
class={'flex min-w-[157px] gap-2.5 bg-white/10 text-white/80 hover:bg-white/20 hover:text-white py-2! pr-1.5!'}
|
|
217
224
|
>
|
|
218
|
-
Outlines
|
|
225
|
+
{showEditableHighlights ? 'Hide Outlines' : 'Show Outlines'}
|
|
219
226
|
<span
|
|
220
227
|
class={cn(
|
|
221
228
|
'inline-block w-6 h-6 rounded-full shrink-0 transition-colors',
|
|
@@ -331,7 +338,7 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
331
338
|
}}
|
|
332
339
|
/>
|
|
333
340
|
{/* Menu popover */}
|
|
334
|
-
<div class="absolute bottom-full right-0 mb-4 min-w-[200px] bg-cms-dark rounded-cms-lg shadow-[0_8px_32px_rgba(0,0,0,0.4)] border border-white/10 overflow-hidden
|
|
341
|
+
<div class="absolute bottom-full right-0 mb-4 min-w-[200px] bg-cms-dark rounded-cms-lg shadow-[0_8px_32px_rgba(0,0,0,0.4)] border border-white/10 overflow-hidden">
|
|
335
342
|
{topLevelItems.map((item, index) => (
|
|
336
343
|
<button
|
|
337
344
|
key={`top-${index}`}
|
|
@@ -341,7 +348,8 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
341
348
|
setIsMenuOpen(false)
|
|
342
349
|
}}
|
|
343
350
|
class={cn(
|
|
344
|
-
'w-full
|
|
351
|
+
'w-full p-2.5 text-sm font-medium text-left transition-colors cursor-pointer flex items-center gap-3',
|
|
352
|
+
index === 0 && 'pt-3.5',
|
|
345
353
|
item.isActive
|
|
346
354
|
? 'bg-white/20 text-white'
|
|
347
355
|
: 'text-white/80 hover:bg-white/10 hover:text-white',
|
|
@@ -351,7 +359,7 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
351
359
|
{item.label}
|
|
352
360
|
</button>
|
|
353
361
|
))}
|
|
354
|
-
{topLevelItems.length > 0 && menuSections.length > 0 && <div class="border-t border-white/10
|
|
362
|
+
{topLevelItems.length > 0 && menuSections.length > 0 && <div class="border-t border-white/10" />}
|
|
355
363
|
{menuSections.map((section) => {
|
|
356
364
|
const isExpanded = expandedSections.has(section.label)
|
|
357
365
|
return (
|
|
@@ -369,7 +377,7 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
369
377
|
return next
|
|
370
378
|
})
|
|
371
379
|
}}
|
|
372
|
-
class="w-full
|
|
380
|
+
class="w-full p-2.5 text-sm font-medium text-left transition-colors cursor-pointer flex items-center gap-3 text-white/80 hover:bg-white/10 hover:text-white"
|
|
373
381
|
>
|
|
374
382
|
<span class="shrink-0 opacity-70">{section.icon}</span>
|
|
375
383
|
{section.label}
|
|
@@ -385,29 +393,38 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
385
393
|
<path d="m6 9 6 6 6-6" />
|
|
386
394
|
</svg>
|
|
387
395
|
</button>
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
396
|
+
<div
|
|
397
|
+
class={cn(
|
|
398
|
+
'grid transition-[grid-template-rows] duration-200 ease-out',
|
|
399
|
+
isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
|
|
400
|
+
)}
|
|
401
|
+
>
|
|
402
|
+
<div class="overflow-hidden">
|
|
403
|
+
{section.items.map((item, index) => (
|
|
404
|
+
<button
|
|
405
|
+
key={index}
|
|
406
|
+
onClick={(e) => {
|
|
407
|
+
e.stopPropagation()
|
|
408
|
+
item.onClick()
|
|
409
|
+
setIsMenuOpen(false)
|
|
410
|
+
}}
|
|
411
|
+
class={cn(
|
|
412
|
+
'w-full pl-9 pr-4 py-2 text-sm text-left transition-colors cursor-pointer flex items-center gap-3',
|
|
413
|
+
item.isActive
|
|
414
|
+
? 'bg-white/20 text-white'
|
|
415
|
+
: 'text-white/60 hover:bg-white/10 hover:text-white',
|
|
416
|
+
)}
|
|
417
|
+
>
|
|
418
|
+
<span class="shrink-0 opacity-70">{item.icon}</span>
|
|
419
|
+
{item.label}
|
|
420
|
+
</button>
|
|
421
|
+
))}
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
407
424
|
</div>
|
|
408
425
|
)
|
|
409
426
|
})}
|
|
410
|
-
{destructiveItems.length > 0 && <div class="border-t border-white/10
|
|
427
|
+
{destructiveItems.length > 0 && <div class="border-t border-white/10" />}
|
|
411
428
|
{destructiveItems.map((item, index) => (
|
|
412
429
|
<button
|
|
413
430
|
key={`destructive-${index}`}
|
|
@@ -416,7 +433,7 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
416
433
|
item.onClick()
|
|
417
434
|
setIsMenuOpen(false)
|
|
418
435
|
}}
|
|
419
|
-
class="w-full
|
|
436
|
+
class="w-full p-2.5 pb-3.5 text-sm font-medium text-left transition-colors cursor-pointer flex items-center gap-3 text-red-400/80 hover:bg-red-500/10 hover:text-red-400"
|
|
420
437
|
>
|
|
421
438
|
<span class="shrink-0 opacity-70">{item.icon}</span>
|
|
422
439
|
{item.label}
|
package/src/editor/dom.ts
CHANGED
|
@@ -45,10 +45,21 @@ export function isPageDark(): boolean {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Get an outline color that contrasts with the
|
|
48
|
+
* Get an outline color that contrasts with the user's `siteTheme` config:
|
|
49
|
+
* - 'light' — host is light → dark outline
|
|
50
|
+
* - 'dark' — host is dark → light outline
|
|
51
|
+
* - 'auto' (default) — detect via `prefers-color-scheme` then fall back to the page background
|
|
49
52
|
*/
|
|
50
53
|
export function getOutlineColor(): string {
|
|
51
|
-
|
|
54
|
+
const cfg = typeof window !== 'undefined' ? window.NuaCmsConfig : undefined
|
|
55
|
+
const theme = cfg?.siteTheme ?? 'auto'
|
|
56
|
+
if (theme === 'light') return 'rgba(26, 26, 26, 0.7)'
|
|
57
|
+
if (theme === 'dark') return 'rgba(255, 255, 255, 0.85)'
|
|
58
|
+
// auto: trust the OS hint when present, otherwise sample the page
|
|
59
|
+
const prefersDark = typeof window !== 'undefined'
|
|
60
|
+
&& window.matchMedia?.('(prefers-color-scheme: dark)').matches
|
|
61
|
+
const hostIsDark = prefersDark || isPageDark()
|
|
62
|
+
return hostIsDark ? '#FFFFFF' : '#1A1A1A'
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
/**
|
package/src/editor/editor.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
saveEditsToStorage,
|
|
34
34
|
saveImageEditsToStorage,
|
|
35
35
|
} from './storage'
|
|
36
|
+
import { STRINGS } from './strings'
|
|
36
37
|
import type { Attribute } from './types'
|
|
37
38
|
import type { AttributeChangePayload, ChangePayload, CmsConfig, ManifestEntry, SavedAttributeEdit } from './types'
|
|
38
39
|
|
|
@@ -88,7 +89,7 @@ function notifyFormattingBlocked(): void {
|
|
|
88
89
|
return
|
|
89
90
|
}
|
|
90
91
|
lastFormattingBlockedToastAt = now
|
|
91
|
-
signals.showToast(
|
|
92
|
+
signals.showToast(STRINGS.editor.formattingBlocked, 'info')
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
export function notifyLockedElement(): void {
|
|
@@ -97,7 +98,7 @@ export function notifyLockedElement(): void {
|
|
|
97
98
|
return
|
|
98
99
|
}
|
|
99
100
|
lastLockedToastAt = now
|
|
100
|
-
signals.showToast(
|
|
101
|
+
signals.showToast(STRINGS.editor.lockedElement, 'info')
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
/**
|
|
@@ -1197,14 +1198,14 @@ function setupMarkdownClickHandler(
|
|
|
1197
1198
|
const entry = manifest.entries[cmsId] as ManifestEntry | undefined
|
|
1198
1199
|
|
|
1199
1200
|
if (!entry) {
|
|
1200
|
-
signals.showToast(
|
|
1201
|
+
signals.showToast(STRINGS.editor.markdownNotInManifest, 'error')
|
|
1201
1202
|
return
|
|
1202
1203
|
}
|
|
1203
1204
|
|
|
1204
1205
|
// Check if it has a content path for markdown
|
|
1205
1206
|
const contentPath = entry.contentPath
|
|
1206
1207
|
if (!contentPath) {
|
|
1207
|
-
signals.showToast(
|
|
1208
|
+
signals.showToast(STRINGS.editor.noMarkdownPath, 'error')
|
|
1208
1209
|
return
|
|
1209
1210
|
}
|
|
1210
1211
|
|
|
@@ -1213,7 +1214,7 @@ function setupMarkdownClickHandler(
|
|
|
1213
1214
|
const result = await getMarkdownContent(config.apiBase, contentPath)
|
|
1214
1215
|
|
|
1215
1216
|
if (!result) {
|
|
1216
|
-
signals.showToast(
|
|
1217
|
+
signals.showToast(STRINGS.editor.markdownContentMissing, 'error')
|
|
1217
1218
|
return
|
|
1218
1219
|
}
|
|
1219
1220
|
|
|
@@ -1231,7 +1232,7 @@ function setupMarkdownClickHandler(
|
|
|
1231
1232
|
signals.setMarkdownEditorOpen(true)
|
|
1232
1233
|
} catch (error) {
|
|
1233
1234
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
1234
|
-
signals.showToast(
|
|
1235
|
+
signals.showToast(STRINGS.editor.markdownLoadFailed(message), 'error')
|
|
1235
1236
|
logDebug(config.debug, 'Failed to fetch markdown content:', error)
|
|
1236
1237
|
}
|
|
1237
1238
|
})
|
|
@@ -2,6 +2,7 @@ import { useCallback, useState } from 'preact/hooks'
|
|
|
2
2
|
import { logDebug } from '../dom'
|
|
3
3
|
import { getComponentInstances } from '../manifest'
|
|
4
4
|
import * as signals from '../signals'
|
|
5
|
+
import { STRINGS } from '../strings'
|
|
5
6
|
import type { CmsConfig, CmsManifest, ComponentInstance, InsertPosition } from '../types'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -86,7 +87,7 @@ export function useBlockEditorHandlers({
|
|
|
86
87
|
(componentId: string, props: Record<string, any>) => {
|
|
87
88
|
logDebug(config.debug, 'Update props for component:', componentId, props)
|
|
88
89
|
// TODO: Implement prop update logic - this will require server-side file modification
|
|
89
|
-
showToast(
|
|
90
|
+
showToast(STRINGS.block.propsPreviewOnly, 'info')
|
|
90
91
|
},
|
|
91
92
|
[config.debug, showToast],
|
|
92
93
|
)
|
|
@@ -151,7 +152,7 @@ export function useBlockEditorHandlers({
|
|
|
151
152
|
throw new Error(error || 'Failed to add array item')
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
showToast(
|
|
155
|
+
showToast(STRINGS.block.insertItem(position), 'success')
|
|
155
156
|
} else {
|
|
156
157
|
// Standard component insertion
|
|
157
158
|
const response = await fetch(`${config.apiBase}/insert-component`, {
|
|
@@ -175,7 +176,7 @@ export function useBlockEditorHandlers({
|
|
|
175
176
|
throw new Error(error || 'Failed to insert component')
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
showToast(
|
|
179
|
+
showToast(STRINGS.block.insertComponent(componentName, position), 'success')
|
|
179
180
|
}
|
|
180
181
|
} catch (error) {
|
|
181
182
|
console.error('[CMS] Failed to insert component:', error)
|
|
@@ -183,7 +184,7 @@ export function useBlockEditorHandlers({
|
|
|
183
184
|
// Remove the preview on failure
|
|
184
185
|
previewEl?.remove()
|
|
185
186
|
|
|
186
|
-
showToast(arrayMode ?
|
|
187
|
+
showToast(arrayMode ? STRINGS.block.insertItemFailed : STRINGS.block.insertComponentFailed, 'error')
|
|
187
188
|
}
|
|
188
189
|
},
|
|
189
190
|
[config.apiBase, config.debug, config, showToast],
|
|
@@ -232,7 +233,7 @@ export function useBlockEditorHandlers({
|
|
|
232
233
|
throw new Error(error || `Failed to ${arrayMode ? 'remove item' : 'remove component'}`)
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
showToast(arrayMode ?
|
|
236
|
+
showToast(arrayMode ? STRINGS.block.removeItem : STRINGS.block.removeComponent, 'success')
|
|
236
237
|
|
|
237
238
|
// Visually collapse and hide the component until HMR refreshes the page
|
|
238
239
|
if (componentEl) {
|
|
@@ -240,7 +241,7 @@ export function useBlockEditorHandlers({
|
|
|
240
241
|
}
|
|
241
242
|
} catch (error) {
|
|
242
243
|
console.error('[CMS] Failed to remove component:', error)
|
|
243
|
-
showToast(arrayMode ?
|
|
244
|
+
showToast(arrayMode ? STRINGS.block.removeItemFailed : STRINGS.block.removeComponentFailed, 'error')
|
|
244
245
|
|
|
245
246
|
// Restore the component's appearance on failure
|
|
246
247
|
if (componentEl) {
|
package/src/editor/index.tsx
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from './signals'
|
|
59
59
|
import * as signals from './signals'
|
|
60
60
|
import { hasAnyMarkdownDraft, hasPendingEntryNavigation, loadEditingState, loadSettingsFromStorage, saveSettingsToStorage } from './storage'
|
|
61
|
+
import { STRINGS } from './strings'
|
|
61
62
|
import CMS_STYLES from './styles.css?inline'
|
|
62
63
|
import { generateCSSVariables, resolveTheme } from './themes'
|
|
63
64
|
|
|
@@ -307,20 +308,20 @@ const CmsUI = () => {
|
|
|
307
308
|
try {
|
|
308
309
|
const result = await saveAllChanges(config, updateUI)
|
|
309
310
|
if (result.success) {
|
|
310
|
-
signals.showToast(
|
|
311
|
+
signals.showToast(STRINGS.save.success(result.updated), 'success')
|
|
311
312
|
} else if (result.errors) {
|
|
312
313
|
const details = result.errors.map(e => e.error).join('; ')
|
|
313
|
-
signals.showToast(
|
|
314
|
+
signals.showToast(STRINGS.save.failed(details), 'error')
|
|
314
315
|
}
|
|
315
316
|
} catch (err) {
|
|
316
317
|
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
317
|
-
signals.showToast(
|
|
318
|
+
signals.showToast(STRINGS.save.failed(message), 'error')
|
|
318
319
|
}
|
|
319
320
|
}, [config, updateUI])
|
|
320
321
|
|
|
321
322
|
const handleDiscard = useCallback(() => {
|
|
322
323
|
discardAllChanges(updateUI)
|
|
323
|
-
signals.showToast(
|
|
324
|
+
signals.showToast(STRINGS.save.discarded, 'info')
|
|
324
325
|
}, [updateUI])
|
|
325
326
|
|
|
326
327
|
const handleOpenCollection = useCallback((name: string) => {
|
|
@@ -334,7 +335,7 @@ const CmsUI = () => {
|
|
|
334
335
|
|
|
335
336
|
const handleEditContent = useCallback(async () => {
|
|
336
337
|
if (!await openMarkdownEditorForCurrentPage()) {
|
|
337
|
-
signals.showToast(
|
|
338
|
+
signals.showToast(STRINGS.save.noCollectionContent, 'error')
|
|
338
339
|
}
|
|
339
340
|
}, [])
|
|
340
341
|
|
|
@@ -738,7 +739,7 @@ class CmsEditor {
|
|
|
738
739
|
this.appRoot.style.display = isHidden ? 'block' : 'none'
|
|
739
740
|
|
|
740
741
|
// Use signals.showToast for consistency
|
|
741
|
-
signals.showToast(isHidden ?
|
|
742
|
+
signals.showToast(isHidden ? STRINGS.editingMode.enabled : STRINGS.editingMode.disabled, 'info')
|
|
742
743
|
}
|
|
743
744
|
}
|
|
744
745
|
|
package/src/editor/signals.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { fetchManifest, getMarkdownContent } from './api'
|
|
|
4
4
|
import type { ToastMessage, ToastType } from './components/toast/types'
|
|
5
5
|
import { getConfig } from './config'
|
|
6
6
|
import { clearMarkdownDraft, loadMarkdownDraft, saveMarkdownDraft } from './storage'
|
|
7
|
+
import { STRINGS } from './strings'
|
|
7
8
|
import type {
|
|
8
9
|
AttributeEditorState,
|
|
9
10
|
BlockEditorState,
|
|
@@ -416,6 +417,10 @@ export const mediaLibraryState = signal<MediaLibraryState>(
|
|
|
416
417
|
// Convenience computed signals for media library
|
|
417
418
|
export const isMediaLibraryOpen = computed(() => mediaLibraryState.value.isOpen)
|
|
418
419
|
|
|
420
|
+
// Background-image overlay panel open state (lifted from local useState so the
|
|
421
|
+
// toolbar can hide itself when this right-side panel is showing).
|
|
422
|
+
export const isBgImageOverlayOpen = signal(false)
|
|
423
|
+
|
|
419
424
|
// ============================================================================
|
|
420
425
|
// Create Page State Signals
|
|
421
426
|
// ============================================================================
|
|
@@ -1205,33 +1210,59 @@ export async function openMarkdownEditorForEntry(
|
|
|
1205
1210
|
sourcePath: string,
|
|
1206
1211
|
collectionDefinition: CollectionDefinition,
|
|
1207
1212
|
): Promise<void> {
|
|
1208
|
-
|
|
1209
|
-
|
|
1213
|
+
// Open the editor immediately with placeholder content so the modal
|
|
1214
|
+
// transition feels seamless — content streams in once fetched.
|
|
1215
|
+
markdownEditorState.value = {
|
|
1216
|
+
isOpen: true,
|
|
1217
|
+
currentPage: {
|
|
1218
|
+
filePath: sourcePath,
|
|
1219
|
+
slug,
|
|
1220
|
+
frontmatter: {},
|
|
1221
|
+
content: '',
|
|
1222
|
+
isDirty: false,
|
|
1223
|
+
},
|
|
1224
|
+
activeElementId: null,
|
|
1225
|
+
mode: 'edit',
|
|
1226
|
+
collectionDefinition,
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Guard against late responses: if the user closed the modal or navigated
|
|
1230
|
+
// to a different entry while the fetch was in-flight, drop the result.
|
|
1231
|
+
const isStillTargeted = () => markdownEditorState.value.currentPage?.filePath === sourcePath
|
|
1232
|
+
|
|
1233
|
+
let frontmatter: Record<string, unknown>
|
|
1234
|
+
let content: string
|
|
1210
1235
|
|
|
1211
1236
|
try {
|
|
1212
1237
|
const result = await getMarkdownContent(config.value.apiBase, sourcePath)
|
|
1213
|
-
if (
|
|
1214
|
-
|
|
1215
|
-
|
|
1238
|
+
if (!isStillTargeted()) return
|
|
1239
|
+
if (!result) {
|
|
1240
|
+
resetMarkdownEditorState()
|
|
1241
|
+
showToast(`Could not load entry: ${slug}`, 'error')
|
|
1242
|
+
return
|
|
1216
1243
|
}
|
|
1244
|
+
frontmatter = result.frontmatter
|
|
1245
|
+
content = result.content
|
|
1217
1246
|
} catch (err) {
|
|
1218
1247
|
console.error('[CMS] Failed to fetch markdown content for entry:', err)
|
|
1248
|
+
if (!isStillTargeted()) return
|
|
1249
|
+
resetMarkdownEditorState()
|
|
1250
|
+
showToast(`Could not load entry: ${slug}`, 'error')
|
|
1251
|
+
return
|
|
1219
1252
|
}
|
|
1220
1253
|
|
|
1221
1254
|
const recovered = await maybeRecoverDraft(sourcePath, frontmatter, content)
|
|
1255
|
+
if (!isStillTargeted()) return
|
|
1222
1256
|
|
|
1223
1257
|
markdownEditorState.value = {
|
|
1224
|
-
|
|
1258
|
+
...markdownEditorState.value,
|
|
1225
1259
|
currentPage: {
|
|
1226
1260
|
filePath: sourcePath,
|
|
1227
1261
|
slug,
|
|
1228
|
-
frontmatter: recovered.frontmatter
|
|
1262
|
+
frontmatter: recovered.frontmatter,
|
|
1229
1263
|
content: recovered.content,
|
|
1230
1264
|
isDirty: recovered.isDirty,
|
|
1231
1265
|
},
|
|
1232
|
-
activeElementId: null,
|
|
1233
|
-
mode: 'edit',
|
|
1234
|
-
collectionDefinition,
|
|
1235
1266
|
}
|
|
1236
1267
|
}
|
|
1237
1268
|
|
|
@@ -1288,10 +1319,10 @@ export function showConfirmDialog(
|
|
|
1288
1319
|
return new Promise((resolve) => {
|
|
1289
1320
|
confirmDialogState.value = {
|
|
1290
1321
|
isOpen: true,
|
|
1291
|
-
title: options.title ??
|
|
1322
|
+
title: options.title ?? STRINGS.dialog.defaults.title,
|
|
1292
1323
|
message: options.message,
|
|
1293
|
-
confirmLabel: options.confirmLabel ??
|
|
1294
|
-
cancelLabel: options.cancelLabel ??
|
|
1324
|
+
confirmLabel: options.confirmLabel ?? STRINGS.dialog.defaults.confirm,
|
|
1325
|
+
cancelLabel: options.cancelLabel ?? STRINGS.dialog.defaults.cancel,
|
|
1295
1326
|
variant: options.variant ?? 'info',
|
|
1296
1327
|
onConfirm: () => {
|
|
1297
1328
|
closeConfirmDialog()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central catalog of editor UI strings. Step 1 of i18n groundwork — no locale
|
|
3
|
+
* switching yet. Parametrized messages are functions so a future `t(key, vars)`
|
|
4
|
+
* helper can swap them in without touching call sites.
|
|
5
|
+
*/
|
|
6
|
+
import type { InsertPosition } from './types'
|
|
7
|
+
|
|
8
|
+
export const STRINGS = {
|
|
9
|
+
dialog: {
|
|
10
|
+
defaults: {
|
|
11
|
+
title: 'Confirm',
|
|
12
|
+
confirm: 'Confirm',
|
|
13
|
+
cancel: 'Cancel',
|
|
14
|
+
},
|
|
15
|
+
discardAll: {
|
|
16
|
+
title: 'Discard Changes',
|
|
17
|
+
message: 'Discard all changes? This cannot be undone.',
|
|
18
|
+
confirm: 'Discard',
|
|
19
|
+
cancel: 'Cancel',
|
|
20
|
+
},
|
|
21
|
+
discardMarkdown: {
|
|
22
|
+
title: 'Discard changes?',
|
|
23
|
+
message: 'You have unsaved changes. Discard them and close?',
|
|
24
|
+
confirm: 'Discard',
|
|
25
|
+
cancel: 'Keep editing',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
toolbar: {
|
|
30
|
+
selectElement: 'Select Element',
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
editingMode: {
|
|
34
|
+
enabled: 'CMS editing enabled',
|
|
35
|
+
disabled: 'CMS editing disabled',
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
editor: {
|
|
39
|
+
formattingBlocked: "Formatting isn't available — this text is used as a plain value",
|
|
40
|
+
lockedElement: "This text can't be edited here — no source file is linked to it",
|
|
41
|
+
markdownNotInManifest: 'Markdown element not found in manifest',
|
|
42
|
+
noMarkdownPath: 'No markdown file path configured for this element',
|
|
43
|
+
markdownContentMissing: 'Markdown content not found',
|
|
44
|
+
markdownLoadFailed: (message: string) => `Failed to load markdown: ${message}`,
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
save: {
|
|
48
|
+
success: (updated: number) => `Saved ${updated} change(s) successfully!`,
|
|
49
|
+
failed: (details: string) => `Save failed: ${details}`,
|
|
50
|
+
discarded: 'All changes discarded',
|
|
51
|
+
noCollectionContent: 'No collection content found on this page',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
block: {
|
|
55
|
+
propsPreviewOnly: 'Props updated (preview only)',
|
|
56
|
+
insertItem: (position: InsertPosition) => `Item added ${position} current item`,
|
|
57
|
+
insertComponent: (componentName: string, position: InsertPosition) => `${componentName} inserted ${position} component`,
|
|
58
|
+
insertItemFailed: 'Failed to add array item',
|
|
59
|
+
insertComponentFailed: 'Failed to insert component',
|
|
60
|
+
removeItem: 'Item removed',
|
|
61
|
+
removeComponent: 'Component removed',
|
|
62
|
+
removeItemFailed: 'Failed to remove item',
|
|
63
|
+
removeComponentFailed: 'Failed to remove component',
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
reference: {
|
|
67
|
+
updated: 'Reference updated',
|
|
68
|
+
updateFailed: 'Failed to update reference',
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
markdown: {
|
|
72
|
+
saveSuccess: 'Content saved',
|
|
73
|
+
saveFailed: 'Failed to save markdown',
|
|
74
|
+
saveFailedDetails: (message: string) => `Save failed: ${message}`,
|
|
75
|
+
initFailed: 'Failed to initialize markdown editor',
|
|
76
|
+
titleRequired: 'Please enter a title',
|
|
77
|
+
slugRequired: 'Please enter a slug',
|
|
78
|
+
pageCreated: 'Page created',
|
|
79
|
+
createFailed: 'Failed to create page',
|
|
80
|
+
createFailedDetails: (message: string) => `Create failed: ${message}`,
|
|
81
|
+
previewElementMissing: 'Could not find page element to preview',
|
|
82
|
+
previewGenerationFailed: 'Failed to generate preview',
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
media: {
|
|
86
|
+
notConfigured: 'CMS not configured',
|
|
87
|
+
loadFailed: 'Failed to load media library',
|
|
88
|
+
imageRequired: 'Please drop an image file',
|
|
89
|
+
uploadSucceeded: 'File uploaded successfully',
|
|
90
|
+
uploadedNextToEntry: 'Uploaded next to entry',
|
|
91
|
+
fileUploaded: 'File uploaded',
|
|
92
|
+
imageInserted: 'Image uploaded and inserted',
|
|
93
|
+
uploadFailed: 'Upload failed',
|
|
94
|
+
invalidFolderName: 'Invalid folder name',
|
|
95
|
+
folderCreated: 'Folder created',
|
|
96
|
+
folderCreateFailed: 'Failed to create folder',
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
redirects: {
|
|
100
|
+
updated: 'Redirect updated',
|
|
101
|
+
updateFailed: 'Failed to update',
|
|
102
|
+
deleted: 'Redirect deleted',
|
|
103
|
+
deleteFailed: 'Failed to delete',
|
|
104
|
+
added: 'Redirect added',
|
|
105
|
+
addFailed: 'Failed to add redirect',
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
slug: {
|
|
109
|
+
updated: 'Slug updated',
|
|
110
|
+
renameFailed: 'Failed to rename',
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
page: {
|
|
114
|
+
deleted: 'Page deleted',
|
|
115
|
+
deleteFailed: 'Failed to delete page',
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
seo: {
|
|
119
|
+
saveSuccess: (updated: number) => `Saved ${updated} SEO change(s) successfully!`,
|
|
120
|
+
saveFailed: (details: string) => `SEO save failed: ${details}`,
|
|
121
|
+
saveFailedFallback: 'Failed to save SEO changes',
|
|
122
|
+
},
|
|
123
|
+
} as const
|