@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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
DEFAULT_TREE_LABELS,
|
|
11
11
|
type FlatRow,
|
|
12
12
|
type TreeActivateOptions,
|
|
13
|
+
type TreeContextMenuSlot,
|
|
13
14
|
type TreeItemId,
|
|
14
15
|
type TreeLabels,
|
|
15
16
|
type TreeNode,
|
|
@@ -22,7 +23,7 @@ import { useExpansion } from './expansion';
|
|
|
22
23
|
import { useSelection } from './selection';
|
|
23
24
|
import { useRename } from './rename';
|
|
24
25
|
import { useClipboard } from './clipboard';
|
|
25
|
-
import { useResolvedMenu } from './menu';
|
|
26
|
+
import { useResolvedMenu, renderItemsAsContextMenu, tidyMenuItems } from './menu';
|
|
26
27
|
import { useDnd, type UseDndReturn } from './dnd';
|
|
27
28
|
import { usePersistSync } from './persist';
|
|
28
29
|
import type { TreeContextValue } from './TreeContextValue';
|
|
@@ -282,6 +283,24 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
282
283
|
pasteFromClipboard: clipboard.pasteFromClipboard,
|
|
283
284
|
});
|
|
284
285
|
|
|
286
|
+
// Translate the declarative resolver into a slot-form
|
|
287
|
+
// `renderContextMenu` so <TreeRow> doesn't need to know about it.
|
|
288
|
+
// Explicit slot prop wins (escape-hatch for full custom menus).
|
|
289
|
+
const finalRenderContextMenu = useMemo<TreeContextMenuSlot<T> | undefined>(
|
|
290
|
+
() => {
|
|
291
|
+
if (renderContextMenu) return renderContextMenu;
|
|
292
|
+
const resolve = resolvedContextMenuActions;
|
|
293
|
+
if (!resolve) return undefined;
|
|
294
|
+
return (rowProps, trigger) => {
|
|
295
|
+
const items = resolve(rowProps);
|
|
296
|
+
const cleaned = items ? tidyMenuItems(items) : null;
|
|
297
|
+
if (!cleaned || cleaned.length === 0) return trigger;
|
|
298
|
+
return renderItemsAsContextMenu(rowProps, cleaned, trigger);
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
[renderContextMenu, resolvedContextMenuActions],
|
|
302
|
+
);
|
|
303
|
+
|
|
285
304
|
// ---- Final value --------------------------------------------------
|
|
286
305
|
|
|
287
306
|
const value = useMemo<TreeContextValue<T>>(
|
|
@@ -333,7 +352,7 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
333
352
|
renderIcon,
|
|
334
353
|
renderLabel,
|
|
335
354
|
renderActions,
|
|
336
|
-
renderContextMenu,
|
|
355
|
+
renderContextMenu: finalRenderContextMenu,
|
|
337
356
|
|
|
338
357
|
adapter,
|
|
339
358
|
resolvedContextMenuActions,
|
|
@@ -371,7 +390,7 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
371
390
|
renderIcon,
|
|
372
391
|
renderLabel,
|
|
373
392
|
renderActions,
|
|
374
|
-
|
|
393
|
+
finalRenderContextMenu,
|
|
375
394
|
adapter,
|
|
376
395
|
resolvedContextMenuActions,
|
|
377
396
|
nodeById,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ContextMenu,
|
|
5
|
+
ContextMenuContent,
|
|
6
|
+
ContextMenuItem,
|
|
7
|
+
ContextMenuSeparator,
|
|
8
|
+
ContextMenuShortcut,
|
|
9
|
+
ContextMenuTrigger,
|
|
10
|
+
} from '@djangocfg/ui-core/components';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
TreeContextMenuItem,
|
|
14
|
+
TreeRowRenderProps,
|
|
15
|
+
} from '../../types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render an array of declarative menu items as a themed `<ContextMenu>`
|
|
19
|
+
* wrapped around the supplied trigger element. Pure presentational layer
|
|
20
|
+
* — the caller resolves and merges items.
|
|
21
|
+
*/
|
|
22
|
+
export function renderItemsAsContextMenu<T>(
|
|
23
|
+
rowProps: TreeRowRenderProps<T>,
|
|
24
|
+
items: TreeContextMenuItem<T>[],
|
|
25
|
+
trigger: React.ReactNode,
|
|
26
|
+
): React.ReactNode {
|
|
27
|
+
return (
|
|
28
|
+
<ContextMenu>
|
|
29
|
+
<ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
|
|
30
|
+
<ContextMenuContent>
|
|
31
|
+
{items.map((item, idx) => {
|
|
32
|
+
if (item === 'separator') {
|
|
33
|
+
return <ContextMenuSeparator key={`sep-${idx}`} />;
|
|
34
|
+
}
|
|
35
|
+
const Icon = item.icon;
|
|
36
|
+
return (
|
|
37
|
+
<ContextMenuItem
|
|
38
|
+
key={item.id}
|
|
39
|
+
disabled={item.disabled}
|
|
40
|
+
variant={item.destructive ? 'destructive' : undefined}
|
|
41
|
+
onSelect={() => item.onSelect(rowProps)}
|
|
42
|
+
>
|
|
43
|
+
{Icon ? <Icon /> : null}
|
|
44
|
+
{item.label}
|
|
45
|
+
{item.shortcut ? (
|
|
46
|
+
<ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>
|
|
47
|
+
) : null}
|
|
48
|
+
</ContextMenuItem>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</ContextMenuContent>
|
|
52
|
+
</ContextMenu>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Drop trailing / leading / duplicate separators so a merged menu never
|
|
58
|
+
* shows a separator next to a section header or another separator.
|
|
59
|
+
*/
|
|
60
|
+
export function tidyMenuItems<T>(
|
|
61
|
+
items: TreeContextMenuItem<T>[],
|
|
62
|
+
): TreeContextMenuItem<T>[] {
|
|
63
|
+
const out: TreeContextMenuItem<T>[] = [];
|
|
64
|
+
for (const it of items) {
|
|
65
|
+
if (it === 'separator') {
|
|
66
|
+
if (out.length === 0) continue;
|
|
67
|
+
if (out[out.length - 1] === 'separator') continue;
|
|
68
|
+
out.push(it);
|
|
69
|
+
} else {
|
|
70
|
+
out.push(it);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
while (out.length > 0 && out[out.length - 1] === 'separator') out.pop();
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
@@ -7,16 +7,30 @@ import type {
|
|
|
7
7
|
TreeAdapter,
|
|
8
8
|
TreeBuiltinAction,
|
|
9
9
|
TreeContextMenuActionsResolver,
|
|
10
|
+
TreeContextMenuItem,
|
|
10
11
|
TreeItemId,
|
|
11
12
|
TreeLabels,
|
|
12
13
|
TreeMovePosition,
|
|
13
14
|
TreeNode,
|
|
15
|
+
TreeRowRenderProps,
|
|
14
16
|
} from '../../types';
|
|
17
|
+
|
|
18
|
+
export type { TreeContextMenuActionsResolver };
|
|
15
19
|
import {
|
|
16
20
|
buildDefaultMenuItems,
|
|
17
21
|
type BuiltinActionContext,
|
|
18
22
|
} from './builtin-actions';
|
|
19
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Internal row-driven resolver — same shape as
|
|
26
|
+
* `TreeContextMenuActionsResolver` but takes a plain `TreeRowRenderProps`
|
|
27
|
+
* instead of the context-with-selection. The provider injects
|
|
28
|
+
* `selectedNodes` itself.
|
|
29
|
+
*/
|
|
30
|
+
export type ResolvedMenuResolver<T> = (
|
|
31
|
+
row: TreeRowRenderProps<T>,
|
|
32
|
+
) => TreeContextMenuItem<T>[] | null | undefined;
|
|
33
|
+
|
|
20
34
|
export interface UseResolvedMenuOptions<T> {
|
|
21
35
|
adapter?: TreeAdapter<T>;
|
|
22
36
|
contextMenuActions?: TreeContextMenuActionsResolver<T>;
|
|
@@ -50,7 +64,7 @@ export interface UseResolvedMenuOptions<T> {
|
|
|
50
64
|
*/
|
|
51
65
|
export function useResolvedMenu<T>(
|
|
52
66
|
opts: UseResolvedMenuOptions<T>,
|
|
53
|
-
):
|
|
67
|
+
): ResolvedMenuResolver<T> | undefined {
|
|
54
68
|
const {
|
|
55
69
|
adapter,
|
|
56
70
|
contextMenuActions,
|
|
@@ -67,7 +81,7 @@ export function useResolvedMenu<T>(
|
|
|
67
81
|
pasteFromClipboard,
|
|
68
82
|
} = opts;
|
|
69
83
|
|
|
70
|
-
return useMemo<
|
|
84
|
+
return useMemo<ResolvedMenuResolver<T> | undefined>(() => {
|
|
71
85
|
if (!adapter && !contextMenuActions) return undefined;
|
|
72
86
|
|
|
73
87
|
return (rowProps) => {
|
|
@@ -137,6 +137,22 @@ export interface TreeRootProps<T> {
|
|
|
137
137
|
*/
|
|
138
138
|
defaultMenuItems?: TreeBuiltinAction[];
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Imperative handle for outer code. The provided ref receives a
|
|
142
|
+
* stable handle to `useTreeActions` once Tree mounts. Lets host
|
|
143
|
+
* components trigger `refresh(id)` / `refreshAll()` from outside
|
|
144
|
+
* Tree (e.g. after a transport-level mutation completes).
|
|
145
|
+
*/
|
|
146
|
+
actionsRef?: React.MutableRefObject<TreeActionsHandle | null>;
|
|
147
|
+
|
|
140
148
|
className?: string;
|
|
141
149
|
style?: CSSProperties;
|
|
142
150
|
}
|
|
151
|
+
|
|
152
|
+
/** Subset of `useTreeActions()` exposed via `<TreeRoot actionsRef={…}>`. */
|
|
153
|
+
export interface TreeActionsHandle {
|
|
154
|
+
refresh: (id: string) => Promise<void>;
|
|
155
|
+
refreshAll: () => Promise<void>;
|
|
156
|
+
expandAll: () => void;
|
|
157
|
+
collapseAll: () => void;
|
|
158
|
+
}
|
package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
Tooltip,
|
|
13
13
|
TooltipContent,
|
|
14
14
|
TooltipTrigger,
|
|
15
|
-
SafeTooltipProvider,
|
|
16
15
|
} from '@djangocfg/ui-core/components';
|
|
17
16
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
18
17
|
|
|
@@ -119,13 +118,11 @@ export function MetaActions({ anchor, presentSections }: MetaActionsProps) {
|
|
|
119
118
|
) : null;
|
|
120
119
|
|
|
121
120
|
return (
|
|
122
|
-
<
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</div>
|
|
129
|
-
</SafeTooltipProvider>
|
|
121
|
+
<div className="flex items-center gap-0.5">
|
|
122
|
+
<IconButton label={linkLabel} onClick={copyLink} active={linkCopied}>
|
|
123
|
+
{linkIcon}
|
|
124
|
+
</IconButton>
|
|
125
|
+
{toggleAllNode}
|
|
126
|
+
</div>
|
|
130
127
|
);
|
|
131
128
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { keyBy } from 'lodash-es';
|
|
5
5
|
|
|
6
|
-
import { Skeleton
|
|
6
|
+
import { Skeleton } from '@djangocfg/ui-core/components';
|
|
7
7
|
import { useMediaQuery } from '@djangocfg/ui-core/hooks';
|
|
8
8
|
|
|
9
9
|
import useOpenApiSchema from '../../hooks/useOpenApiSchema';
|
|
@@ -234,8 +234,7 @@ export const DocsLayout: React.FC = () => {
|
|
|
234
234
|
// ─── Desktop ──────────────────────────────────────────────────────────
|
|
235
235
|
|
|
236
236
|
return (
|
|
237
|
-
<
|
|
238
|
-
<div className="grid grid-cols-[260px_minmax(0,1fr)] items-start">
|
|
237
|
+
<div className="grid grid-cols-[260px_minmax(0,1fr)] items-start">
|
|
239
238
|
<EndpointDraftSync schemaId={currentSchema?.id ?? null} />
|
|
240
239
|
<div
|
|
241
240
|
className="sticky top-[var(--navbar-height,64px)]"
|
|
@@ -283,6 +282,5 @@ export const DocsLayout: React.FC = () => {
|
|
|
283
282
|
above the whole layout (sidebar + navbar included). */}
|
|
284
283
|
<SlideInPlayground open={slideOpen} onClose={handleCloseSlide} />
|
|
285
284
|
</div>
|
|
286
|
-
</TooltipProvider>
|
|
287
285
|
);
|
|
288
286
|
};
|
|
@@ -46,6 +46,8 @@ export const Editor = forwardRef<EditorRef, EditorProps>(function Editor(
|
|
|
46
46
|
autoHeight = false,
|
|
47
47
|
minHeight = 100,
|
|
48
48
|
maxHeight = 600,
|
|
49
|
+
autoFocus = false,
|
|
50
|
+
onSave,
|
|
49
51
|
},
|
|
50
52
|
ref
|
|
51
53
|
) {
|
|
@@ -67,9 +69,11 @@ export const Editor = forwardRef<EditorRef, EditorProps>(function Editor(
|
|
|
67
69
|
// without going stale when the parent passes new function identities.
|
|
68
70
|
const onChangeRef = useRef(onChange);
|
|
69
71
|
const onMountRef = useRef(onMount);
|
|
72
|
+
const onSaveRef = useRef(onSave);
|
|
70
73
|
useEffect(() => {
|
|
71
74
|
onChangeRef.current = onChange;
|
|
72
75
|
onMountRef.current = onMount;
|
|
76
|
+
onSaveRef.current = onSave;
|
|
73
77
|
});
|
|
74
78
|
|
|
75
79
|
// Expose editor methods via ref
|
|
@@ -126,6 +130,21 @@ export const Editor = forwardRef<EditorRef, EditorProps>(function Editor(
|
|
|
126
130
|
updateContentHeight(editor);
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
// Cmd/Ctrl+S → save. Registered as a Monaco command so it wins over
|
|
134
|
+
// the browser's "save page" default whenever the editor has focus.
|
|
135
|
+
// Read through the ref so swapping handlers does not need to rebuild.
|
|
136
|
+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
|
137
|
+
onSaveRef.current?.(editor.getValue());
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// autoFocus on first mount — Monaco refuses focus during layout
|
|
141
|
+
// measurement otherwise.
|
|
142
|
+
if (autoFocus) {
|
|
143
|
+
// queueMicrotask: defer past Monaco's own post-create layout so the
|
|
144
|
+
// focus call lands on a fully laid-out editor.
|
|
145
|
+
queueMicrotask(() => editorRef.current?.focus());
|
|
146
|
+
}
|
|
147
|
+
|
|
129
148
|
// Call onMount callback
|
|
130
149
|
onMountRef.current?.(editor);
|
|
131
150
|
|
|
@@ -61,6 +61,13 @@ export interface EditorProps {
|
|
|
61
61
|
minHeight?: number;
|
|
62
62
|
/** Max height in px when autoHeight is enabled (default: 600) */
|
|
63
63
|
maxHeight?: number;
|
|
64
|
+
/** Focus the editor once Monaco mounts. Pair with `key={path}` upstream
|
|
65
|
+
* for per-file focus reset. */
|
|
66
|
+
autoFocus?: boolean;
|
|
67
|
+
/** Bound to Cmd/Ctrl+S inside the editor via Monaco's command palette.
|
|
68
|
+
* Receives the current value. The browser default is suppressed by
|
|
69
|
+
* Monaco when a command is registered for that chord. */
|
|
70
|
+
onSave?: (value: string) => void;
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
export interface DiffEditorProps {
|
|
@@ -7,6 +7,7 @@ import Mention from '@tiptap/extension-mention';
|
|
|
7
7
|
import { Markdown } from '@tiptap/markdown';
|
|
8
8
|
import type { AnyExtension } from '@tiptap/core';
|
|
9
9
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
|
|
10
|
+
import { useHotkey } from '@djangocfg/ui-core/hooks';
|
|
10
11
|
import {
|
|
11
12
|
Bold, Italic, Strikethrough, Heading1, Heading2, Heading3,
|
|
12
13
|
List, ListOrdered, Quote, Minus, Code, type LucideIcon,
|
|
@@ -125,6 +126,17 @@ export interface MarkdownEditorProps {
|
|
|
125
126
|
* empty draft".
|
|
126
127
|
*/
|
|
127
128
|
onSubmit?: () => boolean | void;
|
|
129
|
+
/**
|
|
130
|
+
* Focus the editor on mount. Pair with `key={file}` upstream when the
|
|
131
|
+
* host wants a fresh focus per file change (inspector / editor tab).
|
|
132
|
+
*/
|
|
133
|
+
autoFocus?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Called when the user presses Cmd/Ctrl+S inside the editor. Receives
|
|
136
|
+
* the current markdown. The browser's "save page" default is suppressed
|
|
137
|
+
* only when this handler is supplied — otherwise Cmd+S falls through.
|
|
138
|
+
*/
|
|
139
|
+
onSave?: (markdown: string) => void;
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
/**
|
|
@@ -159,6 +171,8 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
159
171
|
slashCommands,
|
|
160
172
|
onMentionIdsChange,
|
|
161
173
|
onSubmit,
|
|
174
|
+
autoFocus = false,
|
|
175
|
+
onSave,
|
|
162
176
|
},
|
|
163
177
|
ref,
|
|
164
178
|
) {
|
|
@@ -380,6 +394,32 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
380
394
|
editor?.setEditable(!disabled);
|
|
381
395
|
}, [editor, disabled]);
|
|
382
396
|
|
|
397
|
+
// Declarative autoFocus — runs once the editor instance exists. Hosts
|
|
398
|
+
// that want per-file focus reset should pair this with `key={path}`.
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
if (!autoFocus || !editor) return;
|
|
401
|
+
editor.commands.focus('end');
|
|
402
|
+
}, [autoFocus, editor]);
|
|
403
|
+
|
|
404
|
+
// Cmd/Ctrl+S → save. Uses the shared `useHotkey` (inInput=true by
|
|
405
|
+
// default for modifier combos). Guarded to fire only when focus is
|
|
406
|
+
// inside this editor's ProseMirror DOM, so multiple sibling editors
|
|
407
|
+
// don't all fire on one chord.
|
|
408
|
+
const onSaveRef = useRef(onSave);
|
|
409
|
+
onSaveRef.current = onSave;
|
|
410
|
+
useHotkey(
|
|
411
|
+
'mod+s',
|
|
412
|
+
() => {
|
|
413
|
+
const h = onSaveRef.current;
|
|
414
|
+
if (!h || !editor) return;
|
|
415
|
+
const dom = editor.view.dom;
|
|
416
|
+
const active = document.activeElement;
|
|
417
|
+
if (!active || !dom.contains(active)) return;
|
|
418
|
+
h(getMarkdown(editor));
|
|
419
|
+
},
|
|
420
|
+
{ enabled: !!editor && !!onSave },
|
|
421
|
+
);
|
|
422
|
+
|
|
383
423
|
// Imperative API for hosts that drive the editor without owning a
|
|
384
424
|
// TipTap ref directly — chat composer registration, voice slot,
|
|
385
425
|
// focus-on-stream-end.
|
|
@@ -2,49 +2,88 @@
|
|
|
2
2
|
outline: none;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/* ─────────────────────────────────────────────────────────────────────
|
|
6
|
+
* Typography — Notion-inspired: comfy line-height, generous heading
|
|
7
|
+
* scale, restrained spacing. Calibrated so the chat composer (compact,
|
|
8
|
+
* single-paragraph) and the document-preview editor (long-form, with
|
|
9
|
+
* many headings) both feel right without a variant prop.
|
|
10
|
+
*
|
|
11
|
+
* Inherit semantic foreground so the editor renders correctly in both
|
|
12
|
+
* light + dark themes (and under any active preset).
|
|
13
|
+
* ───────────────────────────────────────────────────────────────────── */
|
|
8
14
|
.markdown-editor .tiptap,
|
|
9
15
|
.markdown-editor .ProseMirror {
|
|
10
16
|
color: var(--color-foreground, var(--foreground));
|
|
17
|
+
font-family:
|
|
18
|
+
-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto,
|
|
19
|
+
"Helvetica Neue", Arial, sans-serif;
|
|
20
|
+
font-size: 15px;
|
|
21
|
+
line-height: 1.6;
|
|
22
|
+
-webkit-font-smoothing: antialiased;
|
|
23
|
+
-moz-osx-font-smoothing: grayscale;
|
|
24
|
+
text-rendering: optimizeLegibility;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
.markdown-editor .tiptap a,
|
|
14
28
|
.markdown-editor .ProseMirror a {
|
|
15
29
|
color: var(--color-primary, var(--primary));
|
|
16
30
|
text-decoration: underline;
|
|
31
|
+
text-underline-offset: 2px;
|
|
32
|
+
text-decoration-thickness: 1px;
|
|
17
33
|
}
|
|
18
34
|
|
|
35
|
+
.markdown-editor .tiptap a:hover,
|
|
36
|
+
.markdown-editor .ProseMirror a:hover {
|
|
37
|
+
text-decoration-thickness: 2px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Headings — Notion scale. Top-padding > bottom so a heading sits
|
|
41
|
+
* visually grouped with the paragraph below it. */
|
|
19
42
|
.markdown-editor .tiptap h1 {
|
|
20
|
-
font-size: 1.
|
|
43
|
+
font-size: 1.875em;
|
|
21
44
|
font-weight: 700;
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
line-height: 1.25;
|
|
46
|
+
margin: 1em 0 0.25em;
|
|
47
|
+
letter-spacing: -0.015em;
|
|
24
48
|
}
|
|
25
49
|
|
|
26
50
|
.markdown-editor .tiptap h2 {
|
|
27
|
-
font-size: 1.
|
|
51
|
+
font-size: 1.5em;
|
|
28
52
|
font-weight: 600;
|
|
29
|
-
margin: 0.5em 0 0.25em;
|
|
30
53
|
line-height: 1.3;
|
|
54
|
+
margin: 0.9em 0 0.25em;
|
|
55
|
+
letter-spacing: -0.01em;
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
.markdown-editor .tiptap h3 {
|
|
59
|
+
font-size: 1.25em;
|
|
60
|
+
font-weight: 600;
|
|
61
|
+
line-height: 1.35;
|
|
62
|
+
margin: 0.8em 0 0.2em;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.markdown-editor .tiptap h4 {
|
|
34
66
|
font-size: 1.1em;
|
|
35
67
|
font-weight: 600;
|
|
36
|
-
|
|
37
|
-
|
|
68
|
+
line-height: 1.4;
|
|
69
|
+
margin: 0.7em 0 0.2em;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* First heading at the very top of the document has no leading air —
|
|
73
|
+
* Notion convention. */
|
|
74
|
+
.markdown-editor .tiptap > :is(h1, h2, h3, h4):first-child {
|
|
75
|
+
margin-top: 0;
|
|
38
76
|
}
|
|
39
77
|
|
|
40
78
|
.markdown-editor .tiptap p {
|
|
41
79
|
margin: 0.25em 0;
|
|
42
80
|
}
|
|
43
81
|
|
|
82
|
+
/* Lists — Notion uses tighter row spacing than browser default. */
|
|
44
83
|
.markdown-editor .tiptap ul,
|
|
45
84
|
.markdown-editor .tiptap ol {
|
|
46
85
|
padding-left: 1.5em;
|
|
47
|
-
margin: 0.
|
|
86
|
+
margin: 0.35em 0;
|
|
48
87
|
}
|
|
49
88
|
|
|
50
89
|
.markdown-editor .tiptap ul {
|
|
@@ -56,29 +95,138 @@
|
|
|
56
95
|
}
|
|
57
96
|
|
|
58
97
|
.markdown-editor .tiptap li {
|
|
59
|
-
margin: 0.
|
|
98
|
+
margin: 0.15em 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.markdown-editor .tiptap li > p {
|
|
102
|
+
margin: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Nested lists get a subtler marker. */
|
|
106
|
+
.markdown-editor .tiptap ul ul {
|
|
107
|
+
list-style: circle;
|
|
108
|
+
}
|
|
109
|
+
.markdown-editor .tiptap ul ul ul {
|
|
110
|
+
list-style: square;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Task list (GFM `- [ ] item`). StarterKit ships TaskList extension. */
|
|
114
|
+
.markdown-editor .tiptap ul[data-type="taskList"] {
|
|
115
|
+
list-style: none;
|
|
116
|
+
padding-left: 0.25em;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.markdown-editor .tiptap ul[data-type="taskList"] li {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: flex-start;
|
|
122
|
+
gap: 0.5em;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.markdown-editor .tiptap ul[data-type="taskList"] li > label {
|
|
126
|
+
flex-shrink: 0;
|
|
127
|
+
margin-top: 0.3em;
|
|
128
|
+
user-select: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.markdown-editor .tiptap ul[data-type="taskList"] li > div {
|
|
132
|
+
flex: 1;
|
|
133
|
+
min-width: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.markdown-editor .tiptap ul[data-type="taskList"] li[data-checked="true"] > div {
|
|
137
|
+
opacity: 0.55;
|
|
138
|
+
text-decoration: line-through;
|
|
60
139
|
}
|
|
61
140
|
|
|
141
|
+
/* Blockquote — Notion uses a chunky left bar without italic. */
|
|
62
142
|
.markdown-editor .tiptap blockquote {
|
|
63
143
|
border-left: 3px solid var(--color-border, var(--border));
|
|
64
|
-
padding
|
|
65
|
-
margin: 0.
|
|
66
|
-
|
|
144
|
+
padding: 0.2em 0 0.2em 1em;
|
|
145
|
+
margin: 0.6em 0;
|
|
146
|
+
color: color-mix(in oklab, var(--color-foreground, var(--foreground)) 80%, transparent);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.markdown-editor .tiptap blockquote > :first-child {
|
|
150
|
+
margin-top: 0;
|
|
151
|
+
}
|
|
152
|
+
.markdown-editor .tiptap blockquote > :last-child {
|
|
153
|
+
margin-bottom: 0;
|
|
67
154
|
}
|
|
68
155
|
|
|
156
|
+
/* Divider — full-width thin rule with breathing room. */
|
|
69
157
|
.markdown-editor .tiptap hr {
|
|
70
158
|
border: none;
|
|
71
159
|
border-top: 1px solid var(--color-border, var(--border));
|
|
72
|
-
margin:
|
|
160
|
+
margin: 1.5em 0;
|
|
73
161
|
}
|
|
74
162
|
|
|
163
|
+
/* Inline code — monospace pill, restrained tint. */
|
|
75
164
|
.markdown-editor .tiptap code {
|
|
165
|
+
background: color-mix(in oklab, var(--color-muted, var(--muted)) 70%, transparent);
|
|
166
|
+
color: var(--color-foreground, var(--foreground));
|
|
167
|
+
padding: 0.15em 0.35em;
|
|
168
|
+
border-radius: 4px;
|
|
169
|
+
font-size: 0.875em;
|
|
170
|
+
font-family:
|
|
171
|
+
"SF Mono", ui-monospace, "JetBrains Mono", "Fira Code", Menlo, Consolas,
|
|
172
|
+
monospace;
|
|
173
|
+
border: 1px solid color-mix(in oklab, var(--color-border, var(--border)) 60%, transparent);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Code block — darker surface so it stands apart from inline code. */
|
|
177
|
+
.markdown-editor .tiptap pre {
|
|
76
178
|
background: var(--color-muted, var(--muted));
|
|
77
|
-
color: var(--color-
|
|
78
|
-
padding: 0.
|
|
79
|
-
border-radius:
|
|
80
|
-
|
|
81
|
-
font-family:
|
|
179
|
+
color: var(--color-foreground, var(--foreground));
|
|
180
|
+
padding: 0.9em 1em;
|
|
181
|
+
border-radius: 6px;
|
|
182
|
+
margin: 0.75em 0;
|
|
183
|
+
font-family:
|
|
184
|
+
"SF Mono", ui-monospace, "JetBrains Mono", "Fira Code", Menlo, Consolas,
|
|
185
|
+
monospace;
|
|
186
|
+
font-size: 0.875em;
|
|
187
|
+
line-height: 1.55;
|
|
188
|
+
overflow-x: auto;
|
|
189
|
+
border: 1px solid color-mix(in oklab, var(--color-border, var(--border)) 50%, transparent);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.markdown-editor .tiptap pre code {
|
|
193
|
+
background: transparent;
|
|
194
|
+
border: none;
|
|
195
|
+
padding: 0;
|
|
196
|
+
border-radius: 0;
|
|
197
|
+
font-size: inherit;
|
|
198
|
+
color: inherit;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Tables — borderless, subtle row separators (Notion-flavour). */
|
|
202
|
+
.markdown-editor .tiptap table {
|
|
203
|
+
border-collapse: collapse;
|
|
204
|
+
width: 100%;
|
|
205
|
+
margin: 0.75em 0;
|
|
206
|
+
font-size: 0.95em;
|
|
207
|
+
table-layout: fixed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.markdown-editor .tiptap th,
|
|
211
|
+
.markdown-editor .tiptap td {
|
|
212
|
+
border: 1px solid var(--color-border, var(--border));
|
|
213
|
+
padding: 0.45em 0.75em;
|
|
214
|
+
vertical-align: top;
|
|
215
|
+
text-align: left;
|
|
216
|
+
min-width: 4ch;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.markdown-editor .tiptap th {
|
|
220
|
+
background: color-mix(in oklab, var(--color-muted, var(--muted)) 60%, transparent);
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Images — rounded, never overflow. */
|
|
225
|
+
.markdown-editor .tiptap img {
|
|
226
|
+
max-width: 100%;
|
|
227
|
+
height: auto;
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
margin: 0.5em 0;
|
|
82
230
|
}
|
|
83
231
|
|
|
84
232
|
.markdown-editor .tiptap strong {
|
|
@@ -93,6 +241,11 @@
|
|
|
93
241
|
text-decoration: line-through;
|
|
94
242
|
}
|
|
95
243
|
|
|
244
|
+
/* Selection highlight — semantic ring colour, not browser-blue. */
|
|
245
|
+
.markdown-editor .tiptap ::selection {
|
|
246
|
+
background: color-mix(in oklab, var(--color-primary, var(--primary)) 25%, transparent);
|
|
247
|
+
}
|
|
248
|
+
|
|
96
249
|
.markdown-editor .tiptap p.is-editor-empty:first-child::before {
|
|
97
250
|
content: attr(data-placeholder);
|
|
98
251
|
float: left;
|