@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.
Files changed (65) hide show
  1. package/.gitattributes +1 -0
  2. package/.prettierignore +5 -0
  3. package/CHANGELOG.md +186 -0
  4. package/FEATURES.md +34 -0
  5. package/README.md +40 -2
  6. package/app/assets/css/pluto-layer.css +40 -0
  7. package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +165 -0
  8. package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
  9. package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +161 -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 +86 -0
  14. package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +232 -0
  15. package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +269 -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 +459 -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/blocks.ts +217 -0
  31. package/modules/pluto-docs/index.ts +230 -0
  32. package/modules/pluto-docs/manifest.ts +159 -0
  33. package/modules/pluto-docs/options.ts +258 -0
  34. package/nuxt.config.ts +36 -0
  35. package/package.json +56 -4
  36. package/server/handlers/navigation.get.ts +7 -0
  37. package/server/handlers/page.get.ts +7 -0
  38. package/server/handlers/surround.get.ts +7 -0
  39. package/server/plugins/pluto-docs.ts +25 -0
  40. package/server/utils/pluto-docs-runtime.ts +504 -0
  41. package/shared/types/docs.d.ts +40 -0
  42. package/shared/types/pluto-blocks.d.ts +10 -0
  43. package/shared/types/pluto-manifest.d.ts +6 -0
  44. package/shared/utils/blocks/component-meta.ts +285 -0
  45. package/shared/utils/blocks/define.ts +15 -0
  46. package/shared/utils/blocks/editor.ts +357 -0
  47. package/shared/utils/blocks/history.ts +94 -0
  48. package/shared/utils/blocks/mdc-props.ts +321 -0
  49. package/shared/utils/blocks/operations.ts +692 -0
  50. package/shared/utils/blocks/parse.ts +444 -0
  51. package/shared/utils/blocks/presets.ts +384 -0
  52. package/shared/utils/blocks/preview-marks.ts +98 -0
  53. package/shared/utils/blocks/preview-protocol.ts +195 -0
  54. package/shared/utils/blocks/registry.ts +295 -0
  55. package/shared/utils/blocks/serialize.ts +107 -0
  56. package/shared/utils/blocks/tree.ts +85 -0
  57. package/shared/utils/blocks/types.ts +195 -0
  58. package/shared/utils/content-route.ts +40 -0
  59. package/shared/utils/docs-type.ts +290 -0
  60. package/shared/utils/mdc-snippets.ts +114 -0
  61. package/shared/utils/navigation-config.ts +19 -0
  62. package/shared/utils/runtime-content.ts +403 -0
  63. package/tailwind.lint.css +10 -0
  64. package/tsconfig.json +18 -0
  65. package/vitest.config.ts +7 -0
@@ -0,0 +1,80 @@
1
+ <script setup lang="ts">
2
+ // One block in the preview page, inside its own error boundary. A component
3
+ // that throws (it needs data that the preview has no, for example) shows a
4
+ // small card in place of the block, and the other blocks keep working.
5
+ // `preview: false` in the block definition shows the same card, with a note.
6
+ import type { MDCRoot } from '@nuxtjs/mdc'
7
+
8
+ const props = defineProps<{
9
+ id: string
10
+ /** The parsed body, or `undefined` for a block that the preview does not render. */
11
+ body?: MDCRoot
12
+ /** The name of the block, for the card. */
13
+ label: string
14
+ /** A parse error of this block. */
15
+ error?: string
16
+ }>()
17
+
18
+ const emit = defineEmits<{
19
+ error: [id: string, message: string]
20
+ }>()
21
+
22
+ const { t } = usePlutoI18n()
23
+
24
+ const failed = ref<string | null>(null)
25
+ const value = computed(() => (props.body ? { body: props.body } : undefined))
26
+
27
+ // The render options of the site page (`usePlutoPreviewRender` in the site's PlutoContentPreview).
28
+ const renderOptions = inject(plutoPreviewRenderKey, {})
29
+ const prose = computed(() => toValue(renderOptions).prose ?? true)
30
+ const components = computed(() => toValue(renderOptions).components ?? {})
31
+
32
+ // A new body gets a new chance to render.
33
+ watch(
34
+ () => props.body,
35
+ () => {
36
+ failed.value = null
37
+ }
38
+ )
39
+
40
+ onErrorCaptured((error) => {
41
+ failed.value = error instanceof Error ? error.message : String(error)
42
+ emit('error', props.id, failed.value)
43
+ // Stop the error here, so the other blocks stay on screen.
44
+ return false
45
+ })
46
+
47
+ const note = computed(() => {
48
+ if (props.error) {
49
+ return t('docs.preview.parseError')
50
+ }
51
+ if (failed.value) {
52
+ return t('docs.preview.blockError')
53
+ }
54
+ return t('docs.preview.notShown')
55
+ })
56
+ </script>
57
+
58
+ <template>
59
+ <ContentRenderer
60
+ v-if="value && !failed && !error"
61
+ :value="value"
62
+ :tag="false as unknown as string"
63
+ :prose="prose"
64
+ :components="components"
65
+ />
66
+
67
+ <div
68
+ v-else
69
+ :data-pluto-block="id"
70
+ class="my-4 flex items-center gap-3 rounded-lg border border-dashed border-default bg-elevated/50 px-4 py-3 text-sm text-muted"
71
+ >
72
+ <UIcon name="i-lucide-eye-off" class="size-5 shrink-0" />
73
+ <div class="min-w-0">
74
+ <p class="font-medium text-default">
75
+ {{ label }}
76
+ </p>
77
+ <p>{{ note }}</p>
78
+ </div>
79
+ </div>
80
+ </template>
@@ -0,0 +1,117 @@
1
+ <script setup lang="ts">
2
+ // The `mdc` widget of the docs layer. It edits the Markdown (MDC) body
3
+ // of a page in one of two modes:
4
+ //
5
+ // - Visual: the block editor. A live preview in the middle, the block list at
6
+ // the left and the controls of the selected block at the right. It needs the
7
+ // 'canvas' form layout (the docs type uses it) and loads on demand, so
8
+ // Tiptap and the remark parser stay out of the other admin pages.
9
+ // - Markdown: `PlutoMarkdownCodeEditor` of core (CodeMirror: the author sees all the MDC,
10
+ // with the format shown as they type, quick formats and an Insert menu). In a canvas form the live preview shows at
11
+ // its side, and a click on a block in the preview goes to that block in the
12
+ // Markdown (PlutoBlockMarkdownSplit, loaded on demand too).
13
+ //
14
+ // The first mode is the `editor` option of the field (`visual` or `source`). A
15
+ // cookie keeps the last choice of the author. The model value is the Markdown
16
+ // text, as it is. The widget contract: props `field`, `modelValue`, `disabled`,
17
+ // and the `update:modelValue` event. The layer registers it as the widget `mdc`.
18
+ import { defineAsyncComponent } from 'vue'
19
+ import PlutoMdcInsertMenu from './PlutoMdcInsertMenu.vue'
20
+
21
+ const props = defineProps<{
22
+ field: PlutoTextField
23
+ modelValue: unknown
24
+ disabled?: boolean
25
+ }>()
26
+
27
+ const emit = defineEmits<{
28
+ 'update:modelValue': [value: string]
29
+ }>()
30
+
31
+ // The block editor and the Markdown split load on demand. The skeleton shows while they load.
32
+ const loadingComponent = () =>
33
+ h('div', { class: 'flex h-full flex-col gap-3 p-6' }, [
34
+ h(resolveComponent('USkeleton'), { class: 'h-8 w-1/3' }),
35
+ h(resolveComponent('USkeleton'), { class: 'h-64 w-full' }),
36
+ ])
37
+ const PlutoBlockEditor = defineAsyncComponent({
38
+ loader: () => import('./Blocks/PlutoBlockEditor.vue'),
39
+ loadingComponent,
40
+ delay: 120,
41
+ })
42
+ const PlutoBlockMarkdownSplit = defineAsyncComponent({
43
+ loader: () => import('./Blocks/PlutoBlockMarkdownSplit.vue'),
44
+ loadingComponent,
45
+ delay: 120,
46
+ })
47
+
48
+ const { t } = usePlutoI18n()
49
+ const form = usePlutoContentForm()
50
+
51
+ const value = computed(() =>
52
+ typeof props.modelValue === 'string' ? props.modelValue : ''
53
+ )
54
+
55
+ // Visual needs the canvas layout: the panels live in the form.
56
+ const canVisual = computed(() => Boolean(form?.canvas.value))
57
+
58
+ type Mode = 'visual' | 'markdown'
59
+ const saved = useCookie<Mode | null>('pluto-mdc-editor-mode', {
60
+ default: () => null,
61
+ maxAge: 60 * 60 * 24 * 365,
62
+ sameSite: 'lax',
63
+ })
64
+ const mode = ref<Mode>(
65
+ canVisual.value
66
+ ? (saved.value ?? (props.field.editor === 'source' ? 'markdown' : 'visual'))
67
+ : 'markdown'
68
+ )
69
+
70
+ function setMode(next: Mode) {
71
+ mode.value = next
72
+ saved.value = next
73
+ }
74
+ </script>
75
+
76
+ <template>
77
+ <PlutoBlockEditor
78
+ v-if="mode === 'visual' && canVisual"
79
+ :model-value="value"
80
+ :disabled="disabled"
81
+ :mode="mode"
82
+ @update:model-value="emit('update:modelValue', $event)"
83
+ @update:mode="setMode"
84
+ />
85
+
86
+ <PlutoBlockMarkdownSplit
87
+ v-else-if="canVisual"
88
+ :model-value="value"
89
+ :disabled="disabled"
90
+ :mode="mode"
91
+ :placeholder="field.placeholder ?? t('docs.mdc.placeholder')"
92
+ :label="field.label"
93
+ @update:model-value="emit('update:modelValue', $event)"
94
+ @update:mode="setMode"
95
+ />
96
+
97
+ <!-- A form without the canvas layout: the Markdown only, with no preview and no Visual mode. -->
98
+ <UFormField
99
+ v-else
100
+ :label="field.label"
101
+ :description="field.description"
102
+ :required="field.required"
103
+ >
104
+ <!-- The core editor (CodeMirror) loads on demand, so the Visual mode does not load it. -->
105
+ <LazyPlutoMarkdownCodeEditor
106
+ :model-value="value"
107
+ :disabled="disabled"
108
+ :placeholder="field.placeholder ?? t('docs.mdc.placeholder')"
109
+ :aria-label="field.label"
110
+ @update:model-value="emit('update:modelValue', $event)"
111
+ >
112
+ <template #toolbar="{ insert }">
113
+ <PlutoMdcInsertMenu :insert="insert" :disabled="disabled" />
114
+ </template>
115
+ </LazyPlutoMarkdownCodeEditor>
116
+ </UFormField>
117
+ </template>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ // The Insert menu of the Markdown editor: one item for each MDC snippet. The
3
+ // `insert` function is the one of the `toolbar` slot of PlutoMarkdownSource.
4
+ const props = defineProps<{
5
+ insert: (text: string, options?: { block?: boolean }) => void
6
+ disabled?: boolean
7
+ }>()
8
+
9
+ const { t } = usePlutoI18n()
10
+
11
+ const items = computed(() =>
12
+ MDC_SNIPPETS.map((snippet) => ({
13
+ label: t(snippet.label),
14
+ icon: snippet.icon,
15
+ onSelect: () => props.insert(snippet.text, { block: snippet.block }),
16
+ }))
17
+ )
18
+ </script>
19
+
20
+ <template>
21
+ <UDropdownMenu
22
+ :items="items"
23
+ :content="{ align: 'start' }"
24
+ :disabled="disabled"
25
+ >
26
+ <UButton
27
+ :label="t('docs.mdc.insert')"
28
+ :disabled="disabled"
29
+ color="neutral"
30
+ variant="ghost"
31
+ size="xs"
32
+ icon="i-lucide-plus"
33
+ trailing-icon="i-lucide-chevron-down"
34
+ />
35
+ </UDropdownMenu>
36
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ // The frame around the blocks in the preview page (`/_pluto/preview`). The
3
+ // default is a generic docs-like page: a header with the title and the
4
+ // description, and the blocks in the body. A site that wants the real layout
5
+ // of its pages overrides this component. Put a component with the same name in
6
+ // the app (`app/components/PlutoContentPreview.vue`) and render the default
7
+ // slot where the blocks go. The slot must hold the blocks directly: no
8
+ // wrapper element, because the prose styles read the first and last child.
9
+ //
10
+ // `frontmatter` is the front matter of the page being edited: the title, the
11
+ // description and the other fields of the form. The preview never fetches.
12
+ defineProps<{
13
+ frontmatter: Record<string, unknown>
14
+ }>()
15
+ </script>
16
+
17
+ <template>
18
+ <UContainer>
19
+ <UPage>
20
+ <UPageHeader
21
+ v-if="typeof frontmatter.title === 'string' && frontmatter.title"
22
+ :title="frontmatter.title"
23
+ :description="
24
+ typeof frontmatter.description === 'string'
25
+ ? frontmatter.description
26
+ : undefined
27
+ "
28
+ />
29
+
30
+ <UPageBody>
31
+ <slot />
32
+ </UPageBody>
33
+ </UPage>
34
+ </UContainer>
35
+ </template>