@olddevs/ui 4.4.0 → 4.5.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/CHANGELOG.md +19 -0
- package/dist/editor/RichTextEditor.d.ts +11 -4
- package/dist/editor/RichTextEditor.d.ts.map +1 -1
- package/dist/editor/RichTextEditor.js +90 -60
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/editor/RichTextView.d.ts +2 -1
- package/dist/editor/RichTextView.d.ts.map +1 -1
- package/dist/editor/RichTextView.js.map +1 -1
- package/dist/editor/core/containers.d.ts +2 -6
- package/dist/editor/core/containers.d.ts.map +1 -1
- package/dist/editor/core/containers.js +2 -1
- package/dist/editor/core/containers.js.map +1 -1
- package/dist/editor/core/customTool.d.ts +8 -2
- package/dist/editor/core/customTool.d.ts.map +1 -1
- package/dist/editor/core/customTool.js.map +1 -1
- package/dist/editor/core/customToolEngine.d.ts +9 -1
- package/dist/editor/core/customToolEngine.d.ts.map +1 -1
- package/dist/editor/core/customToolEngine.js +12 -0
- package/dist/editor/core/customToolEngine.js.map +1 -1
- package/dist/editor/core/extensions.d.ts.map +1 -1
- package/dist/editor/core/extensions.js +1 -1
- package/dist/editor/core/extensions.js.map +1 -1
- package/dist/editor/core/handle.d.ts +18 -1
- package/dist/editor/core/handle.d.ts.map +1 -1
- package/dist/editor/core/handle.js +2 -1
- package/dist/editor/core/handle.js.map +1 -1
- package/dist/editor/core/handleWatchers.d.ts +30 -0
- package/dist/editor/core/handleWatchers.d.ts.map +1 -1
- package/dist/editor/core/handleWatchers.js +15 -0
- package/dist/editor/core/handleWatchers.js.map +1 -1
- package/dist/editor/core/nodeNames.d.ts +7 -0
- package/dist/editor/core/nodeNames.d.ts.map +1 -0
- package/dist/editor/core/nodeNames.js +2 -0
- package/dist/editor/core/nodeNames.js.map +1 -0
- package/dist/editor/core/schema.d.ts.map +1 -1
- package/dist/editor/core/schema.js +4 -2
- package/dist/editor/core/schema.js.map +1 -1
- package/dist/editor/core/toolCommands.d.ts +78 -0
- package/dist/editor/core/toolCommands.d.ts.map +1 -0
- package/dist/editor/core/toolCommands.js +2 -0
- package/dist/editor/core/toolCommands.js.map +1 -0
- package/dist/editor/core/toolDefinition.d.ts +139 -0
- package/dist/editor/core/toolDefinition.d.ts.map +1 -0
- package/dist/editor/core/toolDefinition.js +28 -0
- package/dist/editor/core/toolDefinition.js.map +1 -0
- package/dist/editor/core/toolDescriptor.d.ts +71 -2
- package/dist/editor/core/toolDescriptor.d.ts.map +1 -1
- package/dist/editor/core/toolDescriptor.js +6 -2
- package/dist/editor/core/toolDescriptor.js.map +1 -1
- package/dist/editor/core/toolOptions.d.ts +157 -0
- package/dist/editor/core/toolOptions.d.ts.map +1 -0
- package/dist/editor/core/toolOptions.js +21 -0
- package/dist/editor/core/toolOptions.js.map +1 -0
- package/dist/editor/core/toolState.d.ts +46 -0
- package/dist/editor/core/toolState.d.ts.map +1 -0
- package/dist/editor/core/toolState.js +2 -0
- package/dist/editor/core/toolState.js.map +1 -0
- package/dist/editor/core/tools.d.ts +3 -1
- package/dist/editor/core/tools.d.ts.map +1 -1
- package/dist/editor/core/tools.js.map +1 -1
- package/dist/editor/core/viewIsland.d.ts.map +1 -1
- package/dist/editor/core/viewIsland.js +2 -1
- package/dist/editor/core/viewIsland.js.map +1 -1
- package/dist/editor/features/builtins.d.ts +9 -4
- package/dist/editor/features/builtins.d.ts.map +1 -1
- package/dist/editor/features/builtins.js +36 -7
- package/dist/editor/features/builtins.js.map +1 -1
- package/dist/editor/features/search/SearchPanel.d.ts.map +1 -1
- package/dist/editor/features/search/SearchPanel.js +26 -12
- package/dist/editor/features/search/SearchPanel.js.map +1 -1
- package/dist/editor/features/search/revealMatch.d.ts +16 -0
- package/dist/editor/features/search/revealMatch.d.ts.map +1 -1
- package/dist/editor/features/search/revealMatch.js +22 -0
- package/dist/editor/features/search/revealMatch.js.map +1 -1
- package/dist/editor/features/search/searchPlugin.d.ts +8 -3
- package/dist/editor/features/search/searchPlugin.d.ts.map +1 -1
- package/dist/editor/features/search/searchPlugin.js +17 -6
- package/dist/editor/features/search/searchPlugin.js.map +1 -1
- package/dist/editor/features/search/searchSnapshot.d.ts +14 -0
- package/dist/editor/features/search/searchSnapshot.d.ts.map +1 -0
- package/dist/editor/features/search/searchSnapshot.js +17 -0
- package/dist/editor/features/search/searchSnapshot.js.map +1 -0
- package/dist/editor/features/search/searchTool.d.ts +3 -0
- package/dist/editor/features/search/searchTool.d.ts.map +1 -1
- package/dist/editor/features/search/searchTool.js +30 -1
- package/dist/editor/features/search/searchTool.js.map +1 -1
- package/dist/editor/features/toc/RichTextToc.d.ts +29 -1
- package/dist/editor/features/toc/RichTextToc.d.ts.map +1 -1
- package/dist/editor/features/toc/RichTextToc.js +64 -12
- package/dist/editor/features/toc/RichTextToc.js.map +1 -1
- package/dist/editor/features/toc/documentHeadings.d.ts +30 -0
- package/dist/editor/features/toc/documentHeadings.d.ts.map +1 -0
- package/dist/editor/features/toc/documentHeadings.js +20 -0
- package/dist/editor/features/toc/documentHeadings.js.map +1 -0
- package/dist/editor/features/toc/headings.d.ts +6 -2
- package/dist/editor/features/toc/headings.d.ts.map +1 -1
- package/dist/editor/features/toc/headings.js +4 -2
- package/dist/editor/features/toc/headings.js.map +1 -1
- package/dist/editor/features/toc/tocSnapshot.d.ts +10 -0
- package/dist/editor/features/toc/tocSnapshot.d.ts.map +1 -0
- package/dist/editor/features/toc/tocSnapshot.js +8 -0
- package/dist/editor/features/toc/tocSnapshot.js.map +1 -0
- package/dist/editor/features/toc/tocTool.d.ts +6 -0
- package/dist/editor/features/toc/tocTool.d.ts.map +1 -0
- package/dist/editor/features/toc/tocTool.js +21 -0
- package/dist/editor/features/toc/tocTool.js.map +1 -0
- package/dist/editor/hooks/useRichTextToolState.d.ts +43 -0
- package/dist/editor/hooks/useRichTextToolState.d.ts.map +1 -0
- package/dist/editor/hooks/useRichTextToolState.js +77 -0
- package/dist/editor/hooks/useRichTextToolState.js.map +1 -0
- package/dist/editor/index.d.ts +8 -0
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +3 -0
- package/dist/editor/index.js.map +1 -1
- package/docs/api/editor-extension.en.md +223 -23
- package/docs/api/editor-extension.md +224 -22
- package/docs/api/migration-notes.en.md +36 -1
- package/docs/api/migration-notes.md +35 -0
- package/docs/components/content/rich-text-toc.en.md +13 -11
- package/docs/components/content/rich-text-toc.md +12 -10
- package/docs/components/index.en.md +2 -2
- package/docs/components/index.md +1 -1
- package/docs/components/inputs/rich-text-editor.en.md +227 -11
- package/docs/components/inputs/rich-text-editor.md +228 -10
- package/docs/getting-started.en.md +7 -7
- package/docs/getting-started.md +6 -6
- package/docs/glossary.en.md +52 -48
- package/docs/glossary.md +51 -47
- package/package.json +1 -1
- package/src/editor/RichTextEditor.tsx +94 -39
- package/src/editor/RichTextView.tsx +2 -1
- package/src/editor/core/containers.ts +8 -6
- package/src/editor/core/customTool.ts +8 -2
- package/src/editor/core/customToolEngine.ts +20 -1
- package/src/editor/core/extensions.ts +2 -1
- package/src/editor/core/handle.ts +25 -1
- package/src/editor/core/handleWatchers.ts +45 -0
- package/src/editor/core/nodeNames.ts +7 -0
- package/src/editor/core/schema.ts +5 -2
- package/src/editor/core/toolCommands.ts +85 -0
- package/src/editor/core/toolDefinition.ts +208 -0
- package/src/editor/core/toolDescriptor.ts +94 -3
- package/src/editor/core/toolOptions.ts +211 -0
- package/src/editor/core/toolState.ts +56 -0
- package/src/editor/core/tools.ts +3 -1
- package/src/editor/core/viewIsland.ts +5 -4
- package/src/editor/features/builtins.ts +72 -13
- package/src/editor/features/search/SearchPanel.tsx +38 -14
- package/src/editor/features/search/revealMatch.ts +45 -0
- package/src/editor/features/search/searchPlugin.ts +40 -11
- package/src/editor/features/search/searchSnapshot.ts +29 -0
- package/src/editor/features/search/searchTool.ts +51 -1
- package/src/editor/features/toc/RichTextToc.tsx +122 -13
- package/src/editor/features/toc/documentHeadings.ts +59 -0
- package/src/editor/features/toc/headings.ts +10 -3
- package/src/editor/features/toc/tocSnapshot.ts +24 -0
- package/src/editor/features/toc/tocTool.ts +41 -0
- package/src/editor/hooks/useRichTextToolState.ts +153 -0
- package/src/editor/index.ts +19 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import type { Editor } from "@tiptap/core";
|
|
3
4
|
import {
|
|
4
5
|
forwardRef,
|
|
5
6
|
useCallback,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
useState,
|
|
9
10
|
type HTMLAttributes,
|
|
10
11
|
type KeyboardEvent,
|
|
12
|
+
type MutableRefObject,
|
|
11
13
|
type RefObject,
|
|
12
14
|
} from "react";
|
|
13
15
|
import {
|
|
@@ -19,6 +21,7 @@ import { useMessages } from "../../../i18n/useMessages.js";
|
|
|
19
21
|
import { cn } from "../../../lib/cn.js";
|
|
20
22
|
import { watchHandleEditor } from "../../core/handleWatchers.js";
|
|
21
23
|
import type { RichTextEditorHandle } from "../../RichTextEditor.js";
|
|
24
|
+
import { tocDocumentHeadings } from "./documentHeadings.js";
|
|
22
25
|
import { collectHeadings, documentRoot, type TocHeading } from "./headings.js";
|
|
23
26
|
import { stickyOffset } from "./stickyOffset.js";
|
|
24
27
|
|
|
@@ -50,6 +53,13 @@ export interface RichTextTocProps extends Omit<HTMLAttributes<HTMLDivElement>, "
|
|
|
50
53
|
* По умолчанию 240px.
|
|
51
54
|
*/
|
|
52
55
|
listWidth?: number | string;
|
|
56
|
+
/**
|
|
57
|
+
* Уровни заголовков, которые попадают в Оглавление: подмножество 1–4. Заголовка
|
|
58
|
+
* другого уровня нет ни в полосе, ни в списке, а отступ пунктов считается от
|
|
59
|
+
* самого крупного из оставшихся. Читается при каждой сборке списка, поэтому
|
|
60
|
+
* смена набора уровней пересобирает его. По умолчанию все четыре.
|
|
61
|
+
*/
|
|
62
|
+
levels?: readonly (1 | 2 | 3 | 4)[];
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/**
|
|
@@ -77,6 +87,9 @@ const itemIndent = (depth: number) => ITEM_INDENT[Math.min(depth, ITEM_INDENT.le
|
|
|
77
87
|
const OPEN_DELAY = 200;
|
|
78
88
|
const CLOSE_DELAY = 300;
|
|
79
89
|
|
|
90
|
+
/** Все уровни заголовков: Оглавление без `levels`. */
|
|
91
|
+
const ALL_LEVELS = [1, 2, 3, 4] as const;
|
|
92
|
+
|
|
80
93
|
/** Пауза после последней правки Документа, по которой список пересобирается. */
|
|
81
94
|
const REBUILD_DELAY = 150;
|
|
82
95
|
|
|
@@ -185,12 +198,23 @@ function jumpTo(
|
|
|
185
198
|
flash(heading);
|
|
186
199
|
|
|
187
200
|
const behavior: ScrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
188
|
-
|
|
201
|
+
const scroller = scrollParent(heading);
|
|
189
202
|
if (offset === null) {
|
|
190
|
-
|
|
203
|
+
// Без шапки отступ задаёт `scroll-margin-top` заголовка (ADR-0016). Окно
|
|
204
|
+
// прокручивает браузер. Но `scrollIntoView` двигает ещё и всех
|
|
205
|
+
// прокручиваемых предков, а значит, при Документе в своей прокручиваемой
|
|
206
|
+
// области уехала бы и страница, — поэтому область прокручивается сама,
|
|
207
|
+
// а окно не трогается.
|
|
208
|
+
if (!scroller) {
|
|
209
|
+
heading.scrollIntoView({ behavior, block: "start" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const margin = parseFloat(getComputedStyle(heading).scrollMarginTop) || 0;
|
|
213
|
+
const edge = scroller.getBoundingClientRect().top;
|
|
214
|
+
const top = heading.getBoundingClientRect().top;
|
|
215
|
+
scroller.scrollTo({ top: scroller.scrollTop + top - edge - margin, behavior });
|
|
191
216
|
return;
|
|
192
217
|
}
|
|
193
|
-
const scroller = scrollParent(heading);
|
|
194
218
|
const top = heading.getBoundingClientRect().top;
|
|
195
219
|
if (scroller) {
|
|
196
220
|
const edge = scroller.getBoundingClientRect().top;
|
|
@@ -320,6 +344,26 @@ export interface TocCoreProps extends RichTextTocProps {
|
|
|
320
344
|
* уровня, а не полоса выше текста.
|
|
321
345
|
*/
|
|
322
346
|
alignToFirstHeading?: boolean;
|
|
347
|
+
/**
|
|
348
|
+
* Текущий раздел сменился (в том числе при первом показе): Редактор кладёт
|
|
349
|
+
* индекс в Состояние Инструмента `toc`. Индекс считается по списку, который
|
|
350
|
+
* видит `TocCore`; пока заголовков меньше двух, он остаётся прежним.
|
|
351
|
+
*/
|
|
352
|
+
onCurrentChange?: (index: number) => void;
|
|
353
|
+
/**
|
|
354
|
+
* Движок Редактора. Нужен команде `jumpTo` (`jumpRef`): индекс команды — это
|
|
355
|
+
* индекс заголовка в снимке Состояния `toc`, а снимок читает Документ движка
|
|
356
|
+
* на каждой транзакции. Список ядра пересобирается из DOM с задержкой
|
|
357
|
+
* (`REBUILD_DELAY`), и сразу после правки индексы двух списков расходились бы.
|
|
358
|
+
* Без движка `jumpTo` считает по списку ядра.
|
|
359
|
+
*/
|
|
360
|
+
editor?: Editor;
|
|
361
|
+
/**
|
|
362
|
+
* Куда ядро кладёт переход к заголовку по индексу снимка Состояния (команда
|
|
363
|
+
* `jumpTo` Инструмента): ведёт себя как клик по пункту. Пока ядро смонтировано,
|
|
364
|
+
* там функция, после — `null`. Нет заголовка с таким индексом — `false`.
|
|
365
|
+
*/
|
|
366
|
+
jumpRef?: MutableRefObject<((index: number) => boolean) | null>;
|
|
323
367
|
}
|
|
324
368
|
|
|
325
369
|
export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore(
|
|
@@ -327,9 +371,13 @@ export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore
|
|
|
327
371
|
target,
|
|
328
372
|
offsetVar = "--topbar-h",
|
|
329
373
|
listWidth = 240,
|
|
374
|
+
levels,
|
|
330
375
|
stickyTop,
|
|
331
376
|
focusHeading,
|
|
332
377
|
alignToFirstHeading = false,
|
|
378
|
+
onCurrentChange,
|
|
379
|
+
editor,
|
|
380
|
+
jumpRef,
|
|
333
381
|
className,
|
|
334
382
|
style,
|
|
335
383
|
...rest
|
|
@@ -338,10 +386,12 @@ export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore
|
|
|
338
386
|
) {
|
|
339
387
|
// Настройки хозяина читаются в обработчиках прокрутки: последние значения —
|
|
340
388
|
// через ссылку, чтобы новая функция на каждом рендере не пересоздавала слушателей.
|
|
341
|
-
const latest = useRef({ stickyTop, focusHeading, offsetVar });
|
|
389
|
+
const latest = useRef({ stickyTop, focusHeading, offsetVar, levels, onCurrentChange, editor });
|
|
342
390
|
useEffect(() => {
|
|
343
|
-
latest.current = { stickyTop, focusHeading, offsetVar };
|
|
391
|
+
latest.current = { stickyTop, focusHeading, offsetVar, levels, onCurrentChange, editor };
|
|
344
392
|
});
|
|
393
|
+
// Набор уровней сравнивается по значению: массив-литерал новый на каждом рендере.
|
|
394
|
+
const levelsKey = levels ? levels.join(",") : "";
|
|
345
395
|
const offsetNow = useCallback(() => {
|
|
346
396
|
const own = latest.current;
|
|
347
397
|
return own.stickyTop ? own.stickyTop() : stickyOffset(own.offsetVar);
|
|
@@ -351,6 +401,10 @@ export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore
|
|
|
351
401
|
const [headings, setHeadings] = useState<TocHeading[]>([]);
|
|
352
402
|
const [gap, setGap] = useState(STRIP_GAP);
|
|
353
403
|
const [current, setCurrent] = useState(0);
|
|
404
|
+
// Текущий раздел — в Состояние Инструмента (`useRichTextToolState`).
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
latest.current.onCurrentChange?.(current);
|
|
407
|
+
}, [current]);
|
|
354
408
|
const [open, setOpen] = useState(false);
|
|
355
409
|
const [fits, setFits] = useState(true);
|
|
356
410
|
const stripRef = useRef<HTMLButtonElement>(null);
|
|
@@ -379,7 +433,7 @@ export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore
|
|
|
379
433
|
return;
|
|
380
434
|
}
|
|
381
435
|
const read = () => {
|
|
382
|
-
const next = collectHeadings(element);
|
|
436
|
+
const next = collectHeadings(element, latest.current.levels);
|
|
383
437
|
setHeadings((prev) => (sameHeadings(prev, next) ? prev : next));
|
|
384
438
|
};
|
|
385
439
|
read();
|
|
@@ -397,7 +451,7 @@ export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore
|
|
|
397
451
|
clearTimeout(timer);
|
|
398
452
|
observer.disconnect();
|
|
399
453
|
};
|
|
400
|
-
}, [element]);
|
|
454
|
+
}, [element, levelsKey]);
|
|
401
455
|
|
|
402
456
|
// Шаг полосы зависит от высоты окна и числа заголовков.
|
|
403
457
|
useEffect(() => {
|
|
@@ -426,7 +480,7 @@ export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore
|
|
|
426
480
|
Пересчёт возвращается со следующей прокрутки после того, как переход
|
|
427
481
|
улёгся (`settled`).
|
|
428
482
|
*/
|
|
429
|
-
const pinned = useRef<{
|
|
483
|
+
const pinned = useRef<{ heading: HTMLElement; settled: boolean; timer: number } | null>(null);
|
|
430
484
|
/** (Пере)запустить ожидание: прокрутка перехода молчит `PIN_SETTLE` — переход улёгся. */
|
|
431
485
|
const settle = (lock: { settled: boolean; timer: number }) => {
|
|
432
486
|
window.clearTimeout(lock.timer);
|
|
@@ -434,12 +488,62 @@ export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore
|
|
|
434
488
|
lock.settled = true;
|
|
435
489
|
}, PIN_SETTLE);
|
|
436
490
|
};
|
|
437
|
-
|
|
491
|
+
/*
|
|
492
|
+
Раздел перехода помнится узлом, а не индексом: список может пересобраться,
|
|
493
|
+
пока идёт прокрутка (правка моложе `REBUILD_DELAY`), и индекс повёл бы к
|
|
494
|
+
соседнему заголовку.
|
|
495
|
+
*/
|
|
496
|
+
const pin = (index: number, heading: HTMLElement) => {
|
|
438
497
|
if (pinned.current) window.clearTimeout(pinned.current.timer);
|
|
439
|
-
pinned.current = {
|
|
498
|
+
pinned.current = { heading, settled: false, timer: 0 };
|
|
440
499
|
settle(pinned.current);
|
|
441
500
|
setCurrent(index);
|
|
442
501
|
};
|
|
502
|
+
/*
|
|
503
|
+
Переход к пункту списка: клик и команда `jumpTo` Инструмента идут одним
|
|
504
|
+
путём. Заголовок, уже вышедший из Документа (правка моложе `REBUILD_DELAY`),
|
|
505
|
+
не пункт: список вот-вот пересоберётся.
|
|
506
|
+
*/
|
|
507
|
+
const go = (index: number): boolean => {
|
|
508
|
+
const heading = headings[index];
|
|
509
|
+
if (!heading?.element.isConnected) return false;
|
|
510
|
+
pin(index, heading.element);
|
|
511
|
+
jumpTo(heading.element, offsetNow(), latest.current.focusHeading);
|
|
512
|
+
return true;
|
|
513
|
+
};
|
|
514
|
+
/*
|
|
515
|
+
Команда `jumpTo(index)`: индекс — из снимка Состояния, а снимок читает
|
|
516
|
+
Документ движка (`tocDocumentHeadings`, тот же обход), поэтому заголовок
|
|
517
|
+
берётся оттуда: `view.nodeDOM` отдаёт его узел, ProseMirror обновляет DOM
|
|
518
|
+
синхронно с транзакцией. Список ядра при этом может отставать до
|
|
519
|
+
`REBUILD_DELAY`, поэтому читается заново, а не из состояния: от него нужен
|
|
520
|
+
только индекс пункта для подсветки.
|
|
521
|
+
*/
|
|
522
|
+
const goToSnapshotIndex = (index: number): boolean => {
|
|
523
|
+
const { editor: engine, levels: shown } = latest.current;
|
|
524
|
+
if (!engine) return go(index);
|
|
525
|
+
if (engine.isDestroyed) return false;
|
|
526
|
+
const found = tocDocumentHeadings(engine, { levels: shown ?? ALL_LEVELS })[index];
|
|
527
|
+
const node = found && engine.view.nodeDOM(found.pos);
|
|
528
|
+
if (!(node instanceof HTMLElement) || !node.isConnected || !element) return false;
|
|
529
|
+
const fresh = collectHeadings(element, shown);
|
|
530
|
+
setHeadings((prev) => (sameHeadings(prev, fresh) ? prev : fresh));
|
|
531
|
+
const listed = fresh.findIndex((heading) => heading.element === node);
|
|
532
|
+
if (listed >= 0) pin(listed, node);
|
|
533
|
+
jumpTo(node, offsetNow(), latest.current.focusHeading);
|
|
534
|
+
return true;
|
|
535
|
+
};
|
|
536
|
+
const goRef = useRef(goToSnapshotIndex);
|
|
537
|
+
useEffect(() => {
|
|
538
|
+
goRef.current = goToSnapshotIndex;
|
|
539
|
+
});
|
|
540
|
+
useEffect(() => {
|
|
541
|
+
if (!jumpRef) return;
|
|
542
|
+
jumpRef.current = (index) => goRef.current(index);
|
|
543
|
+
return () => {
|
|
544
|
+
jumpRef.current = null;
|
|
545
|
+
};
|
|
546
|
+
}, [jumpRef]);
|
|
443
547
|
|
|
444
548
|
// Подсветка текущего раздела. Слушатель прокрутки — на документе с захватом:
|
|
445
549
|
// так его достигает прокрутка и окна, и любого контейнера вокруг цели.
|
|
@@ -448,7 +552,13 @@ export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore
|
|
|
448
552
|
let frame = 0;
|
|
449
553
|
const update = () => {
|
|
450
554
|
frame = 0;
|
|
451
|
-
|
|
555
|
+
// Список пересобрался, пока идёт прокрутка перехода: раздел перехода тот
|
|
556
|
+
// же заголовок, но под новым индексом. Замер посреди прокрутки назвал бы
|
|
557
|
+
// соседний, и он остался бы текущим, когда прокрутка уляжется.
|
|
558
|
+
const lock = pinned.current;
|
|
559
|
+
const held =
|
|
560
|
+
lock && !lock.settled ? headings.findIndex((h) => h.element === lock.heading) : -1;
|
|
561
|
+
setCurrent(held >= 0 ? held : currentIndex(headings, offsetNow()));
|
|
452
562
|
};
|
|
453
563
|
const schedule = (event?: Event) => {
|
|
454
564
|
// Считается только прокрутка, которая двигает Документ: окна или
|
|
@@ -670,8 +780,7 @@ export const TocCore = forwardRef<HTMLDivElement, TocCoreProps>(function TocCore
|
|
|
670
780
|
index === current ? "bg-bg-2 text-fg-0" : "text-fg-2 hover:text-fg-0"
|
|
671
781
|
)}
|
|
672
782
|
onClick={(event) => {
|
|
673
|
-
|
|
674
|
-
jumpTo(heading.element, offsetNow(), latest.current.focusHeading);
|
|
783
|
+
go(index);
|
|
675
784
|
/*
|
|
676
785
|
В Редакторе в правке фокус на заголовок не переходит и
|
|
677
786
|
остаётся на пункте, а фокус в списке держит его открытым:
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import type { Node as PmNode } from "@tiptap/pm/model";
|
|
3
|
+
import { WRAPPER_NODE } from "../../core/nodeNames.js";
|
|
4
|
+
import type { TocLevel, TocToolOptions } from "../../core/toolOptions.js";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Модуль без Tiptap в рантайме (только типы): его читает ядро Оглавления
|
|
8
|
+
(`TocCore`, `RichTextToc.tsx`), которому движок в бандл не нужен.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Заголовок Документа с позицией: по ней команда `jumpTo` находит DOM-узел. */
|
|
12
|
+
export interface TocDocumentHeading {
|
|
13
|
+
level: TocLevel;
|
|
14
|
+
text: string;
|
|
15
|
+
/** Позиция узла заголовка в Документе (для `editor.view.nodeDOM`). */
|
|
16
|
+
pos: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Заголовки Оглавления по Документу движка, а не по DOM: ядро Оглавления
|
|
21
|
+
* (`TocCore`) — ленивый чанк, и пока он не пришёл (или нарисован HTML-режим),
|
|
22
|
+
* узлов для обхода нет, а данные экран просит с первого кадра. Это единственный
|
|
23
|
+
* обход, по которому считаются индексы: и снимок Состояния `toc`, и команда
|
|
24
|
+
* `jumpTo` берут заголовок отсюда, поэтому индекс из снимка ведёт к тому же
|
|
25
|
+
* заголовку сразу после правки, не дожидаясь, пока ядро пересоберёт свой список
|
|
26
|
+
* из DOM (`REBUILD_DELAY`).
|
|
27
|
+
*
|
|
28
|
+
* ТЕ ЖЕ ПРАВИЛА, ЧТО У `collectHeadings` (`headings.ts`), иначе индексы разойдутся
|
|
29
|
+
* с пунктами списка: заголовок на верхнем уровне или внутри Обёрток (колонки
|
|
30
|
+
* Раскладки, свои Блоки, цитаты и списки — нет), уровень из `levels`, текст без
|
|
31
|
+
* крайних пробелов не пуст. Текст — тот же, что у `textContent` DOM: мягкий
|
|
32
|
+
* перенос в него не входит, а Текст атома — входит.
|
|
33
|
+
*
|
|
34
|
+
* Обход по верхнему уровню и Обёрткам, а не по всему Документу: стоимость — число
|
|
35
|
+
* Блоков верхнего уровня, она не растёт от вложенности и заметна не будет даже
|
|
36
|
+
* на каждой транзакции.
|
|
37
|
+
*/
|
|
38
|
+
export function tocDocumentHeadings(
|
|
39
|
+
editor: Editor,
|
|
40
|
+
{ levels }: Pick<TocToolOptions, "levels">
|
|
41
|
+
): TocDocumentHeading[] {
|
|
42
|
+
const headings: TocDocumentHeading[] = [];
|
|
43
|
+
// `start` — позиция, с которой начинается содержимое `parent`.
|
|
44
|
+
const visit = (parent: PmNode, start: number) =>
|
|
45
|
+
parent.forEach((node, offset) => {
|
|
46
|
+
const pos = start + offset;
|
|
47
|
+
if (node.type.name === WRAPPER_NODE) return visit(node, pos + 1);
|
|
48
|
+
if (node.type.name !== "heading") return;
|
|
49
|
+
const level = node.attrs.level as TocLevel;
|
|
50
|
+
const text = node
|
|
51
|
+
.textBetween(0, node.content.size, "", (leaf) =>
|
|
52
|
+
leaf.type.name === "hardBreak" ? "" : leaf.textContent
|
|
53
|
+
)
|
|
54
|
+
.trim();
|
|
55
|
+
if (text && levels.includes(level)) headings.push({ level, text, pos });
|
|
56
|
+
});
|
|
57
|
+
visit(editor.state.doc, 0);
|
|
58
|
+
return headings;
|
|
59
|
+
}
|
|
@@ -51,13 +51,20 @@ function isStructural(heading: Element, root: HTMLElement): boolean {
|
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Заголовки Документа в порядке чтения. `only` — какие уровни (1–4) берутся:
|
|
56
|
+
* остальные не попадают в список, и глубина (`depth`) считается от самого
|
|
57
|
+
* крупного из оставшихся. Нет — все.
|
|
58
|
+
*/
|
|
59
|
+
export function collectHeadings(target: HTMLElement, only?: readonly number[]): TocHeading[] {
|
|
56
60
|
const root = documentRoot(target);
|
|
57
61
|
// Пустой заголовок (автор только начал его в Редакторе) — не пункт: у
|
|
58
62
|
// кнопки без текста не было бы имени, а у полосы — лишний штрих.
|
|
59
63
|
const found = [...root.querySelectorAll<HTMLElement>(HEADING)].filter(
|
|
60
|
-
(heading) =>
|
|
64
|
+
(heading) =>
|
|
65
|
+
isStructural(heading, root) &&
|
|
66
|
+
heading.textContent?.trim() &&
|
|
67
|
+
(!only || only.includes(Number(heading.tagName[1])))
|
|
61
68
|
);
|
|
62
69
|
const levels = found.map((heading) => Number(heading.tagName[1]));
|
|
63
70
|
const top = Math.min(...levels);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Editor } from "@tiptap/core";
|
|
4
|
+
import type { TocToolOptions } from "../../core/toolOptions.js";
|
|
5
|
+
import type { TocToolHeading, TocToolState } from "../../core/toolState.js";
|
|
6
|
+
import { tocDocumentHeadings } from "./documentHeadings.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Снимок Состояния `toc` для `useRichTextToolState`: заголовки из
|
|
10
|
+
* `tocDocumentHeadings` без позиций (они в контракт не входят) и текущий раздел.
|
|
11
|
+
* `at` — индекс текущего раздела, который сообщило ядро Оглавления.
|
|
12
|
+
*/
|
|
13
|
+
export function tocSnapshot(
|
|
14
|
+
editor: Editor,
|
|
15
|
+
options: Pick<TocToolOptions, "levels">,
|
|
16
|
+
at: number | undefined
|
|
17
|
+
): TocToolState {
|
|
18
|
+
const headings: TocToolHeading[] = tocDocumentHeadings(editor, options).map(
|
|
19
|
+
({ level, text }, index) => ({ level, text, index })
|
|
20
|
+
);
|
|
21
|
+
// Прокрутка сообщает индекс по прежнему списку: после правки он мог выйти за край.
|
|
22
|
+
const current = headings.length ? Math.min(Math.max(at ?? 0, 0), headings.length - 1) : -1;
|
|
23
|
+
return { headings, current };
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ToolDescriptor } from "../../core/toolDescriptor.js";
|
|
4
|
+
import type { TocToolCommands } from "../../core/toolCommands.js";
|
|
5
|
+
import type { TocLevel, TocToolOptions } from "../../core/toolOptions.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Оглавление на описании Инструмента (ADR-0068). Части для движка и UI-записей
|
|
9
|
+
* у него нет: полосу и список рисует `RichTextEditor` в левом поле рамки
|
|
10
|
+
* (`TocCore`, ленивым чанком), а описание несёт Опции — умолчания и проверки,
|
|
11
|
+
* из которых `expandTools` собирает Опции Редактора.
|
|
12
|
+
*
|
|
13
|
+
* Тяжёлых импортов нет: описание лежит в стартовом куске Редактора, а UI
|
|
14
|
+
* Оглавления — нет.
|
|
15
|
+
*/
|
|
16
|
+
const TOC_LEVELS: readonly TocLevel[] = [1, 2, 3, 4];
|
|
17
|
+
|
|
18
|
+
/** Умолчания Опций: переменная `TopBar` (ADR-0016), все четыре уровня, список в 240 пикселей. */
|
|
19
|
+
export const TOC_DEFAULTS: TocToolOptions = {
|
|
20
|
+
offsetVar: "--topbar-h",
|
|
21
|
+
levels: TOC_LEVELS,
|
|
22
|
+
listWidth: 240,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const TOC_TOOL: ToolDescriptor = {
|
|
26
|
+
name: "toc",
|
|
27
|
+
defaults: TOC_DEFAULTS,
|
|
28
|
+
checks: {
|
|
29
|
+
// Имя вставляется в `var(…)` стиля поля, поэтому — только слово с дефисами.
|
|
30
|
+
offsetVar: (value) => typeof value === "string" && /^--[\w-]+$/.test(value),
|
|
31
|
+
levels: (value) =>
|
|
32
|
+
Array.isArray(value) &&
|
|
33
|
+
value.length > 0 &&
|
|
34
|
+
value.every((level) => (TOC_LEVELS as readonly unknown[]).includes(level)),
|
|
35
|
+
listWidth: (value) =>
|
|
36
|
+
(typeof value === "number" && Number.isFinite(value) && value > 0) ||
|
|
37
|
+
(typeof value === "string" && value.trim() !== ""),
|
|
38
|
+
},
|
|
39
|
+
// Переход живёт в ленивом ядре Оглавления: мост зовёт его, пока оно смонтировано.
|
|
40
|
+
commands: ({ bridge }): TocToolCommands => ({ jumpTo: bridge.jumpToc }),
|
|
41
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Editor } from "@tiptap/react";
|
|
4
|
+
import { useCallback, useRef, useSyncExternalStore, type RefObject } from "react";
|
|
5
|
+
import { toolSignals, watchHandleEditor } from "../core/handleWatchers.js";
|
|
6
|
+
import { toolOptions } from "../core/toolDescriptor.js";
|
|
7
|
+
import type { RichTextTools } from "../core/toolOptions.js";
|
|
8
|
+
import type { RichTextEditorHandle } from "../RichTextEditor.js";
|
|
9
|
+
import { searchSnapshot } from "../features/search/searchSnapshot.js";
|
|
10
|
+
import { tocSnapshot } from "../features/toc/tocSnapshot.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Состояние Инструмента по имени: тип из карты `RichTextTools`. Имя, которого в
|
|
14
|
+
* карте нет, — `unknown`: типизация добровольна, и Инструмент без записи в карте
|
|
15
|
+
* не ломает сборку.
|
|
16
|
+
*/
|
|
17
|
+
export type RichTextToolState<Name extends string> = Name extends keyof RichTextTools
|
|
18
|
+
? RichTextTools[Name] extends { state: infer State }
|
|
19
|
+
? State
|
|
20
|
+
: unknown
|
|
21
|
+
: unknown;
|
|
22
|
+
|
|
23
|
+
/** Снимок встроенного Инструмента; `null` — Инструмента в движке нет. */
|
|
24
|
+
const SNAPSHOTS = new Map<string, (editor: Editor, ref: object) => unknown>([
|
|
25
|
+
[
|
|
26
|
+
"search",
|
|
27
|
+
(editor, ref) => {
|
|
28
|
+
const { tools, searchOpen } = toolSignals(ref);
|
|
29
|
+
return tools?.has("search") ? searchSnapshot(editor, searchOpen === true) : null;
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
"toc",
|
|
34
|
+
(editor, ref) => {
|
|
35
|
+
const { tools, tocAt } = toolSignals(ref);
|
|
36
|
+
const options = tools && toolOptions(tools, "toc");
|
|
37
|
+
return options && tocSnapshot(editor, options, tocAt);
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Простой объект: литерал или `Object.create(null)`, не экземпляр класса. */
|
|
43
|
+
function isPlainObject(value: object): boolean {
|
|
44
|
+
const prototype = Object.getPrototypeOf(value) as unknown;
|
|
45
|
+
return prototype === Object.prototype || prototype === null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Равенство для снимков. По значению сравниваются только простые данные:
|
|
50
|
+
* примитивы, массивы и простые объекты. Всё прочее — `Date`, `Map`, `Set`,
|
|
51
|
+
* экземпляры классов — по ссылке: у них нет собственных ключей, по которым можно
|
|
52
|
+
* было бы увидеть разницу, и «равными по значению» оказались бы любые два, то есть
|
|
53
|
+
* экран тихо не обновлялся бы. Новый экземпляр — новый снимок.
|
|
54
|
+
*/
|
|
55
|
+
function sameValue(a: unknown, b: unknown): boolean {
|
|
56
|
+
if (Object.is(a, b)) return true;
|
|
57
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
58
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
59
|
+
if (!Array.isArray(a) && !(isPlainObject(a) && isPlainObject(b))) return false;
|
|
60
|
+
const left = a as Record<string, unknown>;
|
|
61
|
+
const right = b as Record<string, unknown>;
|
|
62
|
+
const keys = Object.keys(left);
|
|
63
|
+
return (
|
|
64
|
+
keys.length === Object.keys(right).length &&
|
|
65
|
+
keys.every((key) => key in right && sameValue(left[key], right[key]))
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Состояние встроенного Инструмента для своего экрана: «3 из 12» в Строке
|
|
71
|
+
* статуса из Поиска, внешняя навигация из заголовков Оглавления. Имя —
|
|
72
|
+
* Инструмент из набора `tools`, а тип снимка выводится из карты
|
|
73
|
+
* `RichTextTools`: `search` отдаёт `SearchToolState`, `toc` — `TocToolState`.
|
|
74
|
+
*
|
|
75
|
+
* ПЕРЕРИСОВКА — ТОЛЬКО ПРИ СМЕНЕ СНИМКА. Снимок пересчитывается на каждую
|
|
76
|
+
* транзакцию движка и на сигналы кита, которых в движке нет: открытие и закрытие
|
|
77
|
+
* панели Поиска, смена текущего раздела Оглавления при прокрутке. Экран
|
|
78
|
+
* перерисовывается, когда результат стал другим (простые данные — примитивы,
|
|
79
|
+
* массивы, простые объекты — сравниваются по значению, как у
|
|
80
|
+
* `useRichTextEditorState`; `Date`, `Map`, `Set` и экземпляры классов — по
|
|
81
|
+
* ссылке), поэтому набор текста, не меняющий снимок, его не трогает. Срез из `selector` сравнивается так же: `(state) => state.total`
|
|
82
|
+
* подписывает экран на одно число.
|
|
83
|
+
*
|
|
84
|
+
* `null` — до монтажа и в первом рендере экрана, на сервере, а также когда
|
|
85
|
+
* Инструмента нет в наборе `tools`: пресеты `toc` не включают, его включают
|
|
86
|
+
* именем или записью `tool("toc")`. Селектор при `null` не вызывается. Один и
|
|
87
|
+
* тот же `ref` отдаётся в `RichTextEditor`.
|
|
88
|
+
*
|
|
89
|
+
* Снимок — данные без DOM-узлов и позиций ProseMirror; изменять его не нужно.
|
|
90
|
+
* Хук в стартовый граф `RichTextEditor` не входит: его цена — только у экрана,
|
|
91
|
+
* который его импортировал.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* const ref = useRef<RichTextEditorHandle>(null);
|
|
95
|
+
* const total = useRichTextToolState(ref, "search", (state) => state.total);
|
|
96
|
+
* const headings = useRichTextToolState(ref, "toc", (state) => state.headings);
|
|
97
|
+
*/
|
|
98
|
+
export function useRichTextToolState<Name extends string>(
|
|
99
|
+
ref: RefObject<RichTextEditorHandle | null>,
|
|
100
|
+
name: Name
|
|
101
|
+
): RichTextToolState<Name> | null;
|
|
102
|
+
export function useRichTextToolState<Name extends string, Slice>(
|
|
103
|
+
ref: RefObject<RichTextEditorHandle | null>,
|
|
104
|
+
name: Name,
|
|
105
|
+
selector: (state: RichTextToolState<Name>) => Slice
|
|
106
|
+
): Slice | null;
|
|
107
|
+
export function useRichTextToolState(
|
|
108
|
+
ref: RefObject<RichTextEditorHandle | null>,
|
|
109
|
+
name: string,
|
|
110
|
+
selector?: (state: never) => unknown
|
|
111
|
+
): unknown {
|
|
112
|
+
const last = useRef<{ value: unknown } | null>(null);
|
|
113
|
+
|
|
114
|
+
const subscribe = useCallback(
|
|
115
|
+
(listener: () => void) => {
|
|
116
|
+
// Транзакции движка — на текущем движке; он меняется — пересаживаем слушателя.
|
|
117
|
+
let followed: Editor | null = null;
|
|
118
|
+
const follow = () => {
|
|
119
|
+
followed?.off("transaction", listener);
|
|
120
|
+
const editor = ref.current?.editor ?? null;
|
|
121
|
+
followed = editor && !editor.isDestroyed ? editor : null;
|
|
122
|
+
followed?.on("transaction", listener);
|
|
123
|
+
};
|
|
124
|
+
follow();
|
|
125
|
+
const stop = watchHandleEditor(ref, () => {
|
|
126
|
+
follow();
|
|
127
|
+
listener();
|
|
128
|
+
});
|
|
129
|
+
return () => {
|
|
130
|
+
stop();
|
|
131
|
+
followed?.off("transaction", listener);
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
[ref]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const read = (): unknown => {
|
|
138
|
+
const editor = ref.current?.editor;
|
|
139
|
+
// Своё Состояние — функция описания из набора этого Редактора; встроенное — таблица.
|
|
140
|
+
const own = toolSignals(ref).tools?.descriptors.find((entry) => entry.name === name)?.state;
|
|
141
|
+
const snapshot =
|
|
142
|
+
editor && !editor.isDestroyed
|
|
143
|
+
? ((own ? own(editor) : SNAPSHOTS.get(name)?.(editor, ref)) ?? null)
|
|
144
|
+
: null;
|
|
145
|
+
const value = snapshot === null || !selector ? snapshot : selector(snapshot as never);
|
|
146
|
+
// Прежняя ссылка, пока значение то же: иначе React видит новый снимок на каждый рендер.
|
|
147
|
+
if (last.current && sameValue(last.current.value, value)) return last.current.value;
|
|
148
|
+
last.current = { value };
|
|
149
|
+
return value;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return useSyncExternalStore(subscribe, read, () => null);
|
|
153
|
+
}
|
package/src/editor/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ export { RichTextEditor } from "./RichTextEditor.js";
|
|
|
19
19
|
export type { RichTextEditorHandle, RichTextEditorProps } from "./RichTextEditor.js";
|
|
20
20
|
export type { RichTextDocument } from "./core/jsonDocument.js";
|
|
21
21
|
export { useRichTextEditorState } from "./hooks/useRichTextEditorState.js";
|
|
22
|
+
export { useRichTextToolState } from "./hooks/useRichTextToolState.js";
|
|
23
|
+
export type { RichTextToolState } from "./hooks/useRichTextToolState.js";
|
|
22
24
|
export { RichTextView } from "./RichTextView.js";
|
|
23
25
|
export type { RichTextViewProps } from "./RichTextView.js";
|
|
24
26
|
export { RichTextToc } from "./features/toc/RichTextToc.js";
|
|
@@ -36,6 +38,23 @@ export type {
|
|
|
36
38
|
} from "./core/schema.js";
|
|
37
39
|
export { RICH_TEXT_PRESETS } from "./core/tools.js";
|
|
38
40
|
export type { RichTextEditorPreset } from "./core/tools.js";
|
|
41
|
+
export { tool } from "./core/toolOptions.js";
|
|
42
|
+
export { defineTool } from "./core/toolDefinition.js";
|
|
43
|
+
export type {
|
|
44
|
+
CustomToolBody,
|
|
45
|
+
ToolDefinition,
|
|
46
|
+
ToolDefinitionContext,
|
|
47
|
+
ToolDefinitionInput,
|
|
48
|
+
} from "./core/toolDefinition.js";
|
|
49
|
+
export type {
|
|
50
|
+
ConfiguredTool,
|
|
51
|
+
RichTextTools,
|
|
52
|
+
SearchToolOptions,
|
|
53
|
+
TocLevel,
|
|
54
|
+
TocToolOptions,
|
|
55
|
+
} from "./core/toolOptions.js";
|
|
56
|
+
export type { SearchToolCommands, TocToolCommands } from "./core/toolCommands.js";
|
|
57
|
+
export type { SearchToolState, TocToolHeading, TocToolState } from "./core/toolState.js";
|
|
39
58
|
export { markRange } from "./core/customTool.js";
|
|
40
59
|
export type {
|
|
41
60
|
TextCardRange,
|