@plutocms/docs 0.0.0-stage → 0.15.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.
Files changed (61) hide show
  1. package/.gitattributes +1 -0
  2. package/.prettierignore +5 -0
  3. package/CHANGELOG.md +178 -0
  4. package/FEATURES.md +30 -0
  5. package/README.md +39 -2
  6. package/app/assets/css/pluto-layer.css +40 -0
  7. package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +163 -0
  8. package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
  9. package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +159 -0
  10. package/app/components/Pluto/Docs/Blocks/PlutoBlockModeSwitch.vue +46 -0
  11. package/app/components/Pluto/Docs/Blocks/PlutoBlockOutline.vue +146 -0
  12. package/app/components/Pluto/Docs/Blocks/PlutoBlockOutlineList.vue +347 -0
  13. package/app/components/Pluto/Docs/Blocks/PlutoBlockPalette.vue +85 -0
  14. package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +233 -0
  15. package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +246 -0
  16. package/app/components/Pluto/Docs/Blocks/PlutoBlockRawEditor.vue +62 -0
  17. package/app/components/Pluto/Docs/Blocks/PlutoBlockTextEditor.vue +92 -0
  18. package/app/components/Pluto/Docs/Blocks/PlutoPreviewBlock.vue +80 -0
  19. package/app/components/Pluto/Docs/PlutoFieldMdc.vue +117 -0
  20. package/app/components/Pluto/Docs/PlutoMdcInsertMenu.vue +36 -0
  21. package/app/components/PlutoContentPreview.vue +35 -0
  22. package/app/composables/pluto-block-editor.ts +443 -0
  23. package/app/composables/pluto-content-edit-link.ts +19 -0
  24. package/app/composables/pluto-content-preview.ts +29 -0
  25. package/app/composables/pluto-content-runtime.ts +237 -0
  26. package/app/pages/_pluto/preview.vue +555 -0
  27. package/app/plugins/pluto-docs.ts +50 -0
  28. package/i18n/locales/en.json +253 -0
  29. package/i18n/locales/pt-BR.json +289 -0
  30. package/modules/pluto-docs/index.ts +123 -0
  31. package/modules/pluto-docs/manifest.ts +159 -0
  32. package/modules/pluto-docs/options.ts +224 -0
  33. package/nuxt.config.ts +36 -0
  34. package/package.json +55 -4
  35. package/server/handlers/navigation.get.ts +7 -0
  36. package/server/handlers/page.get.ts +7 -0
  37. package/server/handlers/surround.get.ts +7 -0
  38. package/server/plugins/pluto-docs.ts +25 -0
  39. package/server/utils/pluto-docs-runtime.ts +504 -0
  40. package/shared/types/docs.d.ts +40 -0
  41. package/shared/types/pluto-manifest.d.ts +6 -0
  42. package/shared/utils/blocks/editor.ts +357 -0
  43. package/shared/utils/blocks/history.ts +94 -0
  44. package/shared/utils/blocks/mdc-props.ts +321 -0
  45. package/shared/utils/blocks/operations.ts +692 -0
  46. package/shared/utils/blocks/parse.ts +444 -0
  47. package/shared/utils/blocks/presets.ts +383 -0
  48. package/shared/utils/blocks/preview-marks.ts +98 -0
  49. package/shared/utils/blocks/preview-protocol.ts +195 -0
  50. package/shared/utils/blocks/registry.ts +163 -0
  51. package/shared/utils/blocks/serialize.ts +107 -0
  52. package/shared/utils/blocks/tree.ts +85 -0
  53. package/shared/utils/blocks/types.ts +165 -0
  54. package/shared/utils/content-route.ts +40 -0
  55. package/shared/utils/docs-type.ts +290 -0
  56. package/shared/utils/mdc-snippets.ts +114 -0
  57. package/shared/utils/navigation-config.ts +19 -0
  58. package/shared/utils/runtime-content.ts +403 -0
  59. package/tailwind.lint.css +10 -0
  60. package/tsconfig.json +18 -0
  61. package/vitest.config.ts +7 -0
@@ -0,0 +1,555 @@
1
+ <script setup lang="ts">
2
+ import type { MDCElement, MDCNode, MDCRoot } from '@nuxtjs/mdc'
3
+ // The preview page of the visual block editor. The editor shows it in an
4
+ // iframe. It fetches nothing. The editor sends the blocks (Markdown) with
5
+ // `postMessage`. This page parses each changed block with the site parser
6
+ // (`parseMarkdown` of @nuxtjs/mdc) and renders it with `ContentRenderer`, so
7
+ // the page looks as it does on the site. A click on a block tells the editor
8
+ // which block it is. See the block-editor skill.
9
+ //
10
+ // Security: same origin only. This page ignores a message from another
11
+ // origin or from a window that is not its parent. No remote data loads here.
12
+ import type { CSSProperties } from 'vue'
13
+ import type { PlutoPreviewBlock as PreviewBlockData } from '../../../shared/utils/blocks/preview-protocol'
14
+ import { parseMarkdown } from '@nuxtjs/mdc/runtime'
15
+ import {
16
+ markPreviewChildren,
17
+ PREVIEW_BLOCK_ATTRIBUTE,
18
+ previewParents,
19
+ } from '../../../shared/utils/blocks/preview-marks'
20
+ import {
21
+ isTrustedPreviewOrigin,
22
+ parsePreviewMessage,
23
+ previewMessage,
24
+ } from '../../../shared/utils/blocks/preview-protocol'
25
+ import PlutoPreviewBlock from '../../components/Pluto/Docs/Blocks/PlutoPreviewBlock.vue'
26
+
27
+ definePageMeta({ layout: false, plutoAdminBar: false })
28
+
29
+ const { t } = usePlutoI18n()
30
+
31
+ useHead({
32
+ title: () => t('docs.preview.title'),
33
+ meta: [{ name: 'robots', content: 'noindex, nofollow' }],
34
+ // The preview is for choosing blocks, not for reading: no text selection, no link or image
35
+ // drag, and the default cursor everywhere (not the hand of a link or a button).
36
+ style: [
37
+ {
38
+ textContent:
39
+ '*, *::before, *::after { cursor: default !important; user-select: none !important; -webkit-user-select: none !important; -webkit-user-drag: none !important; }',
40
+ },
41
+ {
42
+ // A thin scrollbar that fits the editor, in both color modes: a gray thumb on no track.
43
+ textContent: [
44
+ 'html { scrollbar-width: thin; scrollbar-color: rgb(128 128 128 / 0.45) transparent; }',
45
+ 'html::-webkit-scrollbar { width: 10px; }',
46
+ 'html::-webkit-scrollbar-track { background: transparent; }',
47
+ 'html::-webkit-scrollbar-thumb { background: rgb(128 128 128 / 0.45); border: 3px solid transparent; border-radius: 9999px; background-clip: content-box; }',
48
+ 'html::-webkit-scrollbar-thumb:hover { background-color: rgb(128 128 128 / 0.7); }',
49
+ ].join('\n'),
50
+ },
51
+ ],
52
+ })
53
+
54
+ // Each block renders with `tag` false, so no wrapper element sits between the blocks (the prose
55
+ // styles read the first and last child). Vue warns in dev about the type of that prop, and about
56
+ // the debug attribute that ContentRenderer adds. The preview app is its own app, so this
57
+ // handler hides only those two messages here.
58
+ if (import.meta.dev) {
59
+ const config = useNuxtApp().vueApp.config
60
+ const previous = config.warnHandler
61
+ config.warnHandler = (message, instance, trace) => {
62
+ if (message.includes('prop "tag"') || message.includes('data-content-id')) {
63
+ return
64
+ }
65
+ if (previous) {
66
+ previous(message, instance, trace)
67
+ } else {
68
+ console.warn(`[Vue warn]: ${message}`, trace)
69
+ }
70
+ }
71
+ }
72
+
73
+ interface RenderedBlock {
74
+ id: string
75
+ label: string
76
+ body?: MDCRoot
77
+ error?: string
78
+ /** The inner components, for the labels and the parents. */
79
+ children?: PreviewBlockData['children']
80
+ }
81
+
82
+ const frontmatter = ref<Record<string, unknown>>({})
83
+ const blocks = shallowRef<RenderedBlock[]>([])
84
+ const selectedId = ref<string | null>(null)
85
+
86
+ // The parsed body of a block, by id and text. The same text gives the same
87
+ // object, so Vue does not render a block that did not change.
88
+ const cache = new Map<string, MDCRoot>()
89
+ const CACHE_LIMIT = 300
90
+ let run = 0
91
+ // A scroll that came before the block was on screen (a new block): do it after the next render.
92
+ let wantedScroll: string | null = null
93
+
94
+ /**
95
+ * Puts the id on each element at the top of a body, and the ids of the inner
96
+ * components on their elements. A component passes the id to its root element.
97
+ * The parse is new, so the inner marks change it in place.
98
+ */
99
+ function mark(root: MDCRoot, block: PreviewBlockData): MDCRoot {
100
+ const children = root.children.map((child): MDCNode => {
101
+ if (child.type !== 'element') {
102
+ return child
103
+ }
104
+ const element = child as MDCElement
105
+ if (block.children) {
106
+ markPreviewChildren(element, block.children)
107
+ }
108
+ return {
109
+ ...element,
110
+ props: { ...element.props, [PREVIEW_BLOCK_ATTRIBUTE]: block.id },
111
+ }
112
+ })
113
+ return { ...root, children }
114
+ }
115
+
116
+ /** The ids of the inner components, for the cache key: the same text with other ids marks other elements. */
117
+ function childIds(children: PreviewBlockData['children']): string {
118
+ return (children ?? [])
119
+ .map((child) => `${child.id}(${childIds(child.children)})`)
120
+ .join(',')
121
+ }
122
+
123
+ async function bodyOf(
124
+ block: PreviewBlockData
125
+ ): Promise<{ body?: MDCRoot; error?: string }> {
126
+ const key = `${block.id}\u0000${block.markdown}\u0000${childIds(block.children)}`
127
+ const cached = cache.get(key)
128
+ if (cached) {
129
+ return { body: cached }
130
+ }
131
+ try {
132
+ const parsed = await parseMarkdown(block.markdown)
133
+ const body = mark(parsed.body, block)
134
+ if (cache.size >= CACHE_LIMIT) {
135
+ cache.delete(cache.keys().next().value!)
136
+ }
137
+ cache.set(key, body)
138
+ return { body }
139
+ } catch (error) {
140
+ return { error: error instanceof Error ? error.message : String(error) }
141
+ }
142
+ }
143
+
144
+ async function render(message: {
145
+ frontmatter: Record<string, unknown>
146
+ blocks: PreviewBlockData[]
147
+ selectedId?: string | null
148
+ }) {
149
+ const current = ++run
150
+ const next = await Promise.all(
151
+ message.blocks.map(async (block): Promise<RenderedBlock> => {
152
+ const label = block.label ?? ''
153
+ if (block.preview === false) {
154
+ return { id: block.id, label }
155
+ }
156
+ return {
157
+ id: block.id,
158
+ label,
159
+ children: block.children,
160
+ ...(await bodyOf(block)),
161
+ }
162
+ })
163
+ )
164
+ // A newer message came while the parser worked.
165
+ if (current !== run) {
166
+ return
167
+ }
168
+ frontmatter.value = message.frontmatter
169
+ blocks.value = next
170
+ if (message.selectedId !== undefined) {
171
+ selectedId.value = message.selectedId
172
+ }
173
+ await nextTick()
174
+ refreshOverlay()
175
+ if (wantedScroll) {
176
+ // The components of a block can mount a moment after the render.
177
+ setTimeout(() => wantedScroll && scrollToBlock(wantedScroll), 80)
178
+ }
179
+ }
180
+
181
+ // --- Talk to the editor ---------------------------------------------------------
182
+
183
+ function post(message: ReturnType<typeof previewMessage>) {
184
+ window.parent?.postMessage(message, window.location.origin)
185
+ }
186
+
187
+ function onMessage(event: MessageEvent) {
188
+ if (
189
+ event.source !== window.parent ||
190
+ !isTrustedPreviewOrigin(event.origin, window.location.origin)
191
+ ) {
192
+ return
193
+ }
194
+ const message = parsePreviewMessage(event.data, 'to-frame')
195
+ if (!message) {
196
+ return
197
+ }
198
+ switch (message.type) {
199
+ case 'render':
200
+ void render(message)
201
+ break
202
+ case 'select':
203
+ selectedId.value = message.id
204
+ nextTick(refreshOverlay)
205
+ break
206
+ case 'scroll-to':
207
+ scrollToBlock(message.id)
208
+ break
209
+ }
210
+ }
211
+
212
+ // --- Outlines -------------------------------------------------------------------
213
+
214
+ const hoverId = ref<string | null>(null)
215
+ const overlay = ref<{ selected: DOMRect | null; hover: DOMRect | null }>({
216
+ selected: null,
217
+ hover: null,
218
+ })
219
+
220
+ /** The elements of a block. A style or a script has no box. */
221
+ function elementsOf(id: string): HTMLElement[] {
222
+ return [
223
+ ...document.querySelectorAll<HTMLElement>(
224
+ `[${PREVIEW_BLOCK_ATTRIBUTE}="${CSS.escape(id)}"]`
225
+ ),
226
+ ].filter(
227
+ (element) => element.tagName !== 'STYLE' && element.tagName !== 'SCRIPT'
228
+ )
229
+ }
230
+
231
+ /** The parent of each inner component: an inner component or a top-level block. */
232
+ const parents = computed(() => {
233
+ const map = new Map<string, string>()
234
+ for (const block of blocks.value) {
235
+ previewParents(block.id, block.children, map)
236
+ }
237
+ return map
238
+ })
239
+
240
+ /**
241
+ * The block that shows for an id: the block itself, or the nearest parent with
242
+ * elements. A component with more than one root element drops the mark, and a
243
+ * text block inside a component has no mark of its own.
244
+ */
245
+ function shownId(id: string | null): string | null {
246
+ let current = id
247
+ while (current && !elementsOf(current).length) {
248
+ current = parents.value.get(current) ?? null
249
+ }
250
+ return current
251
+ }
252
+
253
+ /** The box around two boxes. */
254
+ function union(a: DOMRect | null, b: DOMRect | null): DOMRect | null {
255
+ if (!a || !b) {
256
+ return a ?? b
257
+ }
258
+ const left = Math.min(a.left, b.left)
259
+ const top = Math.min(a.top, b.top)
260
+ return new DOMRect(
261
+ left,
262
+ top,
263
+ Math.max(a.right, b.right) - left,
264
+ Math.max(a.bottom, b.bottom) - top
265
+ )
266
+ }
267
+
268
+ /**
269
+ * The box of an element. An element with no area of its own gives the box of
270
+ * its children: a linked card of Nuxt UI (`UPageCard` with `to`) puts the
271
+ * attributes on an inline link of 0 px width, and a child of that link covers
272
+ * the whole card.
273
+ */
274
+ function elementBox(element: Element, depth = 0): DOMRect | null {
275
+ const rect = element.getBoundingClientRect()
276
+ if (rect.width > 0 && rect.height > 0) {
277
+ return rect
278
+ }
279
+ if (depth > 3) {
280
+ return null
281
+ }
282
+ let box: DOMRect | null = null
283
+ for (const child of element.children) {
284
+ box = union(box, elementBox(child, depth + 1))
285
+ }
286
+ return box
287
+ }
288
+
289
+ /** The box around all elements of a block, in page coordinates. */
290
+ function boxOf(id: string | null): DOMRect | null {
291
+ const shown = shownId(id)
292
+ if (!shown) {
293
+ return null
294
+ }
295
+ let box: DOMRect | null = null
296
+ for (const element of elementsOf(shown)) {
297
+ box = union(box, elementBox(element))
298
+ }
299
+ return box
300
+ ? new DOMRect(
301
+ box.left + window.scrollX,
302
+ box.top + window.scrollY,
303
+ box.width,
304
+ box.height
305
+ )
306
+ : null
307
+ }
308
+
309
+ let frame = 0
310
+ function refreshOverlay() {
311
+ cancelAnimationFrame(frame)
312
+ frame = requestAnimationFrame(() => {
313
+ overlay.value = {
314
+ selected: boxOf(selectedId.value),
315
+ hover: hoverId.value === selectedId.value ? null : boxOf(hoverId.value),
316
+ }
317
+ })
318
+ }
319
+
320
+ function scrollToBlock(id: string) {
321
+ const box = boxOf(id)
322
+ if (!box) {
323
+ wantedScroll = id
324
+ return
325
+ }
326
+ wantedScroll = null
327
+ const top = box.top - window.scrollY
328
+ const margin = 24
329
+ if (top < margin || top + box.height > window.innerHeight - margin) {
330
+ window.scrollTo({ top: Math.max(0, box.top - margin), behavior: 'smooth' })
331
+ }
332
+ }
333
+
334
+ /** The label of each block, the inner components too. */
335
+ const labels = computed(() => {
336
+ const map = new Map<string, string>()
337
+ const add = (children: RenderedBlock['children']) => {
338
+ for (const child of children ?? []) {
339
+ map.set(child.id, child.label ?? '')
340
+ add(child.children)
341
+ }
342
+ }
343
+ for (const block of blocks.value) {
344
+ map.set(block.id, block.label)
345
+ add(block.children)
346
+ }
347
+ return map
348
+ })
349
+
350
+ /** The innermost block under an element: an inner component, else its top-level block. */
351
+ function blockIdAt(target: EventTarget | null): string | null {
352
+ return target instanceof Element
353
+ ? (target
354
+ .closest<HTMLElement>(`[${PREVIEW_BLOCK_ATTRIBUTE}]`)
355
+ ?.getAttribute(PREVIEW_BLOCK_ATTRIBUTE) ?? null)
356
+ : null
357
+ }
358
+
359
+ function selectBlock(id: string | null) {
360
+ selectedId.value = id
361
+ if (id) {
362
+ post(previewMessage('select', { id }))
363
+ }
364
+ refreshOverlay()
365
+ }
366
+
367
+ // A click selects the block. The page does not follow links or press buttons.
368
+ function onClick(event: MouseEvent) {
369
+ event.preventDefault()
370
+ event.stopPropagation()
371
+ selectBlock(blockIdAt(event.target))
372
+ }
373
+
374
+ // A middle click or a Ctrl/Cmd+click (`auxclick`) does not open a link in a new tab, a drag does
375
+ // not take a link or an image, and the text does not select.
376
+ function stopEvent(event: Event) {
377
+ event.preventDefault()
378
+ event.stopPropagation()
379
+ }
380
+
381
+ function onPointerMove(event: PointerEvent) {
382
+ const id = blockIdAt(event.target)
383
+ if (id !== hoverId.value) {
384
+ hoverId.value = id
385
+ post(previewMessage('hover', { id }))
386
+ refreshOverlay()
387
+ }
388
+ }
389
+
390
+ function onPointerLeave() {
391
+ if (hoverId.value !== null) {
392
+ hoverId.value = null
393
+ post(previewMessage('hover', { id: null }))
394
+ refreshOverlay()
395
+ }
396
+ }
397
+
398
+ // Keyboard: Enter on a focused block selects it, so the preview works without a mouse too. Esc
399
+ // selects the block around the selected inner block.
400
+ function onKeydown(event: KeyboardEvent) {
401
+ if (event.key === 'Enter' || event.key === ' ') {
402
+ // Enter on a focused link must not follow it. The preview has nothing to type into.
403
+ event.preventDefault()
404
+ const id = blockIdAt(event.target)
405
+ if (id) {
406
+ selectBlock(id)
407
+ }
408
+ } else if (event.key === 'Escape' && selectedId.value) {
409
+ const parent = parents.value.get(selectedId.value)
410
+ if (parent) {
411
+ event.preventDefault()
412
+ selectBlock(parent)
413
+ }
414
+ }
415
+ }
416
+
417
+ function onBlockError(id: string, message: string) {
418
+ post(previewMessage('error', { id, message }))
419
+ }
420
+
421
+ let observer: ResizeObserver | undefined
422
+ let mutations: MutationObserver | undefined
423
+
424
+ onMounted(() => {
425
+ window.addEventListener('message', onMessage)
426
+ window.addEventListener('scroll', refreshOverlay, { passive: true })
427
+ window.addEventListener('resize', refreshOverlay)
428
+ document.addEventListener('click', onClick, true)
429
+ document.addEventListener('pointermove', onPointerMove, { passive: true })
430
+ document.addEventListener('pointerleave', onPointerLeave)
431
+ document.addEventListener('keydown', onKeydown, true)
432
+ document.addEventListener('auxclick', stopEvent, true)
433
+ document.addEventListener('dragstart', stopEvent, true)
434
+ document.addEventListener('selectstart', stopEvent, true)
435
+ observer = new ResizeObserver(refreshOverlay)
436
+ observer.observe(document.body)
437
+ // Images, fonts and lazy components change the layout after the render.
438
+ mutations = new MutationObserver(refreshOverlay)
439
+ mutations.observe(document.body, {
440
+ childList: true,
441
+ subtree: true,
442
+ attributes: true,
443
+ attributeFilter: ['class', 'style', 'open'],
444
+ })
445
+ post(previewMessage('ready', {}))
446
+ })
447
+
448
+ onBeforeUnmount(() => {
449
+ window.removeEventListener('message', onMessage)
450
+ window.removeEventListener('scroll', refreshOverlay)
451
+ window.removeEventListener('resize', refreshOverlay)
452
+ document.removeEventListener('click', onClick, true)
453
+ document.removeEventListener('pointermove', onPointerMove)
454
+ document.removeEventListener('pointerleave', onPointerLeave)
455
+ document.removeEventListener('keydown', onKeydown, true)
456
+ document.removeEventListener('auxclick', stopEvent, true)
457
+ document.removeEventListener('dragstart', stopEvent, true)
458
+ document.removeEventListener('selectstart', stopEvent, true)
459
+ observer?.disconnect()
460
+ mutations?.disconnect()
461
+ cancelAnimationFrame(frame)
462
+ })
463
+
464
+ const hoverLabel = computed(
465
+ () => (hoverId.value ? labels.value.get(hoverId.value) : '') ?? ''
466
+ )
467
+
468
+ function boxStyle(
469
+ rect: DOMRect | null,
470
+ kind: 'selected' | 'hover'
471
+ ): CSSProperties {
472
+ if (!rect) {
473
+ return { display: 'none' }
474
+ }
475
+ // The box has the size of the block, and the ring is an inset box shadow. So a full-width block
476
+ // (a hero, a page section) gives no horizontal scroll (a border on a larger box did), and the
477
+ // four sides of the ring stay on screen.
478
+ const color = 'var(--ui-primary, #6366f1)'
479
+ return {
480
+ position: 'absolute',
481
+ left: `${rect.left}px`,
482
+ top: `${rect.top}px`,
483
+ width: `${rect.width}px`,
484
+ height: `${rect.height}px`,
485
+ pointerEvents: 'none',
486
+ zIndex: '2147483000',
487
+ borderRadius: '4px',
488
+ boxShadow:
489
+ kind === 'selected'
490
+ ? `inset 0 0 0 2px ${color}`
491
+ : `inset 0 0 0 1px color-mix(in oklab, ${color} 70%, transparent)`,
492
+ background:
493
+ kind === 'selected'
494
+ ? `color-mix(in oklab, ${color} 8%, transparent)`
495
+ : 'transparent',
496
+ }
497
+ }
498
+
499
+ const chipStyle: CSSProperties = {
500
+ position: 'absolute',
501
+ top: '-22px',
502
+ left: '0',
503
+ padding: '1px 8px',
504
+ borderRadius: '6px 6px 0 0',
505
+ fontSize: '11px',
506
+ lineHeight: '20px',
507
+ fontFamily: 'system-ui, sans-serif',
508
+ background: 'var(--ui-primary, #6366f1)',
509
+ color: 'var(--ui-bg, #fff)',
510
+ whiteSpace: 'nowrap',
511
+ }
512
+ </script>
513
+
514
+ <template>
515
+ <div>
516
+ <PlutoContentPreview :frontmatter="frontmatter">
517
+ <PlutoPreviewBlock
518
+ v-for="block in blocks"
519
+ :id="block.id"
520
+ :key="block.id"
521
+ :body="block.body"
522
+ :label="block.label"
523
+ :error="block.error"
524
+ @error="onBlockError"
525
+ />
526
+ </PlutoContentPreview>
527
+
528
+ <!-- The outlines use inline styles only, so they do not depend on the site CSS. -->
529
+ <div
530
+ aria-hidden="true"
531
+ style="
532
+ position: absolute;
533
+ inset: 0;
534
+ pointer-events: none;
535
+ overflow: visible;
536
+ height: 0;
537
+ "
538
+ >
539
+ <div :style="boxStyle(overlay.hover, 'hover')">
540
+ <span
541
+ v-if="overlay.hover && hoverLabel"
542
+ :style="{ ...chipStyle, opacity: '0.85' }"
543
+ >{{ hoverLabel }}</span
544
+ >
545
+ </div>
546
+ <div :style="boxStyle(overlay.selected, 'selected')">
547
+ <span
548
+ v-if="overlay.selected && selectedId && labels.get(selectedId)"
549
+ :style="chipStyle"
550
+ >{{ labels.get(selectedId) }}</span
551
+ >
552
+ </div>
553
+ </div>
554
+ </div>
555
+ </template>
@@ -0,0 +1,50 @@
1
+ import type { PlutoDocsTypeOptions } from '../../modules/pluto-docs/options'
2
+ import { defineAsyncComponent } from 'vue'
3
+
4
+ // The field widget loads on demand and stays out of the entry chunk. Core shows a
5
+ // skeleton while the field chunk loads.
6
+ const PlutoFieldMdc = defineAsyncComponent(
7
+ () => import('../components/Pluto/Docs/PlutoFieldMdc.vue')
8
+ )
9
+
10
+ // Registers the types, the capabilities and the `mdc` field widget. The types set
11
+ // `adminBar`, so core shows "New page" and "Edit page" in the admin bar. The module
12
+ // sets the config only when the CMS is on. Without it, the layer adds nothing to the admin.
13
+ export default defineNuxtPlugin(() => {
14
+ const config = useRuntimeConfig().public.plutoDocs as unknown as
15
+ | {
16
+ types: PlutoDocsTypeOptions[]
17
+ render?: string
18
+ }
19
+ | undefined
20
+ if (!config) {
21
+ return
22
+ }
23
+
24
+ const types = config.types.map((entry) =>
25
+ createDocsType({
26
+ ...entry,
27
+ liveOnSave: config.render === 'hybrid' || config.render === 'runtime',
28
+ })
29
+ )
30
+
31
+ definePlutoExtension({
32
+ id: 'docs',
33
+ contentTypes: types.map((type) => ({ id: type.name, type })),
34
+ capabilities: [
35
+ {
36
+ id: 'docs-write',
37
+ key: DOCS_CAPABILITIES.write,
38
+ label: 'docs.capabilities.write',
39
+ },
40
+ {
41
+ id: 'docs-delete',
42
+ key: DOCS_CAPABILITIES.delete,
43
+ label: 'docs.capabilities.delete',
44
+ },
45
+ ],
46
+ contentFieldWidgets: [
47
+ { id: 'mdc', widget: 'mdc', component: PlutoFieldMdc },
48
+ ],
49
+ })
50
+ })