@djangocfg/ui-tools 2.1.416 → 2.1.418
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/dist/audio-player/index.cjs +2098 -0
- package/dist/audio-player/index.cjs.map +1 -0
- package/dist/audio-player/index.css +65 -0
- package/dist/audio-player/index.css.map +1 -0
- package/dist/audio-player/index.d.cts +166 -0
- package/dist/audio-player/index.d.ts +166 -0
- package/dist/audio-player/index.mjs +2075 -0
- package/dist/audio-player/index.mjs.map +1 -0
- package/dist/composer-registry/index.cjs +45 -0
- package/dist/composer-registry/index.cjs.map +1 -0
- package/dist/composer-registry/index.d.cts +73 -0
- package/dist/composer-registry/index.d.ts +73 -0
- package/dist/composer-registry/index.mjs +39 -0
- package/dist/composer-registry/index.mjs.map +1 -0
- package/dist/tree/index.cjs +82 -63
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +15 -1
- package/dist/tree/index.d.ts +15 -1
- package/dist/tree/index.mjs +83 -64
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +38 -17
- package/src/tools/chat/composer/Composer.tsx +8 -8
- package/src/tools/chat/context/ChatProvider.tsx +13 -78
- package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
- package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
- package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
- package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
- package/src/tools/data/Tree/TreeRoot.tsx +33 -109
- package/src/tools/data/Tree/context/TreeContext.tsx +22 -3
- package/src/tools/data/Tree/context/menu/index.ts +1 -0
- package/src/tools/data/Tree/context/menu/render.tsx +75 -0
- package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +16 -2
- package/src/tools/data/Tree/index.tsx +1 -0
- package/src/tools/data/Tree/types/index.ts +1 -1
- package/src/tools/data/Tree/types/root-props.ts +16 -0
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
- package/src/tools/forms/CodeEditor/components/Editor.tsx +19 -0
- package/src/tools/forms/CodeEditor/types/index.ts +7 -0
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +40 -0
- package/src/tools/forms/MarkdownEditor/styles.css +174 -21
- package/src/tools/forms/NotionEditor/CustomKeymap.ts +48 -0
- package/src/tools/forms/NotionEditor/LinkDialog.tsx +133 -0
- package/src/tools/forms/NotionEditor/NotionEditor.tsx +304 -0
- package/src/tools/forms/NotionEditor/SlashExtension.ts +32 -0
- package/src/tools/forms/NotionEditor/SlashList.tsx +136 -0
- package/src/tools/forms/NotionEditor/TaskItemView.tsx +41 -0
- package/src/tools/forms/NotionEditor/createSlashSuggestion.ts +121 -0
- package/src/tools/forms/NotionEditor/extensions.ts +105 -0
- package/src/tools/forms/NotionEditor/index.ts +1 -0
- package/src/tools/forms/NotionEditor/lazy.tsx +44 -0
- package/src/tools/forms/NotionEditor/slashItems.ts +159 -0
- package/src/tools/forms/NotionEditor/styles.css +478 -0
- package/src/tools/forms/NotionEditor/types.ts +28 -0
- package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
- package/src/tools/integration/ComposerRegistry/index.ts +105 -0
- package/src/tools/media/AudioPlayer/Player.tsx +2 -0
- package/src/tools/media/AudioPlayer/PlayerShell.tsx +29 -22
- package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
- package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
- package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
- package/src/tools/media/AudioPlayer/types.ts +8 -0
- package/src/tools/media/ImageViewer/components/ImageViewer.tsx +8 -0
- package/src/tools/media/ImageViewer/types.ts +4 -0
- package/src/tools/media/VideoPlayer/VideoPlayer.tsx +20 -1
- package/src/tools/media/VideoPlayer/types.ts +4 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ReactRenderer } from '@tiptap/react';
|
|
2
|
+
import type { SuggestionOptions } from '@tiptap/suggestion';
|
|
3
|
+
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
|
|
4
|
+
import { SlashList, type SlashListRef } from './SlashList';
|
|
5
|
+
import { filterSlashItems, type SlashItem } from './slashItems';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `@tiptap/suggestion` config for the slash menu. Mirrors the mention
|
|
9
|
+
* suggestion pattern in `MarkdownEditor/createMentionSuggestion.ts`:
|
|
10
|
+
* floating-ui positioning, virtual element backed by the caret rect,
|
|
11
|
+
* keyboard nav delegated to `SlashList` via an imperative ref.
|
|
12
|
+
*
|
|
13
|
+
* Editor never imports this directly — `notionExtensions()` consumes it.
|
|
14
|
+
*/
|
|
15
|
+
export function createSlashSuggestion(): Omit<SuggestionOptions<SlashItem>, 'editor'> {
|
|
16
|
+
return {
|
|
17
|
+
char: '/',
|
|
18
|
+
// Block the menu inside code blocks — `/` is valid syntax there
|
|
19
|
+
// (regex literals, paths) and a floating popover would be noise.
|
|
20
|
+
// Use `editor.isActive('codeBlock')` rather than `state.doc.resolve()`
|
|
21
|
+
// so nested cases (code block in a table cell, in a list item) all
|
|
22
|
+
// resolve correctly — `parent.type.name` only sees the immediate
|
|
23
|
+
// ancestor, which can be the cell/li instead of the code block.
|
|
24
|
+
allow: ({ editor }) => !editor.isActive('codeBlock'),
|
|
25
|
+
items: ({ query }) => filterSlashItems(query),
|
|
26
|
+
command: ({ editor, range, props }) => {
|
|
27
|
+
props.command({ editor, range });
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
render: () => {
|
|
31
|
+
let component: ReactRenderer<SlashListRef> | null = null;
|
|
32
|
+
let popup: HTMLDivElement | null = null;
|
|
33
|
+
let cleanupAutoUpdate: (() => void) | null = null;
|
|
34
|
+
let getReferenceRect: (() => DOMRect | null) | null = null;
|
|
35
|
+
|
|
36
|
+
const buildVirtualElement = () => ({
|
|
37
|
+
getBoundingClientRect: () => getReferenceRect?.() ?? new DOMRect(0, 0, 0, 0),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const setPopupVisible = (visible: boolean) => {
|
|
41
|
+
if (popup) popup.style.display = visible ? '' : 'none';
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const updatePosition = () => {
|
|
45
|
+
if (!popup) return;
|
|
46
|
+
const virtualEl = buildVirtualElement();
|
|
47
|
+
void computePosition(virtualEl, popup, {
|
|
48
|
+
placement: 'bottom-start',
|
|
49
|
+
middleware: [
|
|
50
|
+
offset(6),
|
|
51
|
+
flip({ fallbackPlacements: ['top-start'] }),
|
|
52
|
+
shift({ padding: 8 }),
|
|
53
|
+
],
|
|
54
|
+
}).then(({ x, y }) => {
|
|
55
|
+
if (!popup) return;
|
|
56
|
+
popup.style.transform = `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`;
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const teardown = () => {
|
|
61
|
+
cleanupAutoUpdate?.();
|
|
62
|
+
cleanupAutoUpdate = null;
|
|
63
|
+
popup?.remove();
|
|
64
|
+
popup = null;
|
|
65
|
+
component?.destroy();
|
|
66
|
+
component = null;
|
|
67
|
+
getReferenceRect = null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
onStart: (props) => {
|
|
72
|
+
component = new ReactRenderer(SlashList, {
|
|
73
|
+
props: {
|
|
74
|
+
items: props.items,
|
|
75
|
+
command: (item: SlashItem) => {
|
|
76
|
+
props.command(item);
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
editor: props.editor,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
popup = document.createElement('div');
|
|
83
|
+
popup.style.cssText = 'position: absolute; top: 0; left: 0; z-index: 99999;';
|
|
84
|
+
popup.appendChild(component.element);
|
|
85
|
+
document.body.appendChild(popup);
|
|
86
|
+
setPopupVisible(props.items.length > 0);
|
|
87
|
+
|
|
88
|
+
getReferenceRect = () => props.clientRect?.() ?? null;
|
|
89
|
+
const virtualEl = buildVirtualElement();
|
|
90
|
+
cleanupAutoUpdate = autoUpdate(virtualEl, popup, updatePosition);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
onUpdate: (props) => {
|
|
94
|
+
component?.updateProps({
|
|
95
|
+
items: props.items,
|
|
96
|
+
command: (item: SlashItem) => {
|
|
97
|
+
props.command(item);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
setPopupVisible(props.items.length > 0);
|
|
101
|
+
getReferenceRect = () => props.clientRect?.() ?? null;
|
|
102
|
+
updatePosition();
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
onKeyDown: (props) => {
|
|
106
|
+
if (props.event.key === 'Escape') {
|
|
107
|
+
teardown();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
// `props.event` is a native KeyboardEvent — SlashListRef types
|
|
111
|
+
// it as such, no cast needed.
|
|
112
|
+
return component?.ref?.onKeyDown(props.event) ?? false;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
onExit: () => {
|
|
116
|
+
teardown();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { AnyExtension } from '@tiptap/core';
|
|
2
|
+
import { ReactNodeViewRenderer } from '@tiptap/react';
|
|
3
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
4
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
5
|
+
import Highlight from '@tiptap/extension-highlight';
|
|
6
|
+
import TaskList from '@tiptap/extension-task-list';
|
|
7
|
+
import TaskItem from '@tiptap/extension-task-item';
|
|
8
|
+
import { Table } from '@tiptap/extension-table';
|
|
9
|
+
import { TableRow } from '@tiptap/extension-table-row';
|
|
10
|
+
import { TableHeader } from '@tiptap/extension-table-header';
|
|
11
|
+
import { TableCell } from '@tiptap/extension-table-cell';
|
|
12
|
+
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
|
13
|
+
import { Markdown } from '@tiptap/markdown';
|
|
14
|
+
import { common, createLowlight } from 'lowlight';
|
|
15
|
+
import GlobalDragHandle from 'tiptap-extension-global-drag-handle';
|
|
16
|
+
import { SlashExtension } from './SlashExtension';
|
|
17
|
+
import { createSlashSuggestion } from './createSlashSuggestion';
|
|
18
|
+
import { TaskItemView } from './TaskItemView';
|
|
19
|
+
import { CustomKeymap } from './CustomKeymap';
|
|
20
|
+
|
|
21
|
+
// Lowlight bundle: "common" languages cover ts/js/json/py/go/rust/etc.
|
|
22
|
+
// The full language pack pulls in ~300KB more — opt-in case by case.
|
|
23
|
+
const lowlight = createLowlight(common);
|
|
24
|
+
|
|
25
|
+
export interface BuildExtensionsOptions {
|
|
26
|
+
placeholder: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Assemble the Notion-flavour TipTap extension stack.
|
|
31
|
+
*
|
|
32
|
+
* Why a single factory rather than inline `useEditor({ extensions: [...] })`:
|
|
33
|
+
* - `useEditor` captures extensions once on first render. If we built
|
|
34
|
+
* them inline, every parent re-render would create new instances and
|
|
35
|
+
* React's deps array would force a teardown loop (or, with stable
|
|
36
|
+
* deps, we'd be hiding the problem).
|
|
37
|
+
* - All consumers want the same baseline. Splitting per-extension wires
|
|
38
|
+
* to a builder keeps the assembly readable.
|
|
39
|
+
*
|
|
40
|
+
* StarterKit overrides:
|
|
41
|
+
* - `codeBlock: false` — replaced by `CodeBlockLowlight` for syntax
|
|
42
|
+
* highlighting via lowlight's `common` language pack.
|
|
43
|
+
*
|
|
44
|
+
* Markdown serialisation:
|
|
45
|
+
* - `@tiptap/markdown` already handles starter-kit nodes + tables.
|
|
46
|
+
* - Task lists serialise as GFM `- [x]` / `- [ ]` via the same package.
|
|
47
|
+
* - `Highlight` round-trips as `==text==` (tiptap-markdown default).
|
|
48
|
+
*/
|
|
49
|
+
export function notionExtensions(opts: BuildExtensionsOptions): AnyExtension[] {
|
|
50
|
+
return [
|
|
51
|
+
StarterKit.configure({
|
|
52
|
+
heading: { levels: [1, 2, 3, 4] },
|
|
53
|
+
// Disable starter-kit's plain CodeBlock — we replace it with the
|
|
54
|
+
// lowlight variant below for syntax highlighting.
|
|
55
|
+
codeBlock: false,
|
|
56
|
+
}),
|
|
57
|
+
Placeholder.configure({
|
|
58
|
+
// Per-node placeholder: empty H1 → "Heading 1", H2 → "Heading 2",
|
|
59
|
+
// empty paragraph → the supplied placeholder ("Type / for…").
|
|
60
|
+
// Matches Notion: an empty heading is labelled with its level so the
|
|
61
|
+
// user sees structure before content; an empty body block shows the
|
|
62
|
+
// command hint. The function form is supported by tiptap-extensions
|
|
63
|
+
// Placeholder; the `node` arg is the block at the caret.
|
|
64
|
+
placeholder: ({ node }) => {
|
|
65
|
+
if (node.type.name === 'heading') {
|
|
66
|
+
const level = node.attrs.level as number | undefined;
|
|
67
|
+
return level ? `Heading ${level}` : 'Heading';
|
|
68
|
+
}
|
|
69
|
+
return opts.placeholder;
|
|
70
|
+
},
|
|
71
|
+
// includeChildren=true so placeholders also reach inside list items
|
|
72
|
+
// and quotes (otherwise an empty bullet line shows no hint).
|
|
73
|
+
includeChildren: true,
|
|
74
|
+
}),
|
|
75
|
+
Markdown,
|
|
76
|
+
Highlight.configure({ multicolor: false }),
|
|
77
|
+
TaskList,
|
|
78
|
+
// React NodeView swaps Tiptap's default `<input type="checkbox">`
|
|
79
|
+
// for our ui-core <Checkbox> — picks up macOS / token styling and
|
|
80
|
+
// tracks the editor theme automatically. See TaskItemView.tsx.
|
|
81
|
+
TaskItem.extend({
|
|
82
|
+
addNodeView() {
|
|
83
|
+
return ReactNodeViewRenderer(TaskItemView);
|
|
84
|
+
},
|
|
85
|
+
}).configure({ nested: true }),
|
|
86
|
+
Table.configure({ resizable: false }),
|
|
87
|
+
TableRow,
|
|
88
|
+
TableHeader,
|
|
89
|
+
TableCell,
|
|
90
|
+
CodeBlockLowlight.configure({ lowlight }),
|
|
91
|
+
// Drag handle for blocks. Renders a small grabber to the left of the
|
|
92
|
+
// hovered block; clicking + dragging reorders the document. Reads CSS
|
|
93
|
+
// from our styles.css (`.drag-handle`).
|
|
94
|
+
GlobalDragHandle.configure({
|
|
95
|
+
dragHandleWidth: 18,
|
|
96
|
+
// `scrollTreshold` (sic) is the option name the upstream library
|
|
97
|
+
// ships — typo and all. Spelling it correctly is silently ignored.
|
|
98
|
+
scrollTreshold: 100,
|
|
99
|
+
}),
|
|
100
|
+
SlashExtension.configure({
|
|
101
|
+
suggestion: createSlashSuggestion(),
|
|
102
|
+
}),
|
|
103
|
+
CustomKeymap,
|
|
104
|
+
];
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lazy';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `@djangocfg/ui-tools/notion-editor` subpath entrypoint.
|
|
5
|
+
*
|
|
6
|
+
* `NotionEditor` is a TipTap WYSIWYG with the full Notion-style stack:
|
|
7
|
+
* StarterKit + Markdown + Highlight + Table + TaskList + CodeBlockLowlight
|
|
8
|
+
* + GlobalDragHandle + a custom `/` slash menu. The full bundle including
|
|
9
|
+
* lowlight's `common` language pack lands around ~350KB minified — wrap
|
|
10
|
+
* the component in React.lazy so pages that don't render the editor pay
|
|
11
|
+
* nothing.
|
|
12
|
+
*
|
|
13
|
+
* Why a hand-rolled lazy wrapper (mirrors MarkdownEditor/lazy.tsx):
|
|
14
|
+
* `createLazyComponent` returns a plain function component that does not
|
|
15
|
+
* forward `ref`, so the imperative `NotionEditorHandle` would be silently
|
|
16
|
+
* dropped. The wrapper below is a `forwardRef` so the ref reaches the
|
|
17
|
+
* underlying TipTap editor through `React.lazy`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Suspense, forwardRef, lazy } from 'react';
|
|
21
|
+
import { LoadingFallback } from '../../../components';
|
|
22
|
+
import type { NotionEditorHandle, NotionEditorProps } from './types';
|
|
23
|
+
|
|
24
|
+
const NotionEditorImpl = lazy(() =>
|
|
25
|
+
import('./NotionEditor').then((m) => ({ default: m.NotionEditor })),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const LazyNotionEditor = forwardRef<NotionEditorHandle, NotionEditorProps>(
|
|
29
|
+
function LazyNotionEditor(props, ref) {
|
|
30
|
+
return (
|
|
31
|
+
<Suspense fallback={<LoadingFallback minHeight={240} text="Loading editor…" />}>
|
|
32
|
+
<NotionEditorImpl {...props} ref={ref} />
|
|
33
|
+
</Suspense>
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// `NotionEditor` is the public eager-looking name — it resolves to the
|
|
39
|
+
// same lazy component so importing it from this subpath does NOT pull
|
|
40
|
+
// ~350KB of TipTap + lowlight into the caller's initial bundle.
|
|
41
|
+
export { LazyNotionEditor as NotionEditor };
|
|
42
|
+
|
|
43
|
+
export type { NotionEditorHandle, NotionEditorProps } from './types';
|
|
44
|
+
export type { SlashItem } from './slashItems';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CheckSquare,
|
|
3
|
+
Code,
|
|
4
|
+
Heading1,
|
|
5
|
+
Heading2,
|
|
6
|
+
Heading3,
|
|
7
|
+
List,
|
|
8
|
+
ListOrdered,
|
|
9
|
+
Minus,
|
|
10
|
+
Quote,
|
|
11
|
+
Table as TableIcon,
|
|
12
|
+
Text,
|
|
13
|
+
type LucideIcon,
|
|
14
|
+
} from 'lucide-react';
|
|
15
|
+
import type { Editor, Range } from '@tiptap/react';
|
|
16
|
+
|
|
17
|
+
export interface SlashItem {
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
icon: LucideIcon;
|
|
21
|
+
/** Markdown shorthand hint shown right-aligned in the menu — teaches
|
|
22
|
+
* the user that they can also type this directly (e.g. `#`, `- [ ]`). */
|
|
23
|
+
hint?: string;
|
|
24
|
+
/** Extra search aliases (besides `title`). */
|
|
25
|
+
aliases?: string[];
|
|
26
|
+
command: (ctx: { editor: Editor; range: Range }) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Canonical Notion-style slash menu. Each command runs `deleteRange(range)`
|
|
31
|
+
* first so the typed `/verb` text is removed, then applies the block
|
|
32
|
+
* transformation. `focus()` keeps the caret in the editor after the
|
|
33
|
+
* popover closes.
|
|
34
|
+
*/
|
|
35
|
+
export const slashItems: SlashItem[] = [
|
|
36
|
+
{
|
|
37
|
+
title: 'Text',
|
|
38
|
+
description: 'Plain paragraph.',
|
|
39
|
+
icon: Text,
|
|
40
|
+
aliases: ['p', 'paragraph'],
|
|
41
|
+
command: ({ editor, range }) => {
|
|
42
|
+
editor.chain().focus().deleteRange(range).setNode('paragraph').run();
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: 'Heading 1',
|
|
47
|
+
description: 'Big section heading.',
|
|
48
|
+
icon: Heading1,
|
|
49
|
+
hint: '#',
|
|
50
|
+
aliases: ['h1', 'title'],
|
|
51
|
+
command: ({ editor, range }) => {
|
|
52
|
+
editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run();
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
title: 'Heading 2',
|
|
57
|
+
description: 'Medium section heading.',
|
|
58
|
+
icon: Heading2,
|
|
59
|
+
hint: '##',
|
|
60
|
+
aliases: ['h2', 'subtitle'],
|
|
61
|
+
command: ({ editor, range }) => {
|
|
62
|
+
editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run();
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: 'Heading 3',
|
|
67
|
+
description: 'Small section heading.',
|
|
68
|
+
icon: Heading3,
|
|
69
|
+
hint: '###',
|
|
70
|
+
aliases: ['h3'],
|
|
71
|
+
command: ({ editor, range }) => {
|
|
72
|
+
editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run();
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
title: 'Bullet list',
|
|
77
|
+
description: 'Simple unordered list.',
|
|
78
|
+
icon: List,
|
|
79
|
+
hint: '- ',
|
|
80
|
+
aliases: ['ul', 'unordered'],
|
|
81
|
+
command: ({ editor, range }) => {
|
|
82
|
+
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
title: 'Numbered list',
|
|
87
|
+
description: 'List with auto-numbering.',
|
|
88
|
+
icon: ListOrdered,
|
|
89
|
+
hint: '1. ',
|
|
90
|
+
aliases: ['ol', 'ordered', 'numbered'],
|
|
91
|
+
command: ({ editor, range }) => {
|
|
92
|
+
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
title: 'To-do list',
|
|
97
|
+
description: 'Tasks with checkboxes.',
|
|
98
|
+
icon: CheckSquare,
|
|
99
|
+
hint: '- [ ]',
|
|
100
|
+
aliases: ['todo', 'task', 'checkbox', 'check'],
|
|
101
|
+
command: ({ editor, range }) => {
|
|
102
|
+
editor.chain().focus().deleteRange(range).toggleTaskList().run();
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
title: 'Quote',
|
|
107
|
+
description: 'Capture a quote.',
|
|
108
|
+
icon: Quote,
|
|
109
|
+
hint: '> ',
|
|
110
|
+
aliases: ['blockquote'],
|
|
111
|
+
command: ({ editor, range }) => {
|
|
112
|
+
editor.chain().focus().deleteRange(range).toggleBlockquote().run();
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: 'Code block',
|
|
117
|
+
description: 'Syntax-highlighted code.',
|
|
118
|
+
icon: Code,
|
|
119
|
+
hint: '```',
|
|
120
|
+
aliases: ['codeblock', 'pre'],
|
|
121
|
+
command: ({ editor, range }) => {
|
|
122
|
+
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
title: 'Divider',
|
|
127
|
+
description: 'Horizontal rule.',
|
|
128
|
+
icon: Minus,
|
|
129
|
+
hint: '---',
|
|
130
|
+
aliases: ['hr', 'rule', 'separator'],
|
|
131
|
+
command: ({ editor, range }) => {
|
|
132
|
+
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
title: 'Table',
|
|
137
|
+
description: '3×3 starter table.',
|
|
138
|
+
icon: TableIcon,
|
|
139
|
+
aliases: ['grid'],
|
|
140
|
+
command: ({ editor, range }) => {
|
|
141
|
+
editor
|
|
142
|
+
.chain()
|
|
143
|
+
.focus()
|
|
144
|
+
.deleteRange(range)
|
|
145
|
+
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
146
|
+
.run();
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
/** Case-insensitive filter over title + aliases. */
|
|
152
|
+
export function filterSlashItems(query: string): SlashItem[] {
|
|
153
|
+
const q = query.trim().toLowerCase();
|
|
154
|
+
if (!q) return slashItems;
|
|
155
|
+
return slashItems.filter((item) => {
|
|
156
|
+
if (item.title.toLowerCase().includes(q)) return true;
|
|
157
|
+
return item.aliases?.some((a) => a.toLowerCase().includes(q)) ?? false;
|
|
158
|
+
});
|
|
159
|
+
}
|