@plutocms/docs 0.0.0-stage → 0.16.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/.gitattributes +1 -0
- package/.prettierignore +5 -0
- package/CHANGELOG.md +186 -0
- package/FEATURES.md +34 -0
- package/README.md +40 -2
- package/app/assets/css/pluto-layer.css +40 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +165 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +161 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockModeSwitch.vue +46 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockOutline.vue +146 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockOutlineList.vue +347 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPalette.vue +86 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +232 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +269 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockRawEditor.vue +62 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockTextEditor.vue +92 -0
- package/app/components/Pluto/Docs/Blocks/PlutoPreviewBlock.vue +80 -0
- package/app/components/Pluto/Docs/PlutoFieldMdc.vue +117 -0
- package/app/components/Pluto/Docs/PlutoMdcInsertMenu.vue +36 -0
- package/app/components/PlutoContentPreview.vue +35 -0
- package/app/composables/pluto-block-editor.ts +459 -0
- package/app/composables/pluto-content-edit-link.ts +19 -0
- package/app/composables/pluto-content-preview.ts +29 -0
- package/app/composables/pluto-content-runtime.ts +237 -0
- package/app/pages/_pluto/preview.vue +555 -0
- package/app/plugins/pluto-docs.ts +50 -0
- package/i18n/locales/en.json +253 -0
- package/i18n/locales/pt-BR.json +289 -0
- package/modules/pluto-docs/blocks.ts +217 -0
- package/modules/pluto-docs/index.ts +230 -0
- package/modules/pluto-docs/manifest.ts +159 -0
- package/modules/pluto-docs/options.ts +258 -0
- package/nuxt.config.ts +36 -0
- package/package.json +56 -4
- package/server/handlers/navigation.get.ts +7 -0
- package/server/handlers/page.get.ts +7 -0
- package/server/handlers/surround.get.ts +7 -0
- package/server/plugins/pluto-docs.ts +25 -0
- package/server/utils/pluto-docs-runtime.ts +504 -0
- package/shared/types/docs.d.ts +40 -0
- package/shared/types/pluto-blocks.d.ts +10 -0
- package/shared/types/pluto-manifest.d.ts +6 -0
- package/shared/utils/blocks/component-meta.ts +285 -0
- package/shared/utils/blocks/define.ts +15 -0
- package/shared/utils/blocks/editor.ts +357 -0
- package/shared/utils/blocks/history.ts +94 -0
- package/shared/utils/blocks/mdc-props.ts +321 -0
- package/shared/utils/blocks/operations.ts +692 -0
- package/shared/utils/blocks/parse.ts +444 -0
- package/shared/utils/blocks/presets.ts +384 -0
- package/shared/utils/blocks/preview-marks.ts +98 -0
- package/shared/utils/blocks/preview-protocol.ts +195 -0
- package/shared/utils/blocks/registry.ts +295 -0
- package/shared/utils/blocks/serialize.ts +107 -0
- package/shared/utils/blocks/tree.ts +85 -0
- package/shared/utils/blocks/types.ts +195 -0
- package/shared/utils/content-route.ts +40 -0
- package/shared/utils/docs-type.ts +290 -0
- package/shared/utils/mdc-snippets.ts +114 -0
- package/shared/utils/navigation-config.ts +19 -0
- package/shared/utils/runtime-content.ts +403 -0
- package/tailwind.lint.css +10 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The right panel (the Block tab): the controls of the selected block. A Text
|
|
3
|
+
// block has a rich text box. A Code block has a language, a file name and a
|
|
4
|
+
// code box. A component has its content (one control for each slot) and its
|
|
5
|
+
// settings. Unknown settings are in the Advanced section. Nothing here shows
|
|
6
|
+
// raw keys or YAML, except in Advanced and in "Edit as Markdown".
|
|
7
|
+
import type {
|
|
8
|
+
PlutoBlock,
|
|
9
|
+
PlutoComponentBlock,
|
|
10
|
+
PlutoPropValue,
|
|
11
|
+
} from '../../../../../shared/utils/blocks/types'
|
|
12
|
+
import {
|
|
13
|
+
resolveSlotMode,
|
|
14
|
+
slotMarkdown,
|
|
15
|
+
} from '../../../../../shared/utils/blocks/editor'
|
|
16
|
+
import { blockMarkdown } from '../../../../../shared/utils/blocks/operations'
|
|
17
|
+
import PlutoBlockPropControl from './PlutoBlockPropControl.vue'
|
|
18
|
+
import PlutoBlockRawEditor from './PlutoBlockRawEditor.vue'
|
|
19
|
+
import PlutoBlockTextEditor from './PlutoBlockTextEditor.vue'
|
|
20
|
+
|
|
21
|
+
defineProps<{
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const editor = usePlutoBlockEditor()
|
|
26
|
+
const { t, tx } = usePlutoI18n()
|
|
27
|
+
const { slotLabel, addLabel } = usePlutoBlockLabels()
|
|
28
|
+
|
|
29
|
+
const block = computed(() => editor.selected.value?.block)
|
|
30
|
+
const info = computed(() =>
|
|
31
|
+
block.value ? editor.describe(block.value) : undefined
|
|
32
|
+
)
|
|
33
|
+
const definition = computed(() =>
|
|
34
|
+
block.value ? editor.definitionOf(block.value) : undefined
|
|
35
|
+
)
|
|
36
|
+
const parent = computed(() => editor.selected.value?.parent)
|
|
37
|
+
|
|
38
|
+
// "Edit as Markdown" shows the Markdown of the block in a box. It resets with the selection.
|
|
39
|
+
const rawOpen = ref(false)
|
|
40
|
+
watch(
|
|
41
|
+
() => block.value?.id,
|
|
42
|
+
() => {
|
|
43
|
+
rawOpen.value = false
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const description = computed(() => {
|
|
48
|
+
const text = definition.value?.description
|
|
49
|
+
return text ? tx(text) : undefined
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const LANGUAGES = [
|
|
53
|
+
'bash',
|
|
54
|
+
'css',
|
|
55
|
+
'diff',
|
|
56
|
+
'html',
|
|
57
|
+
'js',
|
|
58
|
+
'json',
|
|
59
|
+
'md',
|
|
60
|
+
'sh',
|
|
61
|
+
'sql',
|
|
62
|
+
'ts',
|
|
63
|
+
'tsx',
|
|
64
|
+
'vue',
|
|
65
|
+
'yaml',
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
// --- Component settings ---------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
const settings = computed(() => {
|
|
71
|
+
const props = definition.value?.props ?? {}
|
|
72
|
+
return Object.entries(props).filter(([, prop]) => !prop.advanced)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const advanced = computed(() => {
|
|
76
|
+
const props = definition.value?.props ?? {}
|
|
77
|
+
return Object.entries(props).filter(([, prop]) => prop.advanced)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// A prop with a `slot` (the label of a button) is one field for the prop and the slot. The slot
|
|
81
|
+
// does not show in Content.
|
|
82
|
+
const coveredSlots = computed(
|
|
83
|
+
() =>
|
|
84
|
+
new Set(
|
|
85
|
+
Object.values(definition.value?.props ?? {}).flatMap((prop) =>
|
|
86
|
+
prop.slot ? [prop.slot] : []
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// The field edits the slot when the block has text in it, else the prop. It is chosen when the
|
|
92
|
+
// block is selected, so clearing the field does not move the text to the other place while typing.
|
|
93
|
+
const slotBacked = ref(new Set<string>())
|
|
94
|
+
watch(
|
|
95
|
+
() => block.value?.id,
|
|
96
|
+
() => {
|
|
97
|
+
const current = block.value
|
|
98
|
+
slotBacked.value = new Set(
|
|
99
|
+
current?.kind === 'component'
|
|
100
|
+
? Object.entries(definition.value?.props ?? {}).flatMap(
|
|
101
|
+
([name, prop]) =>
|
|
102
|
+
prop.slot && slotMarkdown(current, prop.slot).trim() ? [name] : []
|
|
103
|
+
)
|
|
104
|
+
: []
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
{ immediate: true }
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
function propValue(name: string): PlutoPropValue | undefined {
|
|
111
|
+
const slot = definition.value?.props?.[name]?.slot
|
|
112
|
+
return slot && slotBacked.value.has(name)
|
|
113
|
+
? slotMarkdown(component(), slot)
|
|
114
|
+
: component().props[name]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function setPropValue(name: string, value: PlutoPropValue) {
|
|
118
|
+
const slot = definition.value?.props?.[name]?.slot
|
|
119
|
+
if (slot && slotBacked.value.has(name)) {
|
|
120
|
+
editor.setSlot(component().id, slot, String(value ?? ''))
|
|
121
|
+
} else {
|
|
122
|
+
editor.setProp(component().id, name, value)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const slots = computed(() => {
|
|
127
|
+
const current = block.value
|
|
128
|
+
if (current?.kind !== 'component') {
|
|
129
|
+
return []
|
|
130
|
+
}
|
|
131
|
+
return Object.entries(definition.value?.slots ?? {})
|
|
132
|
+
.filter(([name]) => !coveredSlots.value.has(name))
|
|
133
|
+
.map(([name, slot]) => ({
|
|
134
|
+
name,
|
|
135
|
+
label: slotLabel(definition.value, name),
|
|
136
|
+
add: addLabel(definition.value, name),
|
|
137
|
+
mode: resolveSlotMode(
|
|
138
|
+
current,
|
|
139
|
+
name,
|
|
140
|
+
slot.content === 'rich' ||
|
|
141
|
+
slot.content === 'inline' ||
|
|
142
|
+
slot.content === 'blocks'
|
|
143
|
+
? slot.content
|
|
144
|
+
: undefined
|
|
145
|
+
),
|
|
146
|
+
children: current.slots[name] ?? [],
|
|
147
|
+
accepts: slot.accepts,
|
|
148
|
+
}))
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
function component(): PlutoComponentBlock {
|
|
152
|
+
return block.value as PlutoComponentBlock
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function childInfo(child: PlutoBlock) {
|
|
156
|
+
const described = editor.describe(child)
|
|
157
|
+
return { ...described, label: tx(described.label) }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function addTo(name: string) {
|
|
161
|
+
editor.openPalette({
|
|
162
|
+
parentId: component().id,
|
|
163
|
+
slot: name,
|
|
164
|
+
index: (component().slots[name] ?? []).length,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function rawMarkdown(): string {
|
|
169
|
+
return block.value ? blockMarkdown(block.value) : ''
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function applyRaw(markdown: string) {
|
|
173
|
+
if (!block.value) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
if (block.value.kind === 'raw' || block.value.kind === 'text') {
|
|
177
|
+
editor.updateText(block.value.id, markdown)
|
|
178
|
+
} else {
|
|
179
|
+
editor.replaceMarkdown(block.value.id, markdown)
|
|
180
|
+
rawOpen.value = false
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<template>
|
|
186
|
+
<div
|
|
187
|
+
:aria-label="t('docs.editor.blockPanel')"
|
|
188
|
+
class="flex flex-col gap-5"
|
|
189
|
+
role="region"
|
|
190
|
+
data-pluto-block-ui
|
|
191
|
+
>
|
|
192
|
+
<UEmpty
|
|
193
|
+
v-if="!block || !info"
|
|
194
|
+
:title="t('docs.editor.nothingSelectedTitle')"
|
|
195
|
+
:description="t('docs.editor.nothingSelected')"
|
|
196
|
+
:actions="[
|
|
197
|
+
{
|
|
198
|
+
label: t('docs.editor.addBlock'),
|
|
199
|
+
icon: 'i-lucide-plus',
|
|
200
|
+
color: 'primary',
|
|
201
|
+
variant: 'soft',
|
|
202
|
+
onClick: () => editor.openPalette(),
|
|
203
|
+
},
|
|
204
|
+
]"
|
|
205
|
+
icon="i-lucide-mouse-pointer-click"
|
|
206
|
+
variant="naked"
|
|
207
|
+
/>
|
|
208
|
+
|
|
209
|
+
<template v-else>
|
|
210
|
+
<header class="flex items-start gap-3">
|
|
211
|
+
<span
|
|
212
|
+
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary"
|
|
213
|
+
>
|
|
214
|
+
<UIcon :name="info.icon" class="size-5" />
|
|
215
|
+
</span>
|
|
216
|
+
<div class="min-w-0 flex-1">
|
|
217
|
+
<h3 class="truncate text-base font-semibold text-highlighted">
|
|
218
|
+
{{ tx(info.label) }}
|
|
219
|
+
</h3>
|
|
220
|
+
<p v-if="description" class="text-sm text-muted">
|
|
221
|
+
{{ description }}
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
</header>
|
|
225
|
+
|
|
226
|
+
<UButton
|
|
227
|
+
v-if="parent"
|
|
228
|
+
:label="
|
|
229
|
+
t('docs.editor.selectParent', {
|
|
230
|
+
name: tx(editor.describe(parent).label),
|
|
231
|
+
})
|
|
232
|
+
"
|
|
233
|
+
icon="i-lucide-corner-left-up"
|
|
234
|
+
color="neutral"
|
|
235
|
+
variant="link"
|
|
236
|
+
size="xs"
|
|
237
|
+
class="-mt-3 self-start p-0"
|
|
238
|
+
@click="editor.select(parent.id)"
|
|
239
|
+
/>
|
|
240
|
+
|
|
241
|
+
<!-- Edit as Markdown -->
|
|
242
|
+
<section v-if="rawOpen" class="flex flex-col gap-2">
|
|
243
|
+
<p class="text-sm text-muted">
|
|
244
|
+
{{ t('docs.editor.rawNote') }}
|
|
245
|
+
</p>
|
|
246
|
+
<PlutoBlockRawEditor
|
|
247
|
+
:model-value="rawMarkdown()"
|
|
248
|
+
:disabled="disabled"
|
|
249
|
+
:label="t('docs.editor.rawLabel')"
|
|
250
|
+
:live="block.kind === 'text' || block.kind === 'raw'"
|
|
251
|
+
:rows="block.kind === 'component' ? 10 : 8"
|
|
252
|
+
@update:model-value="applyRaw"
|
|
253
|
+
/>
|
|
254
|
+
</section>
|
|
255
|
+
|
|
256
|
+
<!-- Text -->
|
|
257
|
+
<template v-else-if="block.kind === 'text'">
|
|
258
|
+
<PlutoBlockTextEditor
|
|
259
|
+
:key="block.id"
|
|
260
|
+
:model-value="block.markdown"
|
|
261
|
+
:disabled="disabled"
|
|
262
|
+
:label="t('docs.editor.textLabel')"
|
|
263
|
+
@update:model-value="editor.updateText(block.id, $event)"
|
|
264
|
+
/>
|
|
265
|
+
</template>
|
|
266
|
+
|
|
267
|
+
<!-- Code -->
|
|
268
|
+
<template v-else-if="block.kind === 'code'">
|
|
269
|
+
<div class="grid grid-cols-2 gap-3">
|
|
270
|
+
<UFormField
|
|
271
|
+
:label="t('docs.editor.language')"
|
|
272
|
+
:help="t('docs.editor.languageHelp')"
|
|
273
|
+
>
|
|
274
|
+
<UInput
|
|
275
|
+
:model-value="block.lang"
|
|
276
|
+
:disabled="disabled"
|
|
277
|
+
list="pluto-block-languages"
|
|
278
|
+
placeholder="ts"
|
|
279
|
+
autocomplete="off"
|
|
280
|
+
spellcheck="false"
|
|
281
|
+
class="w-full"
|
|
282
|
+
@update:model-value="
|
|
283
|
+
editor.updateCode(block.id, { lang: String($event ?? '') })
|
|
284
|
+
"
|
|
285
|
+
/>
|
|
286
|
+
<datalist id="pluto-block-languages">
|
|
287
|
+
<option
|
|
288
|
+
v-for="language in LANGUAGES"
|
|
289
|
+
:key="language"
|
|
290
|
+
:value="language"
|
|
291
|
+
/>
|
|
292
|
+
</datalist>
|
|
293
|
+
</UFormField>
|
|
294
|
+
<UFormField
|
|
295
|
+
:label="t('docs.editor.fileName')"
|
|
296
|
+
:help="t('docs.editor.fileNameHelp')"
|
|
297
|
+
>
|
|
298
|
+
<UInput
|
|
299
|
+
:model-value="block.filename ?? ''"
|
|
300
|
+
:disabled="disabled"
|
|
301
|
+
placeholder="app.ts"
|
|
302
|
+
autocomplete="off"
|
|
303
|
+
spellcheck="false"
|
|
304
|
+
class="w-full"
|
|
305
|
+
@update:model-value="
|
|
306
|
+
editor.updateCode(block.id, {
|
|
307
|
+
filename: String($event ?? '') || undefined,
|
|
308
|
+
})
|
|
309
|
+
"
|
|
310
|
+
/>
|
|
311
|
+
</UFormField>
|
|
312
|
+
</div>
|
|
313
|
+
<UFormField :label="t('docs.editor.code')">
|
|
314
|
+
<UTextarea
|
|
315
|
+
:model-value="block.code"
|
|
316
|
+
:disabled="disabled"
|
|
317
|
+
:rows="10"
|
|
318
|
+
:maxrows="28"
|
|
319
|
+
:ui="{ base: 'font-mono text-sm/6' }"
|
|
320
|
+
spellcheck="false"
|
|
321
|
+
autocomplete="off"
|
|
322
|
+
autocapitalize="off"
|
|
323
|
+
class="w-full"
|
|
324
|
+
autoresize
|
|
325
|
+
@update:model-value="
|
|
326
|
+
editor.updateCode(block.id, { code: String($event ?? '') })
|
|
327
|
+
"
|
|
328
|
+
/>
|
|
329
|
+
</UFormField>
|
|
330
|
+
</template>
|
|
331
|
+
|
|
332
|
+
<!-- Markdown (raw) -->
|
|
333
|
+
<template v-else-if="block.kind === 'raw'">
|
|
334
|
+
<UAlert
|
|
335
|
+
:description="
|
|
336
|
+
block.reason === 'table'
|
|
337
|
+
? t('docs.editor.tableNote')
|
|
338
|
+
: t('docs.editor.rawBlockNote')
|
|
339
|
+
"
|
|
340
|
+
color="neutral"
|
|
341
|
+
variant="subtle"
|
|
342
|
+
icon="i-lucide-info"
|
|
343
|
+
/>
|
|
344
|
+
<PlutoBlockRawEditor
|
|
345
|
+
:model-value="block.markdown"
|
|
346
|
+
:disabled="disabled"
|
|
347
|
+
:label="t('docs.editor.rawLabel')"
|
|
348
|
+
:rows="12"
|
|
349
|
+
live
|
|
350
|
+
@update:model-value="editor.updateText(block.id, $event)"
|
|
351
|
+
/>
|
|
352
|
+
</template>
|
|
353
|
+
|
|
354
|
+
<!-- Component -->
|
|
355
|
+
<template v-else>
|
|
356
|
+
<section v-if="slots.length" class="flex flex-col gap-4">
|
|
357
|
+
<h4 class="text-xs font-semibold uppercase tracking-wide text-muted">
|
|
358
|
+
{{ t('docs.editor.content') }}
|
|
359
|
+
</h4>
|
|
360
|
+
|
|
361
|
+
<UFormField
|
|
362
|
+
v-for="slot in slots"
|
|
363
|
+
:key="slot.name"
|
|
364
|
+
:label="slots.length > 1 ? slot.label : undefined"
|
|
365
|
+
>
|
|
366
|
+
<PlutoBlockTextEditor
|
|
367
|
+
v-if="slot.mode === 'rich'"
|
|
368
|
+
:model-value="slotMarkdown(component(), slot.name)"
|
|
369
|
+
:disabled="disabled"
|
|
370
|
+
:label="slot.label"
|
|
371
|
+
compact
|
|
372
|
+
@update:model-value="editor.setSlot(block.id, slot.name, $event)"
|
|
373
|
+
/>
|
|
374
|
+
|
|
375
|
+
<UInput
|
|
376
|
+
v-else-if="slot.mode === 'inline'"
|
|
377
|
+
:model-value="slotMarkdown(component(), slot.name)"
|
|
378
|
+
:disabled="disabled"
|
|
379
|
+
:aria-label="slot.label"
|
|
380
|
+
class="w-full"
|
|
381
|
+
@update:model-value="
|
|
382
|
+
editor.setSlot(block.id, slot.name, String($event ?? ''))
|
|
383
|
+
"
|
|
384
|
+
/>
|
|
385
|
+
|
|
386
|
+
<PlutoBlockRawEditor
|
|
387
|
+
v-else-if="slot.mode === 'markdown'"
|
|
388
|
+
:model-value="slotMarkdown(component(), slot.name)"
|
|
389
|
+
:disabled="disabled"
|
|
390
|
+
:label="slot.label"
|
|
391
|
+
:rows="5"
|
|
392
|
+
live
|
|
393
|
+
@update:model-value="editor.setSlot(block.id, slot.name, $event)"
|
|
394
|
+
/>
|
|
395
|
+
|
|
396
|
+
<div v-else class="flex flex-col gap-1">
|
|
397
|
+
<ul v-if="slot.children.length" class="flex flex-col gap-1">
|
|
398
|
+
<li v-for="child in slot.children" :key="child.id">
|
|
399
|
+
<button
|
|
400
|
+
type="button"
|
|
401
|
+
class="flex w-full items-center gap-2 rounded-md border border-default px-2 py-1.5 text-start text-sm hover:bg-elevated focus-visible:outline-2 focus-visible:outline-primary"
|
|
402
|
+
@click="editor.select(child.id)"
|
|
403
|
+
>
|
|
404
|
+
<UIcon
|
|
405
|
+
:name="childInfo(child).icon"
|
|
406
|
+
class="size-4 shrink-0 text-muted"
|
|
407
|
+
/>
|
|
408
|
+
<span class="min-w-0 flex-1 truncate">
|
|
409
|
+
<span class="font-medium text-highlighted">{{
|
|
410
|
+
childInfo(child).label
|
|
411
|
+
}}</span>
|
|
412
|
+
<span v-if="childInfo(child).excerpt" class="text-muted"
|
|
413
|
+
>: {{ childInfo(child).excerpt }}</span
|
|
414
|
+
>
|
|
415
|
+
</span>
|
|
416
|
+
<UIcon
|
|
417
|
+
name="i-lucide-chevron-right"
|
|
418
|
+
class="size-4 shrink-0 text-dimmed"
|
|
419
|
+
/>
|
|
420
|
+
</button>
|
|
421
|
+
</li>
|
|
422
|
+
</ul>
|
|
423
|
+
<p v-else class="text-sm text-muted">
|
|
424
|
+
{{ t('docs.editor.slotEmpty') }}
|
|
425
|
+
</p>
|
|
426
|
+
<UButton
|
|
427
|
+
:label="slot.add"
|
|
428
|
+
:disabled="disabled"
|
|
429
|
+
icon="i-lucide-plus"
|
|
430
|
+
color="neutral"
|
|
431
|
+
variant="outline"
|
|
432
|
+
size="xs"
|
|
433
|
+
class="self-start"
|
|
434
|
+
@click="addTo(slot.name)"
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
</UFormField>
|
|
438
|
+
</section>
|
|
439
|
+
|
|
440
|
+
<section v-if="settings.length" class="flex flex-col gap-4">
|
|
441
|
+
<h4 class="text-xs font-semibold uppercase tracking-wide text-muted">
|
|
442
|
+
{{ t('docs.editor.settings') }}
|
|
443
|
+
</h4>
|
|
444
|
+
<PlutoBlockPropControl
|
|
445
|
+
v-for="[name, prop] in settings"
|
|
446
|
+
:key="name"
|
|
447
|
+
:name="name"
|
|
448
|
+
:definition="prop"
|
|
449
|
+
:model-value="propValue(name)"
|
|
450
|
+
:disabled="disabled"
|
|
451
|
+
@update:model-value="setPropValue(name, $event)"
|
|
452
|
+
/>
|
|
453
|
+
</section>
|
|
454
|
+
|
|
455
|
+
<details
|
|
456
|
+
v-if="advanced.length"
|
|
457
|
+
class="group rounded-lg border border-default"
|
|
458
|
+
>
|
|
459
|
+
<summary
|
|
460
|
+
class="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-sm font-medium text-highlighted focus-visible:outline-2 focus-visible:outline-primary"
|
|
461
|
+
>
|
|
462
|
+
<UIcon
|
|
463
|
+
name="i-lucide-chevron-right"
|
|
464
|
+
class="size-4 text-muted transition-transform group-open:rotate-90"
|
|
465
|
+
/>
|
|
466
|
+
{{ t('docs.editor.advanced') }}
|
|
467
|
+
</summary>
|
|
468
|
+
<div class="flex flex-col gap-4 border-t border-default p-3">
|
|
469
|
+
<PlutoBlockPropControl
|
|
470
|
+
v-for="[name, prop] in advanced"
|
|
471
|
+
:key="name"
|
|
472
|
+
:name="name"
|
|
473
|
+
:definition="prop"
|
|
474
|
+
:model-value="component().props[name]"
|
|
475
|
+
:disabled="disabled"
|
|
476
|
+
@update:model-value="editor.setProp(block.id, name, $event)"
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
</details>
|
|
480
|
+
</template>
|
|
481
|
+
|
|
482
|
+
<!-- Actions -->
|
|
483
|
+
<div class="flex flex-wrap gap-2 border-t border-default pt-4">
|
|
484
|
+
<UButton
|
|
485
|
+
:label="t('docs.editor.duplicate')"
|
|
486
|
+
:disabled="disabled"
|
|
487
|
+
icon="i-lucide-copy"
|
|
488
|
+
color="neutral"
|
|
489
|
+
variant="outline"
|
|
490
|
+
size="sm"
|
|
491
|
+
@click="editor.duplicate(block.id)"
|
|
492
|
+
/>
|
|
493
|
+
<UButton
|
|
494
|
+
:label="
|
|
495
|
+
rawOpen
|
|
496
|
+
? t('docs.editor.backToControls')
|
|
497
|
+
: t('docs.editor.editAsMarkdown')
|
|
498
|
+
"
|
|
499
|
+
:icon="rawOpen ? 'i-lucide-sliders-horizontal' : 'i-lucide-file-code'"
|
|
500
|
+
:disabled="disabled || block.kind === 'raw'"
|
|
501
|
+
color="neutral"
|
|
502
|
+
variant="outline"
|
|
503
|
+
size="sm"
|
|
504
|
+
@click="rawOpen = !rawOpen"
|
|
505
|
+
/>
|
|
506
|
+
<UButton
|
|
507
|
+
:label="t('docs.editor.delete')"
|
|
508
|
+
:disabled="disabled"
|
|
509
|
+
icon="i-lucide-trash-2"
|
|
510
|
+
color="error"
|
|
511
|
+
variant="soft"
|
|
512
|
+
size="sm"
|
|
513
|
+
class="ms-auto"
|
|
514
|
+
@click="editor.remove(block.id)"
|
|
515
|
+
/>
|
|
516
|
+
</div>
|
|
517
|
+
</template>
|
|
518
|
+
</div>
|
|
519
|
+
</template>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The Markdown mode of the `mdc` widget in a canvas form: the Markdown at the
|
|
3
|
+
// left and the live preview at the right, side by side (stacked on a narrow
|
|
4
|
+
// screen). The handle between them resizes them, and a cookie keeps the size.
|
|
5
|
+
// The preview is the frame of the visual editor, so it renders with the site
|
|
6
|
+
// parser. A click on a block in the preview selects the first line of that
|
|
7
|
+
// block in the Markdown and scrolls to it. The Markdown editor is
|
|
8
|
+
// PlutoMarkdownCodeEditor of core (CodeMirror). The Markdown is the only editor
|
|
9
|
+
// here: the block model only feeds the preview and never writes.
|
|
10
|
+
import type { SplitterItem } from '@nuxt/ui'
|
|
11
|
+
import type { PlutoMarkdownCodeEditor as CodeEditor } from '#components'
|
|
12
|
+
// The component layer. Only the two editor roots import it, so it stays out of the public pages.
|
|
13
|
+
import componentBlocks from '#pluto-docs/blocks'
|
|
14
|
+
import { findBlock } from '../../../../../shared/utils/blocks/tree'
|
|
15
|
+
import PlutoMdcInsertMenu from '../PlutoMdcInsertMenu.vue'
|
|
16
|
+
import PlutoBlockModeSwitch from './PlutoBlockModeSwitch.vue'
|
|
17
|
+
import PlutoBlockPreviewFrame from './PlutoBlockPreviewFrame.vue'
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{
|
|
20
|
+
modelValue: string
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
mode: 'visual' | 'markdown'
|
|
23
|
+
placeholder?: string
|
|
24
|
+
label?: string
|
|
25
|
+
}>()
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{
|
|
28
|
+
'update:modelValue': [value: string]
|
|
29
|
+
'update:mode': [mode: 'visual' | 'markdown']
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const form = usePlutoContentForm()
|
|
33
|
+
const blockSources = usePlutoBlockSources(componentBlocks)
|
|
34
|
+
|
|
35
|
+
const editor = createPlutoBlockEditor({
|
|
36
|
+
source: () => props.modelValue,
|
|
37
|
+
emit: () => {},
|
|
38
|
+
sources: blockSources,
|
|
39
|
+
})
|
|
40
|
+
provide(plutoBlockEditorKey, editor)
|
|
41
|
+
|
|
42
|
+
// The front matter for the preview header: the item without the body.
|
|
43
|
+
const frontmatter = computed<Record<string, unknown>>(() => {
|
|
44
|
+
const item = form?.item.value ?? {}
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(item).filter(([key]) => key !== 'body')
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const code = useTemplateRef<InstanceType<typeof CodeEditor>>('code')
|
|
51
|
+
|
|
52
|
+
// --- The split ---------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
const isDesktop = useMediaQuery('(min-width: 64rem)')
|
|
55
|
+
// The size of the Markdown pane, in percent of the row (desktop) or of the column (narrow).
|
|
56
|
+
const splitSize = useCookie<number>('pluto-mdc-split', {
|
|
57
|
+
default: () => 50,
|
|
58
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
59
|
+
sameSite: 'lax',
|
|
60
|
+
})
|
|
61
|
+
const MIN = 25
|
|
62
|
+
const MAX = 75
|
|
63
|
+
// Read once: a reactive `defaultSize` would rebuild the panels on each save of the size.
|
|
64
|
+
const initialSize = Math.min(MAX, Math.max(MIN, Number(splitSize.value) || 50))
|
|
65
|
+
const items: SplitterItem[] = [
|
|
66
|
+
{
|
|
67
|
+
id: 'pluto-mdc-source',
|
|
68
|
+
slot: 'source',
|
|
69
|
+
defaultSize: initialSize,
|
|
70
|
+
minSize: MIN,
|
|
71
|
+
maxSize: MAX,
|
|
72
|
+
},
|
|
73
|
+
{ id: 'pluto-mdc-preview', slot: 'preview' },
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
// The pointer over the iframe goes to the frame, and the drag stops. So the preview takes no
|
|
77
|
+
// pointer events while the handle moves. The size is saved when the drag ends.
|
|
78
|
+
const dragging = ref(false)
|
|
79
|
+
let lastLayout: number[] = []
|
|
80
|
+
function onLayout(sizes: number[]) {
|
|
81
|
+
lastLayout = sizes
|
|
82
|
+
}
|
|
83
|
+
function onDragging(_index: number, value: boolean) {
|
|
84
|
+
dragging.value = value
|
|
85
|
+
if (!value && lastLayout[0]) {
|
|
86
|
+
splitSize.value = Math.round(lastLayout[0])
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A click on a block in the preview: go to that block in the Markdown.
|
|
91
|
+
watch(
|
|
92
|
+
() => editor.selection.value,
|
|
93
|
+
(selection) => {
|
|
94
|
+
if (selection?.source !== 'preview') {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
const original = findBlock(editor.doc.value, selection.id)?.block.original
|
|
98
|
+
if (original) {
|
|
99
|
+
code.value?.reveal(original.start, original.end)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<template>
|
|
106
|
+
<USplitter
|
|
107
|
+
id="pluto-mdc-split"
|
|
108
|
+
:items="items"
|
|
109
|
+
:orientation="isDesktop ? 'horizontal' : 'vertical'"
|
|
110
|
+
:ui="{
|
|
111
|
+
root: 'h-full min-h-0',
|
|
112
|
+
// A 1px line. A `::before` 2.5px wider on each side is the 6px grab area,
|
|
113
|
+
// and the 6px haze-400 bar: it fades in after 300ms of hover, and shows at
|
|
114
|
+
// once while dragging. In both directions. The `!` keeps the duration and
|
|
115
|
+
// the delay against the no-transition rule of the admin (core pluto-layer.css).
|
|
116
|
+
handle:
|
|
117
|
+
'bg-(--ui-border) data-[orientation=horizontal]:w-px data-[orientation=vertical]:h-px before:absolute before:z-10 before:transition-colors before:duration-150! hover:before:bg-haze-400 hover:before:delay-300! data-[resize-handle-state=drag]:before:bg-haze-400 data-[resize-handle-state=drag]:before:delay-0! data-[orientation=horizontal]:before:inset-y-0 data-[orientation=horizontal]:before:inset-x-[-2.5px] data-[orientation=vertical]:before:inset-x-0 data-[orientation=vertical]:before:inset-y-[-2.5px]',
|
|
118
|
+
}"
|
|
119
|
+
data-block-canvas
|
|
120
|
+
@layout="onLayout"
|
|
121
|
+
@dragging="onDragging"
|
|
122
|
+
>
|
|
123
|
+
<!-- The source pane: the code editor, and the mode switch in a fixed footer, as in the Visual mode. -->
|
|
124
|
+
<template #source>
|
|
125
|
+
<div class="flex size-full min-h-0 flex-col">
|
|
126
|
+
<div class="min-h-0 flex-1 p-4">
|
|
127
|
+
<PlutoMarkdownCodeEditor
|
|
128
|
+
ref="code"
|
|
129
|
+
:model-value="modelValue"
|
|
130
|
+
:disabled="disabled"
|
|
131
|
+
:placeholder="placeholder"
|
|
132
|
+
:aria-label="label"
|
|
133
|
+
fill
|
|
134
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
135
|
+
>
|
|
136
|
+
<template #toolbar="{ insert }">
|
|
137
|
+
<PlutoMdcInsertMenu :insert="insert" :disabled="disabled" />
|
|
138
|
+
</template>
|
|
139
|
+
</PlutoMarkdownCodeEditor>
|
|
140
|
+
</div>
|
|
141
|
+
<div
|
|
142
|
+
class="flex shrink-0 items-center justify-between gap-2 border-t border-default px-3 py-2"
|
|
143
|
+
>
|
|
144
|
+
<PlutoBlockModeSwitch
|
|
145
|
+
:mode="mode"
|
|
146
|
+
:disabled="disabled"
|
|
147
|
+
@update:mode="emit('update:mode', $event)"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</template>
|
|
152
|
+
|
|
153
|
+
<template #preview>
|
|
154
|
+
<PlutoBlockPreviewFrame
|
|
155
|
+
:frontmatter="frontmatter"
|
|
156
|
+
:class="{ 'pointer-events-none': dragging }"
|
|
157
|
+
class="size-full"
|
|
158
|
+
/>
|
|
159
|
+
</template>
|
|
160
|
+
</USplitter>
|
|
161
|
+
</template>
|