@plutocms/docs 0.15.0 → 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.
@@ -1,12 +1,22 @@
1
+ import type { Nuxt } from '@nuxt/schema'
1
2
  import type { PlutoDocsOptions } from './options'
2
3
  import { join } from 'node:path'
3
4
  import {
5
+ addImports,
4
6
  addServerHandler,
7
+ addVitePlugin,
5
8
  createResolver,
6
9
  defineNuxtModule,
7
10
  useLogger,
8
11
  } from '@nuxt/kit'
9
12
  import { PREVIEW_PATH } from '../../shared/utils/blocks/preview-protocol'
13
+ import {
14
+ BLOCK_MACRO,
15
+ BlockCatalog,
16
+ blocksModuleSource,
17
+ CONTENT_COMPONENTS_DIR,
18
+ MACRO_KEY,
19
+ } from './blocks'
10
20
  import { buildContentManifest, manifestModuleSource } from './manifest'
11
21
  import { resolvePlutoDocsOptions } from './options'
12
22
 
@@ -33,7 +43,7 @@ export default defineNuxtModule<PlutoDocsOptions>({
33
43
  nuxt.options as { plutoGithub?: { enabled?: boolean | 'auto' } }
34
44
  ).plutoGithub
35
45
 
36
- const { enabled, render, runtimeMaxAge, types, errors } =
46
+ const { enabled, render, runtimeMaxAge, types, blocks, errors } =
37
47
  resolvePlutoDocsOptions(options, {
38
48
  cmsEnabled: cms?.enabled !== false,
39
49
  })
@@ -41,6 +51,17 @@ export default defineNuxtModule<PlutoDocsOptions>({
41
51
  for (const error of errors) {
42
52
  logger.error(error)
43
53
  }
54
+ // `definePlutoBlock(...)` in a component file. A typed no-op at run time: the build reads the call.
55
+ addImports({
56
+ name: BLOCK_MACRO,
57
+ from: createResolver(import.meta.url).resolve(
58
+ '../../shared/utils/blocks/define'
59
+ ),
60
+ })
61
+
62
+ // The editor imports the virtual module even when the layer is off, so it always exists.
63
+ setupBlockDiscovery(nuxt, logger, enabled && blocks.discover)
64
+
44
65
  if (!enabled) {
45
66
  return
46
67
  }
@@ -121,3 +142,89 @@ export default defineNuxtModule<PlutoDocsOptions>({
121
142
  }
122
143
  },
123
144
  })
145
+
146
+ /**
147
+ * The component layer of the block editor. The virtual module `#pluto-docs/blocks` holds the blocks
148
+ * of the `components/content/` folders. Only the two lazy editor roots import it, so it stays out of
149
+ * the public pages. The server build gets an empty object: the editor never renders on the server.
150
+ * The cost comes when the editor first loads, not at start-up. A change to a component file makes
151
+ * the module load again, and the catalog parses only the file that changed.
152
+ */
153
+ function setupBlockDiscovery(
154
+ nuxt: Nuxt,
155
+ logger: ReturnType<typeof useLogger>,
156
+ discover: boolean
157
+ ) {
158
+ const id = '#pluto-docs/blocks'
159
+ // The `\0` prefix marks a virtual id. No `#` in it: a browser URL ends at the `#`.
160
+ const resolved = '\0pluto-docs-blocks'
161
+ // The app first, then its layers (the order of priority).
162
+ const dirs = () =>
163
+ nuxt.options._layers.map((layer) =>
164
+ join(layer.config.srcDir, CONTENT_COMPONENTS_DIR)
165
+ )
166
+
167
+ // The cache of the parser names a file from the text after the last slash, so give it slashes.
168
+ const slashes = (path: string) => path.replaceAll('\\', '/')
169
+
170
+ type Reader = typeof import('nuxt-component-meta/parser').getComponentMeta
171
+ let getComponentMeta: Reader | undefined
172
+ const catalog = new BlockCatalog(
173
+ dirs,
174
+ (file) =>
175
+ getComponentMeta!(slashes(file), {
176
+ rootDir: nuxt.options.rootDir,
177
+ cache: true,
178
+ cacheDir: join(nuxt.options.buildDir, 'cache/pluto-blocks'),
179
+ extendMetaFunctions: [
180
+ { name: BLOCK_MACRO, transform: (arg) => ({ [MACRO_KEY]: arg }) },
181
+ ],
182
+ }) as never,
183
+ logger
184
+ )
185
+
186
+ const isBlockFile = (file: string) =>
187
+ file.endsWith('.vue') &&
188
+ dirs().some((dir) => slashes(file).startsWith(slashes(dir)))
189
+
190
+ addVitePlugin({
191
+ name: 'pluto-docs:blocks',
192
+ enforce: 'pre',
193
+ resolveId(source) {
194
+ return source === id ? resolved : undefined
195
+ },
196
+ async load(loaded, options) {
197
+ if (loaded !== resolved) {
198
+ return undefined
199
+ }
200
+ if (options?.ssr || !discover) {
201
+ return blocksModuleSource({})
202
+ }
203
+ // The parser loads here, not at start-up, because it brings the TypeScript compiler.
204
+ getComponentMeta ||= (await import('nuxt-component-meta/parser'))
205
+ .getComponentMeta
206
+ return blocksModuleSource(catalog.collect())
207
+ },
208
+ configureServer(server) {
209
+ const invalidate = (file: string) => {
210
+ if (!isBlockFile(file)) {
211
+ return
212
+ }
213
+ const module = server.moduleGraph.getModuleById(resolved)
214
+ if (module) {
215
+ server.moduleGraph.invalidateModule(module)
216
+ server.ws.send({ type: 'full-reload' })
217
+ }
218
+ }
219
+ server.watcher.on('add', invalidate)
220
+ server.watcher.on('unlink', invalidate)
221
+ },
222
+ handleHotUpdate({ file, server }) {
223
+ if (!isBlockFile(file)) {
224
+ return undefined
225
+ }
226
+ const module = server.moduleGraph.getModuleById(resolved)
227
+ return module ? [module] : undefined
228
+ },
229
+ })
230
+ }
@@ -41,6 +41,16 @@ export interface PlutoDocsOptions {
41
41
  * It is short, so a save shows soon. `0` turns the cache header into `no-store`.
42
42
  */
43
43
  runtimeMaxAge?: number
44
+ /** How the block editor finds the block components of the site. */
45
+ blocks?: PlutoDocsBlocksOptions
46
+ }
47
+
48
+ export interface PlutoDocsBlocksOptions {
49
+ /**
50
+ * Read `components/content/` of the app and of its layers. A component that calls
51
+ * `definePlutoBlock(...)` becomes a block. Default `true`. `false` turns the search off.
52
+ */
53
+ discover?: boolean
44
54
  }
45
55
 
46
56
  export interface PlutoDocsResolveContext {
@@ -52,6 +62,7 @@ export interface PlutoDocsResolveResult {
52
62
  enabled: boolean
53
63
  render: PlutoDocsRender
54
64
  runtimeMaxAge: number
65
+ blocks: { discover: boolean }
55
66
  types: PlutoDocsTypeOptions[]
56
67
  errors: string[]
57
68
  }
@@ -110,6 +121,7 @@ export function resolvePlutoDocsOptions(
110
121
  enabled: false,
111
122
  render: 'build',
112
123
  runtimeMaxAge: DEFAULT_RUNTIME_MAX_AGE,
124
+ blocks: { discover: false },
113
125
  types: [],
114
126
  errors: [CMS_OFF_ERROR],
115
127
  }
@@ -143,6 +155,21 @@ export function resolvePlutoDocsOptions(
143
155
  }
144
156
  }
145
157
 
158
+ let discover = true
159
+ if (options.blocks !== undefined) {
160
+ if (!isRecord(options.blocks)) {
161
+ errors.push('plutoDocs.blocks must be an object.')
162
+ } else if (options.blocks.discover !== undefined) {
163
+ if (typeof options.blocks.discover === 'boolean') {
164
+ discover = options.blocks.discover
165
+ } else {
166
+ errors.push(
167
+ 'plutoDocs.blocks.discover must be true or false. Using true.'
168
+ )
169
+ }
170
+ }
171
+ }
172
+
146
173
  const types: PlutoDocsTypeOptions[] = []
147
174
  const names = new Set<string>()
148
175
  const entries: unknown[] = Array.isArray(options.types) ? options.types : []
@@ -220,5 +247,12 @@ export function resolvePlutoDocsOptions(
220
247
  })
221
248
  }
222
249
 
223
- return { enabled: true, render, runtimeMaxAge, types, errors }
250
+ return {
251
+ enabled: true,
252
+ render,
253
+ runtimeMaxAge,
254
+ blocks: { discover },
255
+ types,
256
+ errors,
257
+ }
224
258
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutocms/docs",
3
3
  "type": "module",
4
- "version": "0.15.0",
4
+ "version": "0.16.0",
5
5
  "description": "Docs layer for Pluto: edit the Markdown pages of a Nuxt Content site in the Pluto admin",
6
6
  "author": "Victor Ribeiro",
7
7
  "license": "MIT",
@@ -25,8 +25,9 @@
25
25
  "dependencies": {
26
26
  "@nuxt/content": "^3.16.2",
27
27
  "@nuxtjs/mdc": "^0.23.2",
28
- "@plutocms/core": ">=0.39.0 <1.0.0",
28
+ "@plutocms/core": ">=0.40.0 <1.0.0",
29
29
  "@types/sortablejs": "^1.15.9",
30
+ "nuxt-component-meta": "0.18.0",
30
31
  "reka-ui": "^2.11.0",
31
32
  "remark-gfm": "4.0.1",
32
33
  "remark-mdc": "3.11.1",
@@ -0,0 +1,10 @@
1
+ // The component layer of the block editor: the blocks of the `components/content/` files that call
2
+ // `definePlutoBlock`. The module makes it as a Vite virtual module. See modules/pluto-docs/blocks.ts.
3
+ // This file has no top-level import, so the declaration is an ambient module.
4
+ declare module '#pluto-docs/blocks' {
5
+ const blocks: Record<
6
+ string,
7
+ import('../utils/blocks/types').PlutoBlockOverride
8
+ >
9
+ export default blocks
10
+ }
@@ -0,0 +1,285 @@
1
+ // Make a block definition from the meta of a Vue component and the optional
2
+ // `definePlutoBlock({...})` call in its file. Pure: the build step (a later
3
+ // change) parses the files with `nuxt-component-meta` and passes the result in.
4
+ // The result is the "component layer" of one component. Opt-in: the build step
5
+ // calls this only for a component that has the macro.
6
+
7
+ import type {
8
+ PlutoBlockPatch,
9
+ PlutoBlockPropDefinition,
10
+ PlutoBlockPropPatch,
11
+ PlutoBlockSlotDefinition,
12
+ } from './types'
13
+ import { BLOCK_COLORS, BLOCK_PRESETS } from './presets'
14
+ import { blockKey, mergeBlockPatch } from './registry'
15
+
16
+ // The parts of the output of `getComponentMeta` that this file reads. The real output has more.
17
+ export interface PlutoMetaSchema {
18
+ kind?: string
19
+ type?: string
20
+ /** For `enum`: the members as text, by index. For `object`: the properties by name. */
21
+ schema?: Record<string, unknown> | string
22
+ }
23
+
24
+ export interface PlutoMetaProp {
25
+ name: string
26
+ /** The type as text, for example `"slow" | "fast" | undefined`. */
27
+ type: string
28
+ schema?: PlutoMetaSchema | string
29
+ /** The default as source text: `300`, `"normal"`, `false`. */
30
+ default?: string
31
+ /** The JSDoc text of the prop. */
32
+ description?: string
33
+ required?: boolean
34
+ }
35
+
36
+ export interface PlutoMetaSlot {
37
+ name: string
38
+ type?: string
39
+ schema?: PlutoMetaSchema | string
40
+ description?: string
41
+ }
42
+
43
+ export interface PlutoComponentMeta {
44
+ props?: PlutoMetaProp[]
45
+ slots?: PlutoMetaSlot[]
46
+ }
47
+
48
+ /** The block key of a component: `PlutoHero` gives `pluto-hero`, `UPageCTA` gives `u-page-c-t-a`. */
49
+ export function componentBlockName(pascalName: string): string {
50
+ return blockKey(pascalName)
51
+ }
52
+
53
+ // --- Type to widget ---------------------------------------------------------------
54
+
55
+ /** Split a union at its top-level bars, so a bar inside quotes or brackets stays. */
56
+ function splitUnion(type: string): string[] {
57
+ const parts: string[] = []
58
+ let depth = 0
59
+ let quote = ''
60
+ let current = ''
61
+ for (const char of type) {
62
+ if (quote) {
63
+ if (char === quote) {
64
+ quote = ''
65
+ }
66
+ } else if (char === '"' || char === "'" || char === '`') {
67
+ quote = char
68
+ } else if ('{[(<'.includes(char)) {
69
+ depth++
70
+ } else if ('}])>'.includes(char)) {
71
+ depth--
72
+ } else if (char === '|' && depth === 0) {
73
+ parts.push(current.trim())
74
+ current = ''
75
+ continue
76
+ }
77
+ current += char
78
+ }
79
+ parts.push(current.trim())
80
+ return parts.filter(Boolean)
81
+ }
82
+
83
+ /** The members of the type without `undefined` and `null`. */
84
+ function typeMembers(prop: PlutoMetaProp): string[] {
85
+ const schema = prop.schema
86
+ const members =
87
+ typeof schema === 'object' &&
88
+ schema.kind === 'enum' &&
89
+ typeof schema.schema === 'object'
90
+ ? Object.values(schema.schema).map(String)
91
+ : splitUnion(prop.type)
92
+ return members.filter((member) => member !== 'undefined' && member !== 'null')
93
+ }
94
+
95
+ const STRING_LITERAL = /^(["'`])(.*)\1$/
96
+
97
+ function stringLiteral(member: string): string | undefined {
98
+ return STRING_LITERAL.exec(member)?.[2]
99
+ }
100
+
101
+ function isIconName(name: string): boolean {
102
+ return name === 'icon' || name.endsWith('Icon')
103
+ }
104
+
105
+ /** The widget for a prop, or `undefined` when no widget fits. */
106
+ function widgetFor(
107
+ name: string,
108
+ members: string[]
109
+ ): Pick<PlutoBlockPropDefinition, 'type' | 'options'> | undefined {
110
+ if (members.length === 0) {
111
+ return undefined
112
+ }
113
+ const literals = members.map(stringLiteral)
114
+ if (literals.every((value) => value !== undefined)) {
115
+ const options = (literals as string[]).map((value) => ({ value }))
116
+ return (literals as string[]).every((value) => BLOCK_COLORS.includes(value))
117
+ ? { type: 'color', options }
118
+ : { type: 'select', options }
119
+ }
120
+ const kinds = new Set(
121
+ members.map((member) => stringLiteral(member) ?? member)
122
+ )
123
+ const isBoolean = members.every((member) =>
124
+ ['boolean', 'true', 'false'].includes(member)
125
+ )
126
+ if (isBoolean) {
127
+ return { type: 'boolean' }
128
+ }
129
+ if (members.length === 1 && members[0] === 'number') {
130
+ return { type: 'number' }
131
+ }
132
+ const textLike = members.every(
133
+ (member) =>
134
+ stringLiteral(member) !== undefined ||
135
+ member === 'string' ||
136
+ member === 'number'
137
+ )
138
+ if (textLike && members.includes('string')) {
139
+ return {
140
+ type: isIconName(name) ? 'icon' : linkName(name) ? 'link' : 'string',
141
+ }
142
+ }
143
+ if (
144
+ linkName(name) &&
145
+ (kinds.has('string') || kinds.has('RouteLocationRaw'))
146
+ ) {
147
+ return { type: 'link' }
148
+ }
149
+ return undefined
150
+ }
151
+
152
+ function linkName(name: string): boolean {
153
+ return name === 'to' || name === 'href'
154
+ }
155
+
156
+ /** A simple default from source text: a number, a boolean or a string. Anything else gives `undefined`. */
157
+ export function parseDefault(
158
+ source: string | undefined
159
+ ): string | number | boolean | undefined {
160
+ if (source === undefined) {
161
+ return undefined
162
+ }
163
+ const text = source.trim()
164
+ if (text === 'true') {
165
+ return true
166
+ }
167
+ if (text === 'false') {
168
+ return false
169
+ }
170
+ if (/^-?\d+(?:\.\d+)?$/.test(text)) {
171
+ return Number(text)
172
+ }
173
+ const literal = STRING_LITERAL.exec(text)
174
+ return literal && !literal[2]!.includes(literal[1]!) ? literal[2] : undefined
175
+ }
176
+
177
+ // --- Shared labels ----------------------------------------------------------------
178
+
179
+ function sharedKeys(
180
+ pick: (block: (typeof BLOCK_PRESETS)[string]) => Array<string | undefined>,
181
+ prefix: string
182
+ ): Set<string> {
183
+ const names = new Set<string>()
184
+ for (const preset of Object.values(BLOCK_PRESETS)) {
185
+ for (const label of pick(preset)) {
186
+ if (label?.startsWith(prefix)) {
187
+ names.add(label.slice(prefix.length))
188
+ }
189
+ }
190
+ }
191
+ return names
192
+ }
193
+
194
+ const PROP_PREFIX = 'docs.blocks.props.'
195
+ const SLOT_PREFIX = 'docs.blocks.slots.'
196
+ let sharedProps: Set<string> | undefined
197
+ let sharedSlots: Set<string> | undefined
198
+
199
+ /** The shared label key of a prop (`icon`, `to`, `title`...) when the presets have one. */
200
+ function propLabelKey(name: string): string | undefined {
201
+ sharedProps ??= sharedKeys(
202
+ (preset) => Object.values(preset.props ?? {}).map((prop) => prop.label),
203
+ PROP_PREFIX
204
+ )
205
+ return sharedProps.has(name) ? `${PROP_PREFIX}${name}` : undefined
206
+ }
207
+
208
+ function slotLabelKey(name: string): string | undefined {
209
+ sharedSlots ??= sharedKeys(
210
+ (preset) => Object.values(preset.slots ?? {}).map((slot) => slot.label),
211
+ SLOT_PREFIX
212
+ )
213
+ return sharedSlots.has(name) ? `${SLOT_PREFIX}${name}` : undefined
214
+ }
215
+
216
+ // --- The block --------------------------------------------------------------------
217
+
218
+ function propFromMeta(prop: PlutoMetaProp): PlutoBlockPropPatch | undefined {
219
+ const widget = widgetFor(prop.name, typeMembers(prop))
220
+ if (!widget) {
221
+ return undefined
222
+ }
223
+ const label = propLabelKey(prop.name)
224
+ const fallback = parseDefault(prop.default)
225
+ return {
226
+ ...widget,
227
+ ...(label ? { label } : {}),
228
+ ...(prop.description ? { description: prop.description.trim() } : {}),
229
+ ...(fallback !== undefined ? { default: fallback } : {}),
230
+ }
231
+ }
232
+
233
+ /** `mdc-unwrap` on a `<slot>` shows as the binding `mdcUnwrap` in the meta. */
234
+ function hasUnwrap(slot: PlutoMetaSlot): boolean {
235
+ const schema = slot.schema
236
+ return (
237
+ (typeof schema === 'object' &&
238
+ typeof schema.schema === 'object' &&
239
+ 'mdcUnwrap' in schema.schema) ||
240
+ /\bmdcUnwrap\b/.test(slot.type ?? '')
241
+ )
242
+ }
243
+
244
+ function slotFromMeta(slot: PlutoMetaSlot): PlutoBlockSlotDefinition {
245
+ const label = slotLabelKey(slot.name)
246
+ return {
247
+ ...(label ? { label } : {}),
248
+ ...(hasUnwrap(slot) ? { content: 'inline' as const } : {}),
249
+ }
250
+ }
251
+
252
+ /**
253
+ * The component layer of one component: the props and slots of its meta, joined with the object of
254
+ * its `definePlutoBlock({...})` call. The macro wins, key by key (`false` removes a prop or a slot).
255
+ * `name` is the component name in Pascal case or kebab case. The result has the block key for the
256
+ * `PlutoBlocksConfig` of the component layer.
257
+ *
258
+ * Props that no widget fits (an object, an array, a function, a number literal) are left out. A
259
+ * page that sets one still shows it in Advanced. The macro can give such a prop a widget.
260
+ */
261
+ export function blockFromComponentMeta(
262
+ name: string,
263
+ meta: PlutoComponentMeta,
264
+ macro?: PlutoBlockPatch | false
265
+ ): { name: string; definition: PlutoBlockPatch } {
266
+ const props: Record<string, PlutoBlockPropPatch> = {}
267
+ for (const prop of meta.props ?? []) {
268
+ const definition = propFromMeta(prop)
269
+ if (definition) {
270
+ props[prop.name] = definition
271
+ }
272
+ }
273
+ const slots: Record<string, PlutoBlockSlotDefinition> = {}
274
+ for (const slot of meta.slots ?? []) {
275
+ slots[slot.name] = slotFromMeta(slot)
276
+ }
277
+ const fromMeta: PlutoBlockPatch = {
278
+ ...(Object.keys(props).length ? { props } : {}),
279
+ ...(Object.keys(slots).length ? { slots } : {}),
280
+ }
281
+ return {
282
+ name: componentBlockName(name),
283
+ definition: mergeBlockPatch(fromMeta, macro),
284
+ }
285
+ }
@@ -0,0 +1,15 @@
1
+ import type { PlutoBlockPatch } from './types'
2
+
3
+ /**
4
+ * Describes a block in `app.config.ts`, or in a component file of `components/content/`.
5
+ * The module adds it as an auto-import, so a component file needs no import line.
6
+ *
7
+ * At run time it does nothing: it returns its argument. The build reads the call in the file and
8
+ * makes the block definition from it. See the block-model skill. With a generic,
9
+ * `definePlutoBlock<Props>({...})` checks that the keys of `props` are props of the component.
10
+ */
11
+ export function definePlutoBlock<
12
+ Props extends object = Record<string, unknown>,
13
+ >(definition: PlutoBlockPatch<Props>): PlutoBlockPatch<Props> {
14
+ return definition
15
+ }
@@ -7,7 +7,7 @@ import type {
7
7
  PlutoBlock,
8
8
  PlutoBlockDefinition,
9
9
  PlutoBlockDoc,
10
- PlutoBlocksConfig,
10
+ PlutoBlockSources,
11
11
  PlutoComponentBlock,
12
12
  } from './types'
13
13
  import { blockMarkdown } from './operations'
@@ -63,7 +63,7 @@ function firstText(component: PlutoComponentBlock): string {
63
63
  /** The label, the icon and the excerpt of a block in the outline. */
64
64
  export function describeBlock(
65
65
  block: PlutoBlock,
66
- overrides: PlutoBlocksConfig = {}
66
+ sources: PlutoBlockSources = {}
67
67
  ): PlutoBlockInfo {
68
68
  switch (block.kind) {
69
69
  case 'text': {
@@ -91,7 +91,7 @@ export function describeBlock(
91
91
  excerpt: block.reason === 'table' ? '' : textExcerpt(block.markdown),
92
92
  }
93
93
  case 'component': {
94
- const definition = resolveBlockDefinition(block.name, { overrides })
94
+ const definition = resolveBlockDefinition(block.name, { sources })
95
95
  const title =
96
96
  typeof block.props.title === 'string'
97
97
  ? block.props.title
@@ -176,15 +176,15 @@ export function previewSelectionId(
176
176
  /** The top-level blocks as the preview frame wants them. */
177
177
  export function toPreviewBlocks(
178
178
  blocks: PlutoBlock[],
179
- overrides: PlutoBlocksConfig = {}
179
+ sources: PlutoBlockSources = {}
180
180
  ): PlutoPreviewBlock[] {
181
181
  return blocks.map((block) => {
182
182
  const markdown = renderBlock(block)
183
183
  if (block.kind !== 'component') {
184
184
  return { id: block.id, markdown }
185
185
  }
186
- const definition = resolveBlockDefinition(block.name, { overrides })
187
- const children = previewChildren(block, overrides)
186
+ const definition = resolveBlockDefinition(block.name, { sources })
187
+ const children = previewChildren(block, sources)
188
188
  return {
189
189
  id: block.id,
190
190
  markdown,
@@ -202,16 +202,16 @@ export function toPreviewBlocks(
202
202
  */
203
203
  export function previewChildren(
204
204
  component: PlutoComponentBlock,
205
- overrides: PlutoBlocksConfig = {}
205
+ sources: PlutoBlockSources = {}
206
206
  ): PlutoPreviewChild[] {
207
207
  return flatChildren(component)
208
208
  .filter((child): child is PlutoComponentBlock => child.kind === 'component')
209
209
  .map((child) => {
210
- const children = previewChildren(child, overrides)
210
+ const children = previewChildren(child, sources)
211
211
  return {
212
212
  id: child.id,
213
213
  name: child.name,
214
- label: resolveBlockDefinition(child.name, { overrides, block: child })
214
+ label: resolveBlockDefinition(child.name, { sources, block: child })
215
215
  .label,
216
216
  ...(children.length ? { children } : {}),
217
217
  }
@@ -279,7 +279,7 @@ const TABLE_MARKDOWN =
279
279
  * target inside a component that takes only some children.
280
280
  */
281
281
  export function buildPalette(
282
- overrides: PlutoBlocksConfig = {},
282
+ sources: PlutoBlockSources = {},
283
283
  accepts?: string[]
284
284
  ): PlutoPaletteGroup[] {
285
285
  const groups: PlutoPaletteGroup[] = []
@@ -322,7 +322,7 @@ export function buildPalette(
322
322
  ],
323
323
  })
324
324
  }
325
- for (const { group, items } of listPaletteBlocks(overrides)) {
325
+ for (const { group, items } of listPaletteBlocks(sources)) {
326
326
  const entries = items
327
327
  .filter((item) => !accepts || accepts.includes(item.name))
328
328
  .map((item): PlutoPaletteEntry => ({
@@ -37,7 +37,8 @@ export function groupLabelKey(group: string): string {
37
37
  : group
38
38
  }
39
39
 
40
- const COLORS = [
40
+ /** The theme colors of Nuxt UI. */
41
+ export const BLOCK_COLORS = [
41
42
  'primary',
42
43
  'secondary',
43
44
  'success',
@@ -125,7 +126,7 @@ const DEFINITIONS: PlutoBlockDefinition[] = [
125
126
  block('callout', 'callouts', 'i-lucide-message-square-quote', {
126
127
  props: {
127
128
  icon: prop('icon', 'icon'),
128
- color: prop('select', 'color', { options: options(COLORS) }),
129
+ color: prop('select', 'color', { options: options(BLOCK_COLORS) }),
129
130
  to: prop('link', 'to', { advanced: true }),
130
131
  target: prop('select', 'target', {
131
132
  options: options(TARGETS),
@@ -139,7 +140,7 @@ const DEFINITIONS: PlutoBlockDefinition[] = [
139
140
  props: {
140
141
  title: prop('string', 'title'),
141
142
  icon: prop('icon', 'icon'),
142
- color: prop('select', 'color', { options: options(COLORS) }),
143
+ color: prop('select', 'color', { options: options(BLOCK_COLORS) }),
143
144
  to: prop('link', 'to'),
144
145
  target: prop('select', 'target', {
145
146
  options: options(TARGETS),
@@ -234,7 +235,7 @@ const DEFINITIONS: PlutoBlockDefinition[] = [
234
235
  hidden: true,
235
236
  props: {
236
237
  label: prop('string', 'label'),
237
- color: prop('select', 'color', { options: options(COLORS) }),
238
+ color: prop('select', 'color', { options: options(BLOCK_COLORS) }),
238
239
  },
239
240
  }),
240
241
  block('kbd', 'text', 'i-lucide-keyboard', {
@@ -348,7 +349,7 @@ const DEFINITIONS: PlutoBlockDefinition[] = [
348
349
  label: prop('string', 'label', { slot: 'default' }),
349
350
  to: prop('link', 'to'),
350
351
  icon: prop('icon', 'icon'),
351
- color: prop('select', 'color', { options: options(COLORS) }),
352
+ color: prop('select', 'color', { options: options(BLOCK_COLORS) }),
352
353
  variant: prop('select', 'variant', {
353
354
  options: options([
354
355
  'solid',