@djangocfg/ui-tools 2.1.417 → 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.
Files changed (35) hide show
  1. package/dist/audio-player/index.cjs +1 -2
  2. package/dist/audio-player/index.cjs.map +1 -1
  3. package/dist/audio-player/index.d.cts +3 -11
  4. package/dist/audio-player/index.d.ts +3 -11
  5. package/dist/audio-player/index.mjs +1 -2
  6. package/dist/audio-player/index.mjs.map +1 -1
  7. package/dist/tree/index.cjs +0 -3
  8. package/dist/tree/index.cjs.map +1 -1
  9. package/dist/tree/index.mjs +0 -3
  10. package/dist/tree/index.mjs.map +1 -1
  11. package/package.json +30 -14
  12. package/src/tools/data/Tree/components/TreeRow.tsx +0 -11
  13. package/src/tools/forms/CodeEditor/components/Editor.tsx +19 -0
  14. package/src/tools/forms/CodeEditor/types/index.ts +7 -0
  15. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +40 -0
  16. package/src/tools/forms/MarkdownEditor/styles.css +174 -21
  17. package/src/tools/forms/NotionEditor/CustomKeymap.ts +48 -0
  18. package/src/tools/forms/NotionEditor/LinkDialog.tsx +133 -0
  19. package/src/tools/forms/NotionEditor/NotionEditor.tsx +304 -0
  20. package/src/tools/forms/NotionEditor/SlashExtension.ts +32 -0
  21. package/src/tools/forms/NotionEditor/SlashList.tsx +136 -0
  22. package/src/tools/forms/NotionEditor/TaskItemView.tsx +41 -0
  23. package/src/tools/forms/NotionEditor/createSlashSuggestion.ts +121 -0
  24. package/src/tools/forms/NotionEditor/extensions.ts +105 -0
  25. package/src/tools/forms/NotionEditor/index.ts +1 -0
  26. package/src/tools/forms/NotionEditor/lazy.tsx +44 -0
  27. package/src/tools/forms/NotionEditor/slashItems.ts +159 -0
  28. package/src/tools/forms/NotionEditor/styles.css +478 -0
  29. package/src/tools/forms/NotionEditor/types.ts +28 -0
  30. package/src/tools/media/AudioPlayer/PlayerShell.tsx +3 -11
  31. package/src/tools/media/AudioPlayer/types.ts +4 -11
  32. package/src/tools/media/ImageViewer/components/ImageViewer.tsx +8 -0
  33. package/src/tools/media/ImageViewer/types.ts +4 -0
  34. package/src/tools/media/VideoPlayer/VideoPlayer.tsx +20 -1
  35. package/src/tools/media/VideoPlayer/types.ts +4 -0
@@ -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
+ }