Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/content/5.content.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,33 @@ export default defineNuxtConfig({
- **ungrouped**: `'include'` (default) shows unmatched components in a fallback group; `'omit'` hides them.
- Group patterns use the same glob rules as `include`/`exclude` (name vs path by `/`).

#### Built-in slash menu (Style / Insert)

The `/` menu includes built-in TipTap sections (**Style** and **Insert**) for headings, lists, marks, images, and so on. `meta.components` only filters **Vue components**; to hide these defaults, use `meta.slashCommand.exclude`:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
studio: {
meta: {
// Hide every built-in block; only your configured component groups / list remain
slashCommand: {
exclude: ['style', 'insert'],
},
},
},
})
```

You can also hide individual entries, for example:

```ts [nuxt.config.ts]
slashCommand: {
exclude: ['blockquote', 'horizontalRule', 'video'],
},
```

Supported keys: `style`, `insert` (whole sections), plus `paragraph`, `heading1`–`heading4`, `bulletList`, `orderedList`, `blockquote`, `codeBlock`, `bold`, `italic`, `strike`, `code`, `image`, `video`, `horizontalRule`.

### Debug Mode

Enable **debug mode** from the footer menu to see the real-time conversion between:
Expand Down
5 changes: 3 additions & 2 deletions src/app/src/composables/useTiptapEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ export function useTiptapEditor() {
* Suggestion menu items
*/
const suggestionItems = computed(() => {
const exclude = host.meta.slashCommand.exclude
const componentGroups = host.meta.components.getGroups(t('studio.tiptap.editor.components'))

if (componentGroups.length === 0) {
return [
...getStandardSuggestionItems(t),
...getStandardSuggestionItems(t, exclude),
[
{
type: 'label',
Expand All @@ -84,7 +85,7 @@ export function useTiptapEditor() {
])

return [
...getStandardSuggestionItems(t),
...getStandardSuggestionItems(t, exclude),
...componentGroupItems,
] satisfies EditorSuggestionMenuItem[][]
})
Expand Down
3 changes: 3 additions & 0 deletions src/app/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ export type { MarkdownParsingOptions } from './types/content'
export type { GitProviderType } from './types/git'
export type { ComponentMeta } from './types/component'
export type { AIGenerateOptions, AIHintOptions, CursorContext, DiffPart, AITransformCallbacks } from './types/ai'
export type { SlashCommandExcludeKey, SlashCommandConfig } from './types/slashCommand'
export { SLASH_COMMAND_EXCLUDE_KEYS } from './types/slashCommand'
export { normalizeSlashCommandConfig } from './utils/slashCommand'

// Temporary export for remark emoji plugin
export * from './utils/emoji'
3 changes: 3 additions & 0 deletions src/app/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Repository } from './git'
import type { ComponentMeta } from './component'
import type { MarkdownParsingOptions, SyntaxHighlightTheme } from './content'
import type { CollectionInfo } from '@nuxt/content'
import type { SlashCommandConfig } from './slashCommand'

export * from './file'
export * from './item'
Expand All @@ -22,6 +23,7 @@ export * from './media'
export * from './content'
export * from './form'
export * from './ai'
export * from './slashCommand'

export interface StudioHost {
meta: {
Expand All @@ -47,6 +49,7 @@ export interface StudioHost {
hasNuxtUI: ComputedRef<boolean>
getGroups: (fallbackLabel: string) => Array<{ label: string, components: ComponentMeta[] }>
}
slashCommand: SlashCommandConfig
defaultLocale: string
getHighlightTheme: () => SyntaxHighlightTheme
}
Expand Down
39 changes: 39 additions & 0 deletions src/app/src/types/slashCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Built-in slash command entries in the TipTap `/` menu (Style and Insert sections).
* Use with `studio.meta.slashCommand.exclude` in `nuxt.config.ts`.
*
* - `style` / `insert` remove the entire section.
* - Other keys remove a single default command (headings use `heading1` to `heading4`).
*/
export const SLASH_COMMAND_EXCLUDE_KEYS = [
'style',
'insert',
'paragraph',
'heading1',
'heading2',
'heading3',
'heading4',
'bulletList',
'orderedList',
'blockquote',
'codeBlock',
'bold',
'italic',
'strike',
'code',
'image',
'video',
'horizontalRule',
] as const

export type SlashCommandExcludeKey = typeof SLASH_COMMAND_EXCLUDE_KEYS[number]

/**
* Configuration for built-in TipTap slash menu defaults (not custom components).
*/
export interface SlashCommandConfig {
/**
* Keys to hide from the `/` suggestion menu. Omitted keys remain visible.
*/
exclude?: SlashCommandExcludeKey[]
}
14 changes: 14 additions & 0 deletions src/app/src/utils/slashCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { SlashCommandConfig, SlashCommandExcludeKey } from '../types/slashCommand'
import { SLASH_COMMAND_EXCLUDE_KEYS } from '../types/slashCommand'

const slashCommandExcludeKeySet = new Set<string>(SLASH_COMMAND_EXCLUDE_KEYS)

function isSlashCommandExcludeKey(key: string): key is SlashCommandExcludeKey {
return slashCommandExcludeKeySet.has(key)
}

export function normalizeSlashCommandConfig(config?: { exclude?: string[] } | SlashCommandConfig): SlashCommandConfig {
return {
exclude: (config?.exclude ?? []).filter(isSlashCommandExcludeKey),
}
}
141 changes: 106 additions & 35 deletions src/app/src/utils/tiptap/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,24 @@ import type { Editor, JSONContent } from '@tiptap/vue-3'
import { mapEditorItems } from '@nuxt/ui/utils/editor'
import { upperFirst } from 'scule'

import type { SlashCommandExcludeKey } from '../../types/slashCommand'

import { isEmpty, omit } from '../object'

type TFunction = (key: string) => string
type SlashCommandSectionKey = Extract<SlashCommandExcludeKey, 'style' | 'insert'>
type SlashCommandItemKey = Exclude<SlashCommandExcludeKey, SlashCommandSectionKey>

interface SlashCommandItemDefinition {
key: SlashCommandItemKey
item: EditorSuggestionMenuItem
}

interface SlashCommandSectionDefinition {
key: SlashCommandSectionKey
label: string
items: SlashCommandItemDefinition[]
}

export const getHeadingItems = (t: TFunction) => [
{ kind: 'heading', level: 1, label: t('studio.tiptap.toolbar.h1'), icon: 'i-lucide-heading-1' },
Expand Down Expand Up @@ -89,47 +104,103 @@ export const getStandardToolbarItems = (t: TFunction) => [
],
] satisfies EditorToolbarItem[][]

export const getStandardSuggestionItems = (t: TFunction): EditorSuggestionMenuItem[][] => [
[
function buildStandardSuggestionSections(t: TFunction): SlashCommandSectionDefinition[] {
const [heading1, heading2, heading3, heading4] = getHeadingItems(t) as EditorSuggestionMenuItem[]
const [bulletList, orderedList] = getListItems(t) as EditorSuggestionMenuItem[]
const [blockquote, codeBlock] = getCodeBlockItem(t) as EditorSuggestionMenuItem[]
const [bold, italic, strike, code] = getMarkItems(t) as EditorSuggestionMenuItem[]

return [
{
type: 'label',
key: 'style',
label: t('studio.tiptap.suggestion.style'),
}, {
kind: 'paragraph',
label: t('studio.tiptap.suggestion.paragraph'),
icon: 'i-lucide-type',
items: [
{
key: 'paragraph',
item: {
kind: 'paragraph',
label: t('studio.tiptap.suggestion.paragraph'),
icon: 'i-lucide-type',
},
},
{ key: 'heading1', item: heading1! },
{ key: 'heading2', item: heading2! },
{ key: 'heading3', item: heading3! },
{ key: 'heading4', item: heading4! },
{ key: 'bulletList', item: bulletList! },
{ key: 'orderedList', item: orderedList! },
{ key: 'blockquote', item: blockquote! },
{ key: 'codeBlock', item: codeBlock! },
{ key: 'bold', item: bold! },
{ key: 'italic', item: italic! },
{ key: 'strike', item: strike! },
{ key: 'code', item: code! },
],
},
...getHeadingItems(t) as EditorSuggestionMenuItem[],
...getListItems(t) as EditorSuggestionMenuItem[],
...getCodeBlockItem(t) as EditorSuggestionMenuItem[],
...getMarkItems(t) as EditorSuggestionMenuItem[],
],
[
{
type: 'label',
key: 'insert',
label: t('studio.tiptap.suggestion.insert'),
items: [
{
key: 'image',
item: {
kind: 'image',
label: t('studio.tiptap.suggestion.image'),
icon: 'i-lucide-image',
},
},
{
key: 'video',
item: {
kind: 'video',
label: t('studio.tiptap.suggestion.video'),
icon: 'i-lucide-video',
},
},
{
key: 'horizontalRule',
item: {
kind: 'horizontalRule',
label: t('studio.tiptap.suggestion.horizontalRule'),
icon: 'i-lucide-separator-horizontal',
},
},
],
},
{
// kind: 'emoji',
// label: 'Emoji',
// icon: 'i-lucide-smile-plus',
// }, {
kind: 'image',
label: t('studio.tiptap.suggestion.image'),
icon: 'i-lucide-image',
},
{
kind: 'video',
label: t('studio.tiptap.suggestion.video'),
icon: 'i-lucide-video',
},
{
kind: 'horizontalRule',
label: t('studio.tiptap.suggestion.horizontalRule'),
icon: 'i-lucide-separator-horizontal',
},
],
]
]
}

/**
* Built-in `/` suggestion sections (Style, Insert), optionally filtered by `studio.meta.slashCommand.exclude`.
*/
export function getStandardSuggestionItems(
t: TFunction,
exclude?: readonly SlashCommandExcludeKey[],
): EditorSuggestionMenuItem[][] {
const hidden = new Set(exclude)

return buildStandardSuggestionSections(t).flatMap((section) => {
if (hidden.has(section.key)) {
return []
}

const items = section.items
.filter(({ key }) => !hidden.has(key))
.map(({ item }) => item)

if (items.length === 0) {
return []
}

return [[
{
type: 'label',
label: section.label,
},
...items,
]]
})
}

export const standardNuxtUIComponents: Record<string, { name: string, icon: string }> = {
'icon-menu-toggle': { name: 'Icon Menu Toggle', icon: 'i-lucide-menu' },
Expand Down
Loading
Loading