@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.
Files changed (66) hide show
  1. package/dist/audio-player/index.cjs +2098 -0
  2. package/dist/audio-player/index.cjs.map +1 -0
  3. package/dist/audio-player/index.css +65 -0
  4. package/dist/audio-player/index.css.map +1 -0
  5. package/dist/audio-player/index.d.cts +166 -0
  6. package/dist/audio-player/index.d.ts +166 -0
  7. package/dist/audio-player/index.mjs +2075 -0
  8. package/dist/audio-player/index.mjs.map +1 -0
  9. package/dist/composer-registry/index.cjs +45 -0
  10. package/dist/composer-registry/index.cjs.map +1 -0
  11. package/dist/composer-registry/index.d.cts +73 -0
  12. package/dist/composer-registry/index.d.ts +73 -0
  13. package/dist/composer-registry/index.mjs +39 -0
  14. package/dist/composer-registry/index.mjs.map +1 -0
  15. package/dist/tree/index.cjs +82 -63
  16. package/dist/tree/index.cjs.map +1 -1
  17. package/dist/tree/index.d.cts +15 -1
  18. package/dist/tree/index.d.ts +15 -1
  19. package/dist/tree/index.mjs +83 -64
  20. package/dist/tree/index.mjs.map +1 -1
  21. package/package.json +38 -17
  22. package/src/tools/chat/composer/Composer.tsx +8 -8
  23. package/src/tools/chat/context/ChatProvider.tsx +13 -78
  24. package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
  25. package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
  26. package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
  27. package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
  28. package/src/tools/data/Tree/TreeRoot.tsx +33 -109
  29. package/src/tools/data/Tree/context/TreeContext.tsx +22 -3
  30. package/src/tools/data/Tree/context/menu/index.ts +1 -0
  31. package/src/tools/data/Tree/context/menu/render.tsx +75 -0
  32. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +16 -2
  33. package/src/tools/data/Tree/index.tsx +1 -0
  34. package/src/tools/data/Tree/types/index.ts +1 -1
  35. package/src/tools/data/Tree/types/root-props.ts +16 -0
  36. package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
  37. package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
  38. package/src/tools/forms/CodeEditor/components/Editor.tsx +19 -0
  39. package/src/tools/forms/CodeEditor/types/index.ts +7 -0
  40. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +40 -0
  41. package/src/tools/forms/MarkdownEditor/styles.css +174 -21
  42. package/src/tools/forms/NotionEditor/CustomKeymap.ts +48 -0
  43. package/src/tools/forms/NotionEditor/LinkDialog.tsx +133 -0
  44. package/src/tools/forms/NotionEditor/NotionEditor.tsx +304 -0
  45. package/src/tools/forms/NotionEditor/SlashExtension.ts +32 -0
  46. package/src/tools/forms/NotionEditor/SlashList.tsx +136 -0
  47. package/src/tools/forms/NotionEditor/TaskItemView.tsx +41 -0
  48. package/src/tools/forms/NotionEditor/createSlashSuggestion.ts +121 -0
  49. package/src/tools/forms/NotionEditor/extensions.ts +105 -0
  50. package/src/tools/forms/NotionEditor/index.ts +1 -0
  51. package/src/tools/forms/NotionEditor/lazy.tsx +44 -0
  52. package/src/tools/forms/NotionEditor/slashItems.ts +159 -0
  53. package/src/tools/forms/NotionEditor/styles.css +478 -0
  54. package/src/tools/forms/NotionEditor/types.ts +28 -0
  55. package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
  56. package/src/tools/integration/ComposerRegistry/index.ts +105 -0
  57. package/src/tools/media/AudioPlayer/Player.tsx +2 -0
  58. package/src/tools/media/AudioPlayer/PlayerShell.tsx +29 -22
  59. package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
  60. package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
  61. package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
  62. package/src/tools/media/AudioPlayer/types.ts +8 -0
  63. package/src/tools/media/ImageViewer/components/ImageViewer.tsx +8 -0
  64. package/src/tools/media/ImageViewer/types.ts +4 -0
  65. package/src/tools/media/VideoPlayer/VideoPlayer.tsx +20 -1
  66. 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
- renderContextMenu,
393
+ finalRenderContextMenu,
375
394
  adapter,
376
395
  resolvedContextMenuActions,
377
396
  nodeById,
@@ -8,3 +8,4 @@ export {
8
8
  export type { BuiltinActionContext } from './builtin-actions';
9
9
  export { useResolvedMenu } from './use-resolved-menu';
10
10
  export type { UseResolvedMenuOptions } from './use-resolved-menu';
11
+ export { renderItemsAsContextMenu, tidyMenuItems } from './render';
@@ -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
- ): TreeContextMenuActionsResolver<T> | undefined {
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<TreeContextMenuActionsResolver<T> | undefined>(() => {
84
+ return useMemo<ResolvedMenuResolver<T> | undefined>(() => {
71
85
  if (!adapter && !contextMenuActions) return undefined;
72
86
 
73
87
  return (rowProps) => {
@@ -105,6 +105,7 @@ export type {
105
105
 
106
106
  export { DEFAULT_TREE_LABELS } from './types';
107
107
  export type {
108
+ TreeActionsHandle,
108
109
  TreeRootProps,
109
110
  TreeNode,
110
111
  TreeItemId,
@@ -24,4 +24,4 @@ export type {
24
24
  TreeAdapter,
25
25
  TreeBuiltinAction,
26
26
  } from './adapter';
27
- export type { TreeRootProps } from './root-props';
27
+ export type { TreeRootProps, TreeActionsHandle } from './root-props';
@@ -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
+ }
@@ -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
- <SafeTooltipProvider delayDuration={200}>
123
- <div className="flex items-center gap-0.5">
124
- <IconButton label={linkLabel} onClick={copyLink} active={linkCopied}>
125
- {linkIcon}
126
- </IconButton>
127
- {toggleAllNode}
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, TooltipProvider } from '@djangocfg/ui-core/components';
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
- <TooltipProvider delayDuration={350}>
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
- /* Inherit semantic foreground so the editor renders correctly in both
6
- light + dark themes (and under any active preset). Without this the
7
- browser falls back to UA black on a token-driven background. */
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.5em;
43
+ font-size: 1.875em;
21
44
  font-weight: 700;
22
- margin: 0.5em 0 0.25em;
23
- line-height: 1.3;
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.25em;
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
- margin: 0.4em 0 0.2em;
37
- line-height: 1.3;
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.25em 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.1em 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-left: 1em;
65
- margin: 0.5em 0;
66
- opacity: 0.8;
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: 0.75em 0;
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-muted-foreground, var(--muted-foreground));
78
- padding: 0.15em 0.3em;
79
- border-radius: 0.25em;
80
- font-size: 0.9em;
81
- font-family: monospace;
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;