@olddevs/ui 4.3.0 → 4.4.0

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 (68) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/editor/RichTextEditor.d.ts.map +1 -1
  3. package/dist/editor/RichTextEditor.js +38 -4
  4. package/dist/editor/RichTextEditor.js.map +1 -1
  5. package/dist/editor/core/handle.d.ts +1 -5
  6. package/dist/editor/core/handle.d.ts.map +1 -1
  7. package/dist/editor/core/handle.js +1 -13
  8. package/dist/editor/core/handle.js.map +1 -1
  9. package/dist/editor/core/handleWatchers.d.ts +6 -0
  10. package/dist/editor/core/handleWatchers.d.ts.map +1 -0
  11. package/dist/editor/core/handleWatchers.js +14 -0
  12. package/dist/editor/core/handleWatchers.js.map +1 -0
  13. package/dist/editor/core/tools.d.ts +7 -2
  14. package/dist/editor/core/tools.d.ts.map +1 -1
  15. package/dist/editor/core/tools.js +2 -1
  16. package/dist/editor/core/tools.js.map +1 -1
  17. package/dist/editor/features/toc/RichTextToc.d.ts +63 -0
  18. package/dist/editor/features/toc/RichTextToc.d.ts.map +1 -0
  19. package/dist/editor/features/toc/RichTextToc.js +372 -0
  20. package/dist/editor/features/toc/RichTextToc.js.map +1 -0
  21. package/dist/editor/features/toc/headings.d.ts +31 -0
  22. package/dist/editor/features/toc/headings.d.ts.map +1 -0
  23. package/dist/editor/features/toc/headings.js +25 -0
  24. package/dist/editor/features/toc/headings.js.map +1 -0
  25. package/dist/editor/features/toc/stickyOffset.d.ts +3 -0
  26. package/dist/editor/features/toc/stickyOffset.d.ts.map +1 -0
  27. package/dist/editor/features/toc/stickyOffset.js +14 -0
  28. package/dist/editor/features/toc/stickyOffset.js.map +1 -0
  29. package/dist/editor/index.d.ts +2 -0
  30. package/dist/editor/index.d.ts.map +1 -1
  31. package/dist/editor/index.js +1 -0
  32. package/dist/editor/index.js.map +1 -1
  33. package/dist/i18n/defaults.d.ts.map +1 -1
  34. package/dist/i18n/defaults.js +1 -0
  35. package/dist/i18n/defaults.js.map +1 -1
  36. package/dist/i18n/messages.d.ts +5 -0
  37. package/dist/i18n/messages.d.ts.map +1 -1
  38. package/dist/locales/ru.d.ts.map +1 -1
  39. package/dist/locales/ru.js +1 -0
  40. package/dist/locales/ru.js.map +1 -1
  41. package/docs/api/messages.en.md +743 -0
  42. package/docs/api/messages.md +1 -0
  43. package/docs/api/utilities.en.md +233 -0
  44. package/docs/components/content/rich-text-toc.en.md +130 -0
  45. package/docs/components/content/rich-text-toc.md +129 -0
  46. package/docs/components/index.en.md +2 -1
  47. package/docs/components/index.md +1 -0
  48. package/docs/components/inputs/rich-text-editor.en.md +52 -34
  49. package/docs/components/inputs/rich-text-editor.md +20 -2
  50. package/docs/conventions.en.md +318 -0
  51. package/docs/getting-started.en.md +4 -4
  52. package/docs/getting-started.md +3 -3
  53. package/docs/glossary.en.md +2 -1
  54. package/docs/glossary.md +1 -0
  55. package/docs/index.en.md +53 -0
  56. package/llms.txt +1 -0
  57. package/package.json +1 -1
  58. package/src/editor/RichTextEditor.tsx +107 -3
  59. package/src/editor/core/handle.ts +2 -23
  60. package/src/editor/core/handleWatchers.ts +27 -0
  61. package/src/editor/core/tools.ts +9 -2
  62. package/src/editor/features/toc/RichTextToc.tsx +712 -0
  63. package/src/editor/features/toc/headings.ts +70 -0
  64. package/src/editor/features/toc/stickyOffset.ts +21 -0
  65. package/src/editor/index.ts +2 -0
  66. package/src/i18n/defaults.ts +1 -0
  67. package/src/i18n/messages.ts +5 -0
  68. package/src/locales/ru.ts +1 -0
@@ -0,0 +1,53 @@
1
+ ---
2
+ sourceHash: 7fafe8d11f89
3
+ ---
4
+
5
+ # `@olddevs/ui` Reference
6
+
7
+ This Reference is the contract for developers and AI agents who use the package's public API. The source of
8
+ truth for the exports is [`src/index.ts`](../src/index.ts). Component sources, tests and Storybook confirm the
9
+ behavior claims on the pages.
10
+
11
+ ## Start here
12
+
13
+ 1. [Install the package](getting-started.md): imports, CSS and Tailwind 4.
14
+ 2. [Choose a component](component-selection.md): pick the meaning first, not a similar look.
15
+ 3. [Shared conventions](conventions.md): the prop vocabulary, controlled state and the limits of the public API.
16
+ 4. [Glossary](glossary.md): the terms of the kit and the Editor, and what not to confuse them with.
17
+ 5. [Accessibility](accessibility.md): the required rules for names, focus and the keyboard.
18
+ 6. [Themes, tokens and brands](theming.md): the four token layers and how to add your own brand in one CSS block.
19
+ 7. [Palette](palette.md): the palette primitive values and the roles by theme, in one table.
20
+ 8. [Localization](i18n.md): the provider, the message catalog and how to connect to the consumer's `I18nProvider`.
21
+ 9. [Component catalog](components/index.md): the full index of the public components.
22
+
23
+ ## Rules for AI
24
+
25
+ - **Start with `llms.txt`** at the package root. It is an index of all Reference pages, with a description of
26
+ each one. A script builds it from `docs/`, so it cannot fall behind them.
27
+ - Import only the public entry points of the package (the list is in the [glossary](glossary.md)). Do not
28
+ import files from the package's `src/` directly.
29
+ - Before you read a page's API, read "When to use" and "When not to use". Components that look alike can
30
+ express different actions.
31
+ - Do not replace a semantic component with hand-written markup if the package already provides a suitable
32
+ public export.
33
+ - Follow the controlled and uncontrolled contract described in the Reference, and the requirements for the
34
+ label (`label`), focus and Escape. A look in a screenshot does not replace these contracts.
35
+ - Document a change to the public API in the same change as the code. The full maintenance rules are in the
36
+ `CLAUDE.md` of the kit's repository. That file does not ship in the package, and there is no link to it here
37
+ on purpose.
38
+
39
+ ## Scope
40
+
41
+ The documents live in the package repository. **Since 3.8.0 they ship with the package** to the registry: the
42
+ whole `docs/` folder and the `llms.txt` index at the package root (ADR-0044). An agent in someone else's
43
+ repository no longer needs a link to that repository or access to it. The Reference is in
44
+ `node_modules/@olddevs/ui/docs/`, in the version that is installed, not the version that is on `main` now.
45
+
46
+ The kit has a public documentation site, but the index does not link to it. The index has no `https://…`
47
+ links. All its paths are relative and point inside the package.
48
+
49
+ **What the package does not include is the ADRs.** Reference pages refer to decisions by number, `ADR-NNNN`,
50
+ many times. But the decisions are kept apart from the code and are not in the package. This does not stop the
51
+ Reference from being complete: the contract, the API and accessibility are described on the pages themselves.
52
+ "Why it was decided this way" stays in the decision records. The trade-off was made on purpose (ADR-0044). If
53
+ you need the reasoning, ask in the repository's issues.
package/llms.txt CHANGED
@@ -127,6 +127,7 @@
127
127
  - [SortableList](docs/components/content/sortable-list.md): Список, порядок которого задаёт пользователь перетаскиванием.
128
128
  - [EditableList](docs/components/content/editable-list.md): Список, в котором строки правят по месту и переставляют перетаскиванием, а новые добавляют снизу.
129
129
  - [RichTextView](docs/components/content/rich-text-view.md): `RichTextView` — показ сохранённого HTML `RichTextEditor` с той же типографикой, что внутри Редактора.
130
+ - [RichTextToc](docs/components/content/rich-text-toc.md): `RichTextToc` — Оглавление Документа рядом с Просмотром (`RichTextView`) или Редактором в `readOnly`.
130
131
  - [VirtualGrid](docs/components/content/virtual-grid.md): Сетка карточек с виртуализацией: в DOM живут видимые ячейки и небольшой запас вокруг.
131
132
  - [Avatar и AvatarGroup](docs/components/content/avatar.md): `Avatar` показывает фотографию по `src` или, если её нет или она не загрузилась, инициалы и стабильный градиент, вычисленные из имени.
132
133
  - [Data display: `Badge`, `Stat`, `DescriptionList`, `Table`](docs/components/content/data-display.md): Выбирайте компонент по форме и смыслу данных: `Badge` отвечает на «сколько» или «есть ли новое», `Stat` — на один показатель с контекстом, `DescriptionList` — на пары «свойство — значение», `Table` —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olddevs/ui",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Olddevs UI — design system: tokens, themes and accessible UI primitives",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -16,6 +16,7 @@ import {
16
16
  useId,
17
17
  useImperativeHandle,
18
18
  useLayoutEffect,
19
+ useCallback,
19
20
  useMemo,
20
21
  useRef,
21
22
  useState,
@@ -58,6 +59,7 @@ import type { ToolPopoverRequest } from "./core/toolButtons.js";
58
59
  import type { TextCardControl } from "./ui/TextCard.js";
59
60
  import { SelectionPopover } from "./ui/ToolButtonView.js";
60
61
  import { tableChunk } from "./features/table/tableTool.js";
62
+ import { stickyOffset } from "./features/toc/stickyOffset.js";
61
63
  import { replaceWithMarkdown } from "./core/markdown.js";
62
64
  import {
63
65
  documentFromJSON,
@@ -107,6 +109,15 @@ const TextCard = lazy(() =>
107
109
  * `basic`, который меряется бюджетом «старта». Плагин подсветки и сочетания
108
110
  * маленькие и стоят в движке сразу.
109
111
  */
112
+ /*
113
+ Ядро Оглавления (Инструмент `toc`) — ленивым чанком: его включают единицы, а
114
+ полоса со списком и `HoverCard` стоили бы каждому Редактору ~5 kB gzip на
115
+ старте. Чанк приходит, только когда Инструмент включён и движок готов.
116
+ */
117
+ const TocCore = lazy(() =>
118
+ import("./features/toc/RichTextToc.js").then((module) => ({ default: module.TocCore }))
119
+ );
120
+
110
121
  const SearchPanel = lazy(() =>
111
122
  import("./features/search/SearchPanel.js").then((module) => ({ default: module.SearchPanel }))
112
123
  );
@@ -994,6 +1005,52 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
994
1005
  }
995
1006
  : { maxHeight: areaLength };
996
1007
 
1008
+ /*
1009
+ Оглавление в левом поле (Инструмент `toc`, ADR-0068). Полоса липнет под
1010
+ фиксированной Панелью: её высота меряется, потому что Панель переносит
1011
+ ряд кнопок на узком поле. В прокручиваемом поле край — верх области.
1012
+ */
1013
+ const tocOn = enabledTools.has("toc");
1014
+ const [toolbarShell, setToolbarShell] = useState<HTMLDivElement | null>(null);
1015
+ const [toolbarHeight, setToolbarHeight] = useState(0);
1016
+ useEffect(() => {
1017
+ if (!toolbarShell || typeof ResizeObserver === "undefined") {
1018
+ setToolbarHeight(toolbarShell?.offsetHeight ?? 0);
1019
+ return;
1020
+ }
1021
+ const observer = new ResizeObserver(() => setToolbarHeight(toolbarShell.offsetHeight));
1022
+ observer.observe(toolbarShell);
1023
+ return () => observer.disconnect();
1024
+ }, [toolbarShell]);
1025
+ /*
1026
+ Нижний край липкого верха поля: шапка приложения и Панель над Документом.
1027
+ Не `null` никогда, даже без шапки: иначе переход ушёл бы в `scrollIntoView`,
1028
+ а тот двигает и страницу вокруг поля с `maxHeight`. В прокручиваемом поле
1029
+ подсветка и переход считают от верха области и этот край не используют.
1030
+ */
1031
+ const tocStickyTop = useCallback(
1032
+ () => (stickyOffset("--topbar-h") ?? 0) + toolbarHeight,
1033
+ [toolbarHeight]
1034
+ );
1035
+ /*
1036
+ Переход в правке ставит каретку в конец заголовка, как структура документа
1037
+ в Google Docs: с ним можно сразу писать. Прокрутку делает Оглавление, а
1038
+ движок не прокручивает, чтобы не спорить с ним.
1039
+ В `readOnly` каретки нет — фокус на заголовок ставит само Оглавление.
1040
+ */
1041
+ const tocFocusHeading = useCallback((heading: HTMLElement) => {
1042
+ const current = editorRef.current;
1043
+ if (!current || current.isDestroyed || !current.isEditable) return false;
1044
+ const inside = current.view.posAtDOM(heading, 0);
1045
+ const end = inside + current.state.doc.resolve(inside).parent.content.size;
1046
+ current.commands.setTextSelection(end);
1047
+ // `view.focus()`, а не команда `focus`: та ставит фокус кадром позже, и
1048
+ // пункт списка, отпускающий фокус после клика, отдал бы его странице.
1049
+ // ProseMirror фокусирует без прокрутки — прокрутку делает Оглавление.
1050
+ current.view.focus();
1051
+ return true;
1052
+ }, []);
1053
+
997
1054
  /*
998
1055
  `Alt+F10` — фокус на первое доступное действие Панели. Плавающую Панель
999
1056
  клавиша показывает сама, не дожидаясь задержки показа: человек с
@@ -1395,6 +1452,15 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
1395
1452
  const leaveTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
1396
1453
  useEffect(() => () => clearTimeout(leaveTimerRef.current), []);
1397
1454
 
1455
+ /** Цель Оглавления — Документ движка; объект новый с каждым движком. */
1456
+ const tocTarget = useMemo(
1457
+ () => ({
1458
+ get current() {
1459
+ return editor && !editor.isDestroyed ? (editor.view.dom as HTMLElement) : null;
1460
+ },
1461
+ }),
1462
+ [editor]
1463
+ );
1398
1464
  const searchPanelElement = (className: string) =>
1399
1465
  editor &&
1400
1466
  searchPanel && (
@@ -1450,7 +1516,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
1450
1516
  `z-[1]` — внутри поля, а не ступень стека слоёв: Панель не
1451
1517
  оверлей и должна оставаться ниже любого из них.
1452
1518
  */
1453
- <div className={TOOLBAR_SHELL_CLASS}>
1519
+ <div ref={setToolbarShell} className={TOOLBAR_SHELL_CLASS}>
1454
1520
  <EditorToolbar
1455
1521
  editor={editor}
1456
1522
  tools={enabledTools}
@@ -1511,7 +1577,37 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
1511
1577
  style={areaStyle}
1512
1578
  >
1513
1579
  {/* В HTML-режиме Документ скрыт, но смонтирован: движок и история живы. */}
1514
- <EditorContent editor={editor} hidden={sourceOpen} />
1580
+ {tocOn ? (
1581
+ /*
1582
+ Левое поле Оглавления: колонка есть, только пока есть полоса —
1583
+ `TocCore` с меньше чем двумя заголовками не рисует ничего, и
1584
+ текст стоит у края, как без Инструмента (ADR-0068, п. 2).
1585
+ */
1586
+ <div className="flex items-start">
1587
+ {editor && !sourceOpen && (
1588
+ <Suspense fallback={null}>
1589
+ <TocCore
1590
+ target={tocTarget}
1591
+ stickyTop={tocStickyTop}
1592
+ focusHeading={tocFocusHeading}
1593
+ alignToFirstHeading
1594
+ data-toc-gutter=""
1595
+ className="sticky mr-3 shrink-0"
1596
+ style={{
1597
+ top: scrolls
1598
+ ? "0.5rem"
1599
+ : `calc(var(--topbar-h, 0px) + ${toolbarHeight}px + 0.5rem)`,
1600
+ }}
1601
+ />
1602
+ </Suspense>
1603
+ )}
1604
+ <div className="min-w-0 flex-1">
1605
+ <EditorContent editor={editor} hidden={sourceOpen} />
1606
+ </div>
1607
+ </div>
1608
+ ) : (
1609
+ <EditorContent editor={editor} hidden={sourceOpen} />
1610
+ )}
1515
1611
  {!editor && (
1516
1612
  /*
1517
1613
  Скелет (ADR-0058, п. 11.4): до Готовности и при сбое загрузчика —
@@ -1737,7 +1833,15 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
1737
1833
  ref={hiddenRef}
1738
1834
  type="hidden"
1739
1835
  name={name}
1740
- defaultValue={initial}
1836
+ /*
1837
+ Значение скрытого поля пишет Редактор (`emit`, эффекты движка), а
1838
+ не React. Но React на КАЖДОМ обновлении заново ставит `defaultValue`,
1839
+ а у `type="hidden"` это и есть его значение: любой рендер родителя
1840
+ откатывал бы поле формы к входящей строке. Поэтому после монтажа в
1841
+ `defaultValue` уходит то, что уже лежит в поле, — React видит равенство
1842
+ и ничего не трогает. Серверный рендер и первый кадр несут `initial`.
1843
+ */
1844
+ defaultValue={hiddenRef.current?.value ?? initial}
1741
1845
  // Выключенное поле не уходит в форму — как нативное. Поле только
1742
1846
  // для чтения уходит: это показанное значение, а не недоступный
1743
1847
  // контрол.
@@ -4,10 +4,11 @@ import { createNodeFromContent, type JSONContent } from "@tiptap/core";
4
4
  import { closeHistory } from "@tiptap/pm/history";
5
5
  import type { Fragment } from "@tiptap/pm/model";
6
6
  import type { Editor } from "@tiptap/react";
7
- import type { RefObject } from "react";
8
7
  import { textBetween } from "./atomText.js";
9
8
  import { isRichTextDocument, type RichTextDocument } from "./jsonDocument.js";
10
9
 
10
+ export { notifyHandleEditor, watchHandleEditor } from "./handleWatchers.js";
11
+
11
12
  /**
12
13
  * Плоские методы Хэндла (ADR-0058, раздел 8): то, что экран делает с Редактором
13
14
  * чаще всего. Остальное — `editor` (команды Меток, Блоков и своих Инструментов).
@@ -287,25 +288,3 @@ export function createHandleMethods(host: HandleHost): RichTextEditorHandleMetho
287
288
  canRedo: () => available("redo"),
288
289
  };
289
290
  }
290
-
291
- /*
292
- Хук-селектор не знает, когда у `ref` появится движок: Редактор создаёт его
293
- после первого рендера экрана и перерисовывается сам, а экран — нет. Поэтому
294
- подписки живут по самому объекту `ref`, который есть с первого рендера, и
295
- Редактор сообщает об смене движка по нему.
296
- */
297
- const watchers = new WeakMap<object, Set<() => void>>();
298
-
299
- /** Подписка хука на смену движка у Редактора, привязанного к этому `ref`. */
300
- export function watchHandleEditor(ref: RefObject<unknown>, listener: () => void): () => void {
301
- let set = watchers.get(ref);
302
- if (!set) watchers.set(ref, (set = new Set()));
303
- set.add(listener);
304
- return () => void set.delete(listener);
305
- }
306
-
307
- /** Редактор сообщает: движок у `ref` появился или сменился. */
308
- export function notifyHandleEditor(ref: unknown): void {
309
- if (typeof ref !== "object" || ref === null) return;
310
- watchers.get(ref)?.forEach((listener) => listener());
311
- }
@@ -0,0 +1,27 @@
1
+ import type { RefObject } from "react";
2
+
3
+ /*
4
+ Хук-селектор не знает, когда у `ref` появится движок: Редактор создаёт его
5
+ после первого рендера экрана и перерисовывается сам, а экран — нет. Поэтому
6
+ подписки живут по самому объекту `ref`, который есть с первого рендера, и
7
+ Редактор сообщает об смене движка по нему.
8
+
9
+ Модуль вынесен из `handle.ts` и не знает движка: подписаться на смену движка
10
+ нужно и `RichTextToc`, а ему Tiptap в бандл не нужен.
11
+ */
12
+
13
+ const watchers = new WeakMap<object, Set<() => void>>();
14
+
15
+ /** Подписка хука на смену движка у Редактора, привязанного к этому `ref`. */
16
+ export function watchHandleEditor(ref: RefObject<unknown>, listener: () => void): () => void {
17
+ let set = watchers.get(ref);
18
+ if (!set) watchers.set(ref, (set = new Set()));
19
+ set.add(listener);
20
+ return () => void set.delete(listener);
21
+ }
22
+
23
+ /** Редактор сообщает: движок у `ref` появился или сменился. */
24
+ export function notifyHandleEditor(ref: unknown): void {
25
+ if (typeof ref !== "object" || ref === null) return;
26
+ watchers.get(ref)?.forEach((listener) => listener());
27
+ }
@@ -50,6 +50,11 @@
50
50
  * разметку решает Схема, и режим её не расширяет. В `basic` его нет: в
51
51
  * комментарии кнопка «HTML» без явного запроса экрана — лишняя.
52
52
  *
53
+ * `toc` — Оглавление в левом поле рамки (ADR-0068): полоса и список
54
+ * `RichTextToc`, липкие под Панелью или в прокручиваемом поле. Своей Схемы нет.
55
+ * В пресеты не входит — ни в `full`, ни в `basic`: оно меняет раскладку поля,
56
+ * и обновление кита не должно сдвигать текст там, где его не просили.
57
+ *
53
58
  * `search` — поиск и замена по Документу: кнопка «Поиск»
54
59
  * фиксированной Панели и `Mod+F` в поле открывают панель в углу поля. Своей
55
60
  * Схемы нет, как у `clearFormatting`. Есть и в `basic`: искать по длинному
@@ -85,6 +90,7 @@ export const TOOLS = [
85
90
  "source",
86
91
  "search",
87
92
  "insertMenu",
93
+ "toc",
88
94
  ] as const;
89
95
 
90
96
  /** Имя инструмента Редактора. Список и смысл каждого — на странице компонента. */
@@ -104,7 +110,8 @@ export type RichTextEditorPreset = "full" | "basic";
104
110
  * markdown-шорткатом, чем через меню.
105
111
  */
106
112
  export const PRESETS: Record<RichTextEditorPreset, readonly Tool[]> = {
107
- full: TOOLS,
113
+ // Все встроенные, кроме Оглавления: его включают только явно (ADR-0068).
114
+ full: TOOLS.filter((tool) => tool !== "toc"),
108
115
  basic: [
109
116
  "history",
110
117
  "bold",
@@ -122,7 +129,7 @@ export const PRESETS: Record<RichTextEditorPreset, readonly Tool[]> = {
122
129
  /**
123
130
  * Пресеты наружу — массивами (ADR-0058, п. 1.2): свой набор собирается из них,
124
131
  * например `[...RICH_TEXT_PRESETS.full, mergeTagTool]`. Копии заморожены:
125
- * `PRESETS.full` — это `TOOLS`, и правка массива потребителем меняла бы набор
132
+ * `PRESETS.full` — почти весь `TOOLS`, и правка массива потребителем меняла бы набор
126
133
  * кита во всех Редакторах страницы. Состав пресета — не контракт (ADR-0058,
127
134
  * п. 12.5): стабильный набор — точный список имён.
128
135
  */