@nuasite/cms 0.18.1 → 0.19.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 +52746 -36711
- package/package.json +16 -14
- package/src/build-processor.ts +4 -1
- package/src/collection-scanner.ts +425 -48
- package/src/dev-middleware.ts +26 -203
- package/src/editor/api.ts +1 -22
- package/src/editor/components/ai-chat.tsx +3 -3
- package/src/editor/components/ai-tooltip.tsx +2 -1
- package/src/editor/components/block-editor.tsx +13 -108
- package/src/editor/components/collections-browser.tsx +168 -205
- package/src/editor/components/component-card.tsx +49 -0
- package/src/editor/components/confirm-dialog.tsx +34 -47
- package/src/editor/components/create-page-modal.tsx +529 -101
- package/src/editor/components/delete-page-dialog.tsx +100 -0
- package/src/editor/components/fields.tsx +175 -0
- package/src/editor/components/frontmatter-fields.tsx +281 -70
- package/src/editor/components/frontmatter-sidebar.tsx +223 -0
- package/src/editor/components/highlight-overlay.ts +3 -2
- package/src/editor/components/markdown-editor-overlay.tsx +131 -85
- package/src/editor/components/markdown-inline-editor.tsx +74 -5
- package/src/editor/components/mdx-block-view.tsx +102 -0
- package/src/editor/components/mdx-component-picker.tsx +123 -0
- package/src/editor/components/mdx-props-editor.tsx +94 -0
- package/src/editor/components/media-library.tsx +373 -100
- package/src/editor/components/modal-shell.tsx +87 -0
- package/src/editor/components/prop-editor.tsx +52 -0
- package/src/editor/components/redirect-countdown.tsx +3 -1
- package/src/editor/components/redirects-manager.tsx +269 -0
- package/src/editor/components/reference-picker.tsx +203 -0
- package/src/editor/components/seo-editor.tsx +285 -303
- package/src/editor/components/toast/toast-container.tsx +2 -1
- package/src/editor/components/toolbar.tsx +177 -46
- package/src/editor/constants.ts +26 -0
- package/src/editor/editor.ts +112 -0
- package/src/editor/fetch.ts +62 -0
- package/src/editor/index.tsx +19 -1
- package/src/editor/markdown-api.ts +105 -156
- package/src/editor/milkdown-mdx-plugin.tsx +269 -0
- package/src/editor/signals.ts +206 -13
- package/src/editor/types.ts +52 -1
- package/src/handlers/api-routes.ts +251 -0
- package/src/handlers/component-ops.ts +2 -18
- package/src/handlers/markdown-ops.ts +202 -47
- package/src/handlers/page-ops.ts +229 -0
- package/src/handlers/redirect-ops.ts +163 -0
- package/src/handlers/source-writer.ts +157 -1
- package/src/html-processor.ts +14 -2
- package/src/index.ts +76 -2
- package/src/manifest-writer.ts +19 -1
- package/src/media/contember.ts +2 -1
- package/src/media/local.ts +66 -28
- package/src/media/project-images.ts +81 -0
- package/src/media/s3.ts +32 -11
- package/src/media/types.ts +24 -2
- package/src/shared.ts +27 -0
- package/src/source-finder/collection-finder.ts +219 -41
- package/src/source-finder/index.ts +7 -1
- package/src/source-finder/search-index.ts +178 -36
- package/src/source-finder/snippet-utils.ts +423 -3
- package/src/types.ts +111 -2
- package/src/utils.ts +40 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { signal } from '@preact/signals'
|
|
2
|
-
import { useMemo } from 'preact/hooks'
|
|
2
|
+
import { useMemo, useState } from 'preact/hooks'
|
|
3
3
|
import { deleteMarkdownPage } from '../markdown-api'
|
|
4
4
|
import {
|
|
5
5
|
closeCollectionsBrowser,
|
|
@@ -12,10 +12,14 @@ import {
|
|
|
12
12
|
selectedBrowserCollection,
|
|
13
13
|
} from '../signals'
|
|
14
14
|
import { savePendingEntryNavigation } from '../storage'
|
|
15
|
+
import { ChevronRightIcon, CollectionIcon } from './create-page-modal'
|
|
16
|
+
import { CloseButton, ModalBackdrop, ModalHeader } from './modal-shell'
|
|
15
17
|
|
|
16
18
|
const deletingEntry = signal<string | null>(null)
|
|
17
19
|
const confirmDeleteSlug = signal<string | null>(null)
|
|
18
20
|
|
|
21
|
+
const EMPTY_ENTRIES: never[] = []
|
|
22
|
+
|
|
19
23
|
export function CollectionsBrowser() {
|
|
20
24
|
const visible = isCollectionsBrowserOpen.value
|
|
21
25
|
const selected = selectedBrowserCollection.value
|
|
@@ -26,23 +30,27 @@ export function CollectionsBrowser() {
|
|
|
26
30
|
return Object.values(collectionDefinitions).sort((a, b) => a.label.localeCompare(b.label))
|
|
27
31
|
}, [collectionDefinitions])
|
|
28
32
|
|
|
33
|
+
const [search, setSearch] = useState('')
|
|
34
|
+
const selectedDef = selected ? collectionDefinitions[selected] : undefined
|
|
35
|
+
const entries = selectedDef?.entries ?? EMPTY_ENTRIES
|
|
36
|
+
|
|
37
|
+
const filteredEntries = useMemo(() => {
|
|
38
|
+
if (!search) return entries
|
|
39
|
+
const q = search.toLowerCase()
|
|
40
|
+
return entries.filter(e => (e.title || '').toLowerCase().includes(q) || e.slug.toLowerCase().includes(q))
|
|
41
|
+
}, [entries, search])
|
|
42
|
+
|
|
29
43
|
if (!visible) return null
|
|
30
44
|
|
|
31
45
|
const handleClose = () => {
|
|
32
46
|
closeCollectionsBrowser()
|
|
33
47
|
}
|
|
34
48
|
|
|
35
|
-
const handleBackdropClick = (e: Event) => {
|
|
36
|
-
handleClose()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
49
|
// View 2: Entry list for selected collection
|
|
40
50
|
if (selected) {
|
|
41
|
-
const def =
|
|
51
|
+
const def = selectedDef
|
|
42
52
|
if (!def) return null
|
|
43
53
|
|
|
44
|
-
const entries = def.entries ?? []
|
|
45
|
-
|
|
46
54
|
const handleEntryClick = (slug: string, sourcePath: string, pathname?: string) => {
|
|
47
55
|
closeCollectionsBrowser()
|
|
48
56
|
if (pathname) {
|
|
@@ -99,228 +107,183 @@ export function CollectionsBrowser() {
|
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
return (
|
|
102
|
-
<
|
|
103
|
-
class="
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
<ModalBackdrop onClose={handleClose} extraClass="flex flex-col max-h-[80vh]">
|
|
111
|
+
<div class="flex items-center justify-between p-5 border-b border-white/10 shrink-0">
|
|
112
|
+
<div class="flex items-center gap-3">
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={() => {
|
|
116
|
+
setSearch('')
|
|
117
|
+
selectBrowserCollection(null)
|
|
118
|
+
}}
|
|
119
|
+
class="text-white/50 hover:text-white p-1 hover:bg-white/10 rounded-full transition-colors"
|
|
120
|
+
data-cms-ui
|
|
121
|
+
>
|
|
122
|
+
<BackArrowIcon />
|
|
123
|
+
</button>
|
|
124
|
+
<h2 class="text-lg font-semibold text-white">{def.label}</h2>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="flex items-center gap-2">
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
onClick={handleAddNew}
|
|
130
|
+
class="px-3 py-1.5 text-sm font-medium text-black bg-cms-primary hover:bg-cms-primary/80 rounded-cms-pill transition-colors"
|
|
131
|
+
data-cms-ui
|
|
132
|
+
>
|
|
133
|
+
+ Add New
|
|
134
|
+
</button>
|
|
135
|
+
<CloseButton onClick={handleClose} />
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{entries.length > 0 && (
|
|
140
|
+
<div class="px-5 pt-4 pb-2 shrink-0">
|
|
141
|
+
<div class="relative">
|
|
142
|
+
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
143
|
+
<circle cx="11" cy="11" r="8" />
|
|
144
|
+
<path stroke-linecap="round" stroke-width="2" d="m21 21-4.3-4.3" />
|
|
145
|
+
</svg>
|
|
146
|
+
<input
|
|
147
|
+
type="text"
|
|
148
|
+
placeholder="Search..."
|
|
149
|
+
value={search}
|
|
150
|
+
onInput={(e) => setSearch((e.target as HTMLInputElement).value)}
|
|
151
|
+
class="w-full pl-9 pr-3 py-2 text-sm text-white bg-white/5 border border-white/10 rounded-cms-lg placeholder:text-white/30 focus:outline-none focus:border-white/20"
|
|
119
152
|
data-cms-ui
|
|
120
|
-
|
|
121
|
-
<BackArrowIcon />
|
|
122
|
-
</button>
|
|
123
|
-
<h2 class="text-lg font-semibold text-white">{def.label}</h2>
|
|
153
|
+
/>
|
|
124
154
|
</div>
|
|
125
|
-
<div class="
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
class="px-3 py-1.5 text-sm font-medium text-black bg-cms-primary hover:bg-cms-primary/80 rounded-cms-pill transition-colors"
|
|
130
|
-
data-cms-ui
|
|
131
|
-
>
|
|
132
|
-
+ Add New
|
|
133
|
-
</button>
|
|
134
|
-
<CloseButton onClick={handleClose} />
|
|
155
|
+
<div class="text-white/30 text-xs mt-2">
|
|
156
|
+
{search
|
|
157
|
+
? `${filteredEntries.length} of ${entries.length}`
|
|
158
|
+
: `${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`}
|
|
135
159
|
</div>
|
|
136
160
|
</div>
|
|
161
|
+
)}
|
|
137
162
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
disabled={deletingEntry.value === entry.slug}
|
|
157
|
-
class="px-3 py-1 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded-cms-pill transition-colors disabled:opacity-50"
|
|
158
|
-
data-cms-ui
|
|
159
|
-
>
|
|
160
|
-
{deletingEntry.value === entry.slug ? 'Deleting...' : 'Delete'}
|
|
161
|
-
</button>
|
|
162
|
-
<button
|
|
163
|
-
type="button"
|
|
164
|
-
onClick={handleCancelDelete}
|
|
165
|
-
class="px-3 py-1 text-xs font-medium text-white/60 hover:text-white bg-white/10 hover:bg-white/20 rounded-cms-pill transition-colors"
|
|
166
|
-
data-cms-ui
|
|
167
|
-
>
|
|
168
|
-
Cancel
|
|
169
|
-
</button>
|
|
163
|
+
<div class="px-5 pb-5 space-y-1 overflow-y-auto flex-1 min-h-0">
|
|
164
|
+
{entries.length === 0 && (
|
|
165
|
+
<div class="text-white/50 text-sm text-center py-8">
|
|
166
|
+
No entries yet. Click "Add New" to create one.
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
{search && filteredEntries.length === 0 && entries.length > 0 && (
|
|
170
|
+
<div class="text-white/50 text-sm text-center py-8">
|
|
171
|
+
No matches for "{search}"
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
{filteredEntries.map((entry) => (
|
|
175
|
+
<div key={entry.slug} class="relative" data-cms-ui>
|
|
176
|
+
{confirmDeleteSlug.value === entry.slug
|
|
177
|
+
? (
|
|
178
|
+
<div class="flex items-center gap-2 px-4 py-3 bg-red-500/10 border border-red-500/20 rounded-cms-lg" data-cms-ui>
|
|
179
|
+
<div class="flex-1 min-w-0 text-sm text-white/70">
|
|
180
|
+
Delete "{entry.title || entry.slug}"?
|
|
170
181
|
</div>
|
|
171
|
-
)
|
|
172
|
-
: (
|
|
173
182
|
<button
|
|
174
183
|
type="button"
|
|
175
|
-
onClick={() =>
|
|
176
|
-
|
|
184
|
+
onClick={() => handleConfirmDelete(entry.slug, entry.sourcePath)}
|
|
185
|
+
disabled={deletingEntry.value === entry.slug}
|
|
186
|
+
class="px-3 py-1 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded-cms-pill transition-colors disabled:opacity-50"
|
|
187
|
+
data-cms-ui
|
|
188
|
+
>
|
|
189
|
+
{deletingEntry.value === entry.slug ? 'Deleting...' : 'Delete'}
|
|
190
|
+
</button>
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={handleCancelDelete}
|
|
194
|
+
class="px-3 py-1 text-xs font-medium text-white/60 hover:text-white bg-white/10 hover:bg-white/20 rounded-cms-pill transition-colors"
|
|
177
195
|
data-cms-ui
|
|
178
196
|
>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
Cancel
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
: (
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => handleEntryClick(entry.slug, entry.sourcePath, entry.pathname)}
|
|
205
|
+
class="w-full flex items-center gap-3 px-4 py-3 hover:bg-white/10 rounded-cms-lg transition-colors text-left group"
|
|
206
|
+
data-cms-ui
|
|
207
|
+
>
|
|
208
|
+
<div class="flex-1 min-w-0">
|
|
209
|
+
<div class={`font-medium truncate ${entry.draft ? 'text-white/40' : 'text-white'}`}>
|
|
210
|
+
{entry.title || entry.slug}
|
|
184
211
|
</div>
|
|
185
|
-
{entry.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
class="w-4 h-4 text-white/20 group-hover:text-white/40 shrink-0 transition-colors"
|
|
201
|
-
fill="none"
|
|
202
|
-
stroke="currentColor"
|
|
203
|
-
viewBox="0 0 24 24"
|
|
204
|
-
>
|
|
205
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
206
|
-
</svg>
|
|
212
|
+
{entry.title && <div class="text-white/30 text-xs truncate">{entry.slug}</div>}
|
|
213
|
+
</div>
|
|
214
|
+
{entry.draft && (
|
|
215
|
+
<span class="shrink-0 px-2 py-0.5 text-xs font-medium text-amber-400/80 bg-amber-400/10 rounded-full border border-amber-400/20">
|
|
216
|
+
Draft
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={(e) => handleDeleteClick(e, entry.slug)}
|
|
222
|
+
class="shrink-0 p-1 text-white/0 group-hover:text-white/30 hover:!text-red-400 rounded transition-colors"
|
|
223
|
+
title="Delete entry"
|
|
224
|
+
data-cms-ui
|
|
225
|
+
>
|
|
226
|
+
<TrashIcon />
|
|
207
227
|
</button>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
228
|
+
<svg
|
|
229
|
+
class="w-4 h-4 text-white/20 group-hover:text-white/40 shrink-0 transition-colors"
|
|
230
|
+
fill="none"
|
|
231
|
+
stroke="currentColor"
|
|
232
|
+
viewBox="0 0 24 24"
|
|
233
|
+
>
|
|
234
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
235
|
+
</svg>
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
212
240
|
</div>
|
|
213
|
-
</
|
|
241
|
+
</ModalBackdrop>
|
|
214
242
|
)
|
|
215
243
|
}
|
|
216
244
|
|
|
217
|
-
//
|
|
245
|
+
// Empty state
|
|
218
246
|
if (collections.length === 0) {
|
|
219
247
|
return (
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
onClick={(e) => e.stopPropagation()}
|
|
228
|
-
data-cms-ui
|
|
229
|
-
>
|
|
230
|
-
<div class="flex items-center justify-between p-5 border-b border-white/10">
|
|
231
|
-
<h2 class="text-lg font-semibold text-white">Collections</h2>
|
|
232
|
-
<CloseButton onClick={handleClose} />
|
|
233
|
-
</div>
|
|
234
|
-
<div class="p-8 text-center">
|
|
235
|
-
<div class="text-white/60 mb-4">No content collections found.</div>
|
|
236
|
-
<p class="text-white/40 text-sm">
|
|
237
|
-
Add markdown files to <code class="bg-white/10 px-1.5 py-0.5 rounded">src/content/</code> subdirectories to enable collections.
|
|
238
|
-
</p>
|
|
239
|
-
</div>
|
|
248
|
+
<ModalBackdrop onClose={handleClose}>
|
|
249
|
+
<ModalHeader title="Collections" onClose={handleClose} />
|
|
250
|
+
<div class="p-8 text-center">
|
|
251
|
+
<div class="text-white/60 mb-4">No content collections found.</div>
|
|
252
|
+
<p class="text-white/40 text-sm">
|
|
253
|
+
Add markdown files to <code class="bg-white/10 px-1.5 py-0.5 rounded">src/content/</code> subdirectories to enable collections.
|
|
254
|
+
</p>
|
|
240
255
|
</div>
|
|
241
|
-
</
|
|
256
|
+
</ModalBackdrop>
|
|
242
257
|
)
|
|
243
258
|
}
|
|
244
259
|
|
|
260
|
+
// Collection list
|
|
245
261
|
return (
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
type="button"
|
|
265
|
-
onClick={() => selectBrowserCollection(col.name)}
|
|
266
|
-
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"
|
|
267
|
-
data-cms-ui
|
|
268
|
-
>
|
|
269
|
-
<div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-md flex items-center justify-center">
|
|
270
|
-
<CollectionIcon />
|
|
271
|
-
</div>
|
|
272
|
-
<div class="flex-1 min-w-0">
|
|
273
|
-
<div class="text-white font-medium">{col.label}</div>
|
|
274
|
-
<div class="text-white/50 text-sm">
|
|
275
|
-
{col.entryCount} {col.entryCount === 1 ? 'entry' : 'entries'}
|
|
276
|
-
</div>
|
|
262
|
+
<ModalBackdrop onClose={handleClose} extraClass="flex flex-col max-h-[80vh]">
|
|
263
|
+
<ModalHeader title="Collections" onClose={handleClose} />
|
|
264
|
+
<div class="p-5 space-y-2 overflow-y-auto flex-1 min-h-0">
|
|
265
|
+
{collections.map((col) => (
|
|
266
|
+
<button
|
|
267
|
+
key={col.name}
|
|
268
|
+
type="button"
|
|
269
|
+
onClick={() => selectBrowserCollection(col.name)}
|
|
270
|
+
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"
|
|
271
|
+
data-cms-ui
|
|
272
|
+
>
|
|
273
|
+
<div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-md flex items-center justify-center">
|
|
274
|
+
<CollectionIcon />
|
|
275
|
+
</div>
|
|
276
|
+
<div class="flex-1 min-w-0">
|
|
277
|
+
<div class="text-white font-medium">{col.label}</div>
|
|
278
|
+
<div class="text-white/50 text-sm">
|
|
279
|
+
{col.entryCount} {col.entryCount === 1 ? 'entry' : 'entries'}
|
|
277
280
|
</div>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
281
|
+
</div>
|
|
282
|
+
<ChevronRightIcon />
|
|
283
|
+
</button>
|
|
284
|
+
))}
|
|
282
285
|
</div>
|
|
283
|
-
</
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ============================================================================
|
|
288
|
-
// Icons
|
|
289
|
-
// ============================================================================
|
|
290
|
-
|
|
291
|
-
function CloseButton({ onClick }: { onClick: () => void }) {
|
|
292
|
-
return (
|
|
293
|
-
<button
|
|
294
|
-
type="button"
|
|
295
|
-
onClick={onClick}
|
|
296
|
-
class="text-white/50 hover:text-white p-1.5 hover:bg-white/10 rounded-full transition-colors"
|
|
297
|
-
data-cms-ui
|
|
298
|
-
>
|
|
299
|
-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
300
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
301
|
-
</svg>
|
|
302
|
-
</button>
|
|
303
|
-
)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function CollectionIcon() {
|
|
307
|
-
return (
|
|
308
|
-
<svg class="w-5 h-5 text-cms-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
309
|
-
<path
|
|
310
|
-
stroke-linecap="round"
|
|
311
|
-
stroke-linejoin="round"
|
|
312
|
-
stroke-width="2"
|
|
313
|
-
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
314
|
-
/>
|
|
315
|
-
</svg>
|
|
316
|
-
)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function ChevronRightIcon() {
|
|
320
|
-
return (
|
|
321
|
-
<svg class="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
322
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
323
|
-
</svg>
|
|
286
|
+
</ModalBackdrop>
|
|
324
287
|
)
|
|
325
288
|
}
|
|
326
289
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ComponentDefinition } from '../types'
|
|
2
|
+
|
|
3
|
+
export function getDefaultProps(definition: ComponentDefinition): Record<string, any> {
|
|
4
|
+
const defaultProps: Record<string, any> = {}
|
|
5
|
+
for (const prop of definition.props) {
|
|
6
|
+
if (prop.defaultValue !== undefined) {
|
|
7
|
+
defaultProps[prop.name] = prop.defaultValue
|
|
8
|
+
} else if (prop.required) {
|
|
9
|
+
defaultProps[prop.name] = ''
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return defaultProps
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ComponentCard({ def, onClick }: { def: ComponentDefinition; onClick: () => void }) {
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
onClick={onClick}
|
|
19
|
+
class="p-4 bg-white/5 border border-white/10 rounded-cms-md cursor-pointer text-left transition-all hover:border-cms-primary/50 hover:bg-white/10 group"
|
|
20
|
+
>
|
|
21
|
+
{def.previewUrl && (
|
|
22
|
+
<div class="mb-3 rounded overflow-hidden bg-white h-30 relative">
|
|
23
|
+
<ComponentPreviewIframe previewUrl={def.previewUrl} previewWidth={def.previewWidth} />
|
|
24
|
+
</div>
|
|
25
|
+
)}
|
|
26
|
+
<div class="font-medium text-white">{def.name}</div>
|
|
27
|
+
{def.description && <div class="text-xs text-white/50 mt-1">{def.description}</div>}
|
|
28
|
+
<div class="text-[11px] text-white/40 mt-2 font-mono">
|
|
29
|
+
{def.props.length} props
|
|
30
|
+
{def.slots && def.slots.length > 0 && ` · ${def.slots.length} slots`}
|
|
31
|
+
</div>
|
|
32
|
+
</button>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ComponentPreviewIframe({ previewUrl, previewWidth }: { previewUrl: string; previewWidth?: number }) {
|
|
37
|
+
const pw = previewWidth ?? 1280
|
|
38
|
+
const scale = 320 / pw
|
|
39
|
+
return (
|
|
40
|
+
<iframe
|
|
41
|
+
src={previewUrl}
|
|
42
|
+
class="border-none pointer-events-none"
|
|
43
|
+
style={{ width: `${pw}px`, height: `${Math.round(120 / scale)}px`, transform: `scale(${scale})`, transformOrigin: 'top left' }}
|
|
44
|
+
sandbox="allow-same-origin"
|
|
45
|
+
loading="lazy"
|
|
46
|
+
tabIndex={-1}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cn } from '../lib/cn'
|
|
2
2
|
import { confirmDialogState } from '../signals'
|
|
3
|
+
import { ModalBackdrop } from './modal-shell'
|
|
3
4
|
|
|
4
5
|
export function ConfirmDialog() {
|
|
5
6
|
const state = confirmDialogState.value
|
|
@@ -14,56 +15,42 @@ export function ConfirmDialog() {
|
|
|
14
15
|
state.onCancel?.()
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const handleBackdropClick = () => {
|
|
18
|
-
handleCancel()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
18
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<div
|
|
28
|
-
class="bg-cms-dark rounded-cms-xl shadow-[0_8px_32px_rgba(0,0,0,0.4)] max-w-sm w-full border border-white/10 mx-4"
|
|
29
|
-
onClick={(e) => e.stopPropagation()}
|
|
30
|
-
data-cms-ui
|
|
31
|
-
>
|
|
32
|
-
{/* Header */}
|
|
33
|
-
<div class="p-5 pb-3">
|
|
34
|
-
<h2 class="text-lg font-semibold text-white">{state.title}</h2>
|
|
35
|
-
</div>
|
|
19
|
+
<ModalBackdrop onClose={handleCancel} maxWidth="max-w-sm" extraClass="mx-4">
|
|
20
|
+
{/* Header */}
|
|
21
|
+
<div class="p-5 pb-3">
|
|
22
|
+
<h2 class="text-lg font-semibold text-white">{state.title}</h2>
|
|
23
|
+
</div>
|
|
36
24
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
{/* Body */}
|
|
26
|
+
<div class="px-5 pb-5">
|
|
27
|
+
<p class="text-sm text-white/70 leading-relaxed">{state.message}</p>
|
|
28
|
+
</div>
|
|
41
29
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</div>
|
|
30
|
+
{/* Footer */}
|
|
31
|
+
<div class="flex items-center justify-end gap-3 p-5 pt-4 border-t border-white/10 bg-white/5 rounded-b-cms-xl">
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={handleCancel}
|
|
35
|
+
class="px-4 py-2.5 text-sm text-white/80 font-medium rounded-cms-pill hover:bg-white/10 hover:text-white transition-colors cursor-pointer"
|
|
36
|
+
data-cms-ui
|
|
37
|
+
>
|
|
38
|
+
{state.cancelLabel}
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={handleConfirm}
|
|
43
|
+
class={cn(
|
|
44
|
+
'px-5 py-2.5 rounded-cms-pill text-sm font-medium transition-colors cursor-pointer',
|
|
45
|
+
state.variant === 'danger' && 'bg-cms-error text-white hover:bg-red-600',
|
|
46
|
+
state.variant === 'warning' && 'bg-amber-500 text-white hover:bg-amber-600',
|
|
47
|
+
state.variant === 'info' && 'bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover',
|
|
48
|
+
)}
|
|
49
|
+
data-cms-ui
|
|
50
|
+
>
|
|
51
|
+
{state.confirmLabel}
|
|
52
|
+
</button>
|
|
66
53
|
</div>
|
|
67
|
-
</
|
|
54
|
+
</ModalBackdrop>
|
|
68
55
|
)
|
|
69
56
|
}
|