@djangocfg/widget-editor 0.1.1
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/LICENSE +21 -0
- package/README.md +40 -0
- package/package.json +121 -0
- package/src/blocks/BlockError.tsx +46 -0
- package/src/blocks/index.ts +34 -0
- package/src/blocks/schemas.ts +143 -0
- package/src/blocks/types.ts +76 -0
- package/src/chips/README.md +67 -0
- package/src/chips/UrlChip.tsx +105 -0
- package/src/chips/index.ts +20 -0
- package/src/index.ts +12 -0
- package/src/link-preview/LinkPreviewCard.tsx +178 -0
- package/src/link-preview/index.ts +12 -0
- package/src/link-preview/types.ts +44 -0
- package/src/markdown/MarkdownEditor.tsx +700 -0
- package/src/markdown/MentionList.tsx +106 -0
- package/src/markdown/README.md +269 -0
- package/src/markdown/chip/ChipNode.ts +526 -0
- package/src/markdown/chip/syncChips.ts +80 -0
- package/src/markdown/createMentionSuggestion.ts +131 -0
- package/src/markdown/filePath/FilePathChip.tsx +68 -0
- package/src/markdown/filePath/detect.ts +289 -0
- package/src/markdown/filePath/index.ts +15 -0
- package/src/markdown/index.ts +29 -0
- package/src/markdown/lazy.tsx +88 -0
- package/src/markdown/markdown-editor.stories.tsx +500 -0
- package/src/markdown/mentionPresets.ts +49 -0
- package/src/markdown/slash/SlashCommandNode.ts +162 -0
- package/src/markdown/slash/index.ts +4 -0
- package/src/markdown/slash/syncSlashNode.ts +97 -0
- package/src/markdown/slash/types.ts +14 -0
- package/src/markdown/styles.css +522 -0
- package/src/markdown/submitOnEnter.ts +83 -0
- package/src/markdown/types.ts +50 -0
- package/src/markdown/url/detect.ts +227 -0
- package/src/notion/BookmarkNode.ts +161 -0
- package/src/notion/BookmarkView.tsx +45 -0
- package/src/notion/CustomKeymap.ts +48 -0
- package/src/notion/LinkDialog.tsx +133 -0
- package/src/notion/MapEditLayer.tsx +199 -0
- package/src/notion/MapNode.ts +220 -0
- package/src/notion/MapView.tsx +103 -0
- package/src/notion/NotionEditor.tsx +309 -0
- package/src/notion/README.md +158 -0
- package/src/notion/SlashExtension.ts +32 -0
- package/src/notion/SlashList.tsx +136 -0
- package/src/notion/TaskItemView.tsx +41 -0
- package/src/notion/createSlashSuggestion.ts +121 -0
- package/src/notion/extensions.ts +120 -0
- package/src/notion/index.ts +1 -0
- package/src/notion/lazy.tsx +44 -0
- package/src/notion/notion-editor.stories.tsx +293 -0
- package/src/notion/slashItems.ts +178 -0
- package/src/notion/styles.css +503 -0
- package/src/notion/types.ts +37 -0
- package/src/tiptap/index.ts +10 -0
- package/src/tiptap/useNodeAttrs.ts +128 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/react';
|
|
2
|
+
import type { ResolveLinkPreview } from '../link-preview';
|
|
3
|
+
|
|
4
|
+
export interface NotionEditorProps {
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (markdown: string) => void;
|
|
7
|
+
/** Markdown placeholder shown when document is empty. */
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
/** Disable editing — turns the editor into a read-only renderer. */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** Focus the editor on mount. Pair with `key={path}` upstream when the
|
|
12
|
+
* host wants a fresh focus per source change. */
|
|
13
|
+
autoFocus?: boolean;
|
|
14
|
+
/** Called when the user presses Cmd/Ctrl+S inside the editor. Receives
|
|
15
|
+
* the current markdown. The browser default is suppressed. */
|
|
16
|
+
onSave?: (markdown: string) => void;
|
|
17
|
+
/** Extra class name on the outer wrapper. */
|
|
18
|
+
className?: string;
|
|
19
|
+
/** Minimum height of the editor surface in px. Default 320. */
|
|
20
|
+
minHeight?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Host resolver for the `bookmark` (link-preview) block. When a user pastes
|
|
23
|
+
* a bare URL on an empty line it becomes a bookmark card; this resolver
|
|
24
|
+
* unfurls the URL into title/description/image (a Wails/Go method, a
|
|
25
|
+
* Next.js API route). Default `undefined` → the card renders its own
|
|
26
|
+
* hostname/favicon fallback. Same seam as chat's `config.linkPreview`.
|
|
27
|
+
*/
|
|
28
|
+
resolveLinkPreview?: ResolveLinkPreview;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Imperative handle exposed via `ref`. Compatible with `ComposerHandle`
|
|
32
|
+
* shape used by the chat suite (focus / moveCursorToEnd). */
|
|
33
|
+
export interface NotionEditorHandle {
|
|
34
|
+
focus: () => void;
|
|
35
|
+
moveCursorToEnd: () => void;
|
|
36
|
+
getEditor: () => Editor | null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TipTap helpers — reusable across every NotionEditor block NodeView.
|
|
3
|
+
*
|
|
4
|
+
* `useNodeAttrs` is the write-back seam an interactive NodeView uses to push UI
|
|
5
|
+
* changes (drag a pin, switch a basemap) back into the document's node attrs,
|
|
6
|
+
* so the change survives the markdown round-trip.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { useNodeAttrs } from './useNodeAttrs';
|
|
10
|
+
export type { UseNodeAttrsResult } from './useNodeAttrs';
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `useNodeAttrs` — the single write-back seam every interactive block NodeView
|
|
5
|
+
* uses to persist UI changes back into the document (→ TipTap node attrs →
|
|
6
|
+
* markdown round-trip).
|
|
7
|
+
*
|
|
8
|
+
* A NodeView is normally READ-ONLY: it renders `node.attrs` but has no path to
|
|
9
|
+
* push a user action (drag a pin, toggle a basemap) back into the node. This
|
|
10
|
+
* hook wraps TipTap's `updateAttributes` so an interactive component can do:
|
|
11
|
+
*
|
|
12
|
+
* const { attrs, patch } = useNodeAttrs<MapBlockPayload>(props);
|
|
13
|
+
* // …on drag-end:
|
|
14
|
+
* patch({ markers: nextMarkers });
|
|
15
|
+
*
|
|
16
|
+
* and the new attrs land in the document — `getMarkdown()` then serialises the
|
|
17
|
+
* updated payload.
|
|
18
|
+
*
|
|
19
|
+
* ## API
|
|
20
|
+
*
|
|
21
|
+
* - `attrs` — `props.node.attrs` typed as `T`. The current committed payload.
|
|
22
|
+
* - `patch(next)` — commit a partial attr update IMMEDIATELY. `updateAttributes`
|
|
23
|
+
* is a single ProseMirror transaction, so this is cheap; use it for DISCRETE
|
|
24
|
+
* edits (drag-END, add/remove pin, basemap pick) — which is the recommended
|
|
25
|
+
* design: the caller fires `patch` only when a value is final, never per
|
|
26
|
+
* drag-frame, so no transaction spam.
|
|
27
|
+
* - `patchDebounced(next, ms?)` — coalesce HIGH-FREQUENCY updates (a live edit
|
|
28
|
+
* firing many times: text input, a continuous slider). Rapid calls collapse
|
|
29
|
+
* into one trailing transaction after `ms` of quiet (default 250ms). Pending
|
|
30
|
+
* patches are MERGED so partial fields accumulate. Auto-flushes on unmount so
|
|
31
|
+
* the last edit is never lost.
|
|
32
|
+
* - `flush()` — commit any pending debounced patch immediately (e.g. on blur).
|
|
33
|
+
*
|
|
34
|
+
* Pure + reusable: no editor-specific coupling, only the `NodeViewProps`
|
|
35
|
+
* contract (`node`, `updateAttributes`). Node attrs stay plain-JSON
|
|
36
|
+
* serializable — pass only JSON-safe values through `patch`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
40
|
+
import type { NodeViewProps } from '@tiptap/react';
|
|
41
|
+
|
|
42
|
+
/** Default debounce window (ms) for `patchDebounced`. */
|
|
43
|
+
const DEFAULT_DEBOUNCE_MS = 250;
|
|
44
|
+
|
|
45
|
+
export interface UseNodeAttrsResult<T extends Record<string, unknown>> {
|
|
46
|
+
/** The node's current committed attrs, typed as `T`. */
|
|
47
|
+
attrs: T;
|
|
48
|
+
/** Commit a partial attr update immediately (one transaction). */
|
|
49
|
+
patch: (next: Partial<T>) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Commit a partial attr update on a trailing debounce; rapid calls merge
|
|
52
|
+
* into a single transaction. For live/high-frequency edits.
|
|
53
|
+
*/
|
|
54
|
+
patchDebounced: (next: Partial<T>, ms?: number) => void;
|
|
55
|
+
/** Flush any pending debounced patch now (e.g. on blur). */
|
|
56
|
+
flush: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Two-way binding between a NodeView's UI and its node attrs.
|
|
61
|
+
*
|
|
62
|
+
* @param props The `NodeViewProps` TipTap hands the React NodeView component.
|
|
63
|
+
*/
|
|
64
|
+
export function useNodeAttrs<T extends Record<string, unknown>>(
|
|
65
|
+
props: NodeViewProps,
|
|
66
|
+
): UseNodeAttrsResult<T> {
|
|
67
|
+
const { node, updateAttributes } = props;
|
|
68
|
+
const attrs = node.attrs as T;
|
|
69
|
+
|
|
70
|
+
// Keep `updateAttributes` reachable from the debounce timer without making
|
|
71
|
+
// the debounced callback re-create on every render (it can change identity).
|
|
72
|
+
const updateRef = useRef(updateAttributes);
|
|
73
|
+
updateRef.current = updateAttributes;
|
|
74
|
+
|
|
75
|
+
// Pending merged patch + its timer, held across renders.
|
|
76
|
+
const pendingRef = useRef<Partial<T> | null>(null);
|
|
77
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
78
|
+
|
|
79
|
+
const flush = useCallback(() => {
|
|
80
|
+
if (timerRef.current != null) {
|
|
81
|
+
clearTimeout(timerRef.current);
|
|
82
|
+
timerRef.current = null;
|
|
83
|
+
}
|
|
84
|
+
const pending = pendingRef.current;
|
|
85
|
+
if (pending) {
|
|
86
|
+
pendingRef.current = null;
|
|
87
|
+
updateRef.current(pending);
|
|
88
|
+
}
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const patch = useCallback(
|
|
92
|
+
(next: Partial<T>) => {
|
|
93
|
+
// A discrete edit supersedes any queued debounced patch: merge the
|
|
94
|
+
// pending fields in, then commit the lot as one transaction.
|
|
95
|
+
const pending = pendingRef.current;
|
|
96
|
+
if (timerRef.current != null) {
|
|
97
|
+
clearTimeout(timerRef.current);
|
|
98
|
+
timerRef.current = null;
|
|
99
|
+
}
|
|
100
|
+
pendingRef.current = null;
|
|
101
|
+
updateRef.current(pending ? { ...pending, ...next } : next);
|
|
102
|
+
},
|
|
103
|
+
[],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const patchDebounced = useCallback(
|
|
107
|
+
(next: Partial<T>, ms: number = DEFAULT_DEBOUNCE_MS) => {
|
|
108
|
+
// Merge so partial fields accumulate across rapid calls.
|
|
109
|
+
pendingRef.current = { ...(pendingRef.current ?? {}), ...next };
|
|
110
|
+
if (timerRef.current != null) clearTimeout(timerRef.current);
|
|
111
|
+
timerRef.current = setTimeout(() => {
|
|
112
|
+
timerRef.current = null;
|
|
113
|
+
const pending = pendingRef.current;
|
|
114
|
+
pendingRef.current = null;
|
|
115
|
+
if (pending) updateRef.current(pending);
|
|
116
|
+
}, ms);
|
|
117
|
+
},
|
|
118
|
+
[],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Never drop the last edit: flush on unmount.
|
|
122
|
+
useEffect(() => () => flush(), [flush]);
|
|
123
|
+
|
|
124
|
+
return useMemo(
|
|
125
|
+
() => ({ attrs, patch, patchDebounced, flush }),
|
|
126
|
+
[attrs, patch, patchDebounced, flush],
|
|
127
|
+
);
|
|
128
|
+
}
|