@olddevs/ui 4.3.0 → 4.5.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 (182) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/editor/RichTextEditor.d.ts +11 -4
  3. package/dist/editor/RichTextEditor.d.ts.map +1 -1
  4. package/dist/editor/RichTextEditor.js +124 -60
  5. package/dist/editor/RichTextEditor.js.map +1 -1
  6. package/dist/editor/RichTextView.d.ts +2 -1
  7. package/dist/editor/RichTextView.d.ts.map +1 -1
  8. package/dist/editor/RichTextView.js.map +1 -1
  9. package/dist/editor/core/containers.d.ts +2 -6
  10. package/dist/editor/core/containers.d.ts.map +1 -1
  11. package/dist/editor/core/containers.js +2 -1
  12. package/dist/editor/core/containers.js.map +1 -1
  13. package/dist/editor/core/customTool.d.ts +8 -2
  14. package/dist/editor/core/customTool.d.ts.map +1 -1
  15. package/dist/editor/core/customTool.js.map +1 -1
  16. package/dist/editor/core/customToolEngine.d.ts +9 -1
  17. package/dist/editor/core/customToolEngine.d.ts.map +1 -1
  18. package/dist/editor/core/customToolEngine.js +12 -0
  19. package/dist/editor/core/customToolEngine.js.map +1 -1
  20. package/dist/editor/core/extensions.d.ts.map +1 -1
  21. package/dist/editor/core/extensions.js +1 -1
  22. package/dist/editor/core/extensions.js.map +1 -1
  23. package/dist/editor/core/handle.d.ts +18 -5
  24. package/dist/editor/core/handle.d.ts.map +1 -1
  25. package/dist/editor/core/handle.js +2 -13
  26. package/dist/editor/core/handle.js.map +1 -1
  27. package/dist/editor/core/handleWatchers.d.ts +36 -0
  28. package/dist/editor/core/handleWatchers.d.ts.map +1 -0
  29. package/dist/editor/core/handleWatchers.js +29 -0
  30. package/dist/editor/core/handleWatchers.js.map +1 -0
  31. package/dist/editor/core/nodeNames.d.ts +7 -0
  32. package/dist/editor/core/nodeNames.d.ts.map +1 -0
  33. package/dist/editor/core/nodeNames.js +2 -0
  34. package/dist/editor/core/nodeNames.js.map +1 -0
  35. package/dist/editor/core/schema.d.ts.map +1 -1
  36. package/dist/editor/core/schema.js +4 -2
  37. package/dist/editor/core/schema.js.map +1 -1
  38. package/dist/editor/core/toolCommands.d.ts +78 -0
  39. package/dist/editor/core/toolCommands.d.ts.map +1 -0
  40. package/dist/editor/core/toolCommands.js +2 -0
  41. package/dist/editor/core/toolCommands.js.map +1 -0
  42. package/dist/editor/core/toolDefinition.d.ts +139 -0
  43. package/dist/editor/core/toolDefinition.d.ts.map +1 -0
  44. package/dist/editor/core/toolDefinition.js +28 -0
  45. package/dist/editor/core/toolDefinition.js.map +1 -0
  46. package/dist/editor/core/toolDescriptor.d.ts +71 -2
  47. package/dist/editor/core/toolDescriptor.d.ts.map +1 -1
  48. package/dist/editor/core/toolDescriptor.js +6 -2
  49. package/dist/editor/core/toolDescriptor.js.map +1 -1
  50. package/dist/editor/core/toolOptions.d.ts +157 -0
  51. package/dist/editor/core/toolOptions.d.ts.map +1 -0
  52. package/dist/editor/core/toolOptions.js +21 -0
  53. package/dist/editor/core/toolOptions.js.map +1 -0
  54. package/dist/editor/core/toolState.d.ts +46 -0
  55. package/dist/editor/core/toolState.d.ts.map +1 -0
  56. package/dist/editor/core/toolState.js +2 -0
  57. package/dist/editor/core/toolState.js.map +1 -0
  58. package/dist/editor/core/tools.d.ts +10 -3
  59. package/dist/editor/core/tools.d.ts.map +1 -1
  60. package/dist/editor/core/tools.js +2 -1
  61. package/dist/editor/core/tools.js.map +1 -1
  62. package/dist/editor/core/viewIsland.d.ts.map +1 -1
  63. package/dist/editor/core/viewIsland.js +2 -1
  64. package/dist/editor/core/viewIsland.js.map +1 -1
  65. package/dist/editor/features/builtins.d.ts +9 -4
  66. package/dist/editor/features/builtins.d.ts.map +1 -1
  67. package/dist/editor/features/builtins.js +36 -7
  68. package/dist/editor/features/builtins.js.map +1 -1
  69. package/dist/editor/features/search/SearchPanel.d.ts.map +1 -1
  70. package/dist/editor/features/search/SearchPanel.js +26 -12
  71. package/dist/editor/features/search/SearchPanel.js.map +1 -1
  72. package/dist/editor/features/search/revealMatch.d.ts +16 -0
  73. package/dist/editor/features/search/revealMatch.d.ts.map +1 -1
  74. package/dist/editor/features/search/revealMatch.js +22 -0
  75. package/dist/editor/features/search/revealMatch.js.map +1 -1
  76. package/dist/editor/features/search/searchPlugin.d.ts +8 -3
  77. package/dist/editor/features/search/searchPlugin.d.ts.map +1 -1
  78. package/dist/editor/features/search/searchPlugin.js +17 -6
  79. package/dist/editor/features/search/searchPlugin.js.map +1 -1
  80. package/dist/editor/features/search/searchSnapshot.d.ts +14 -0
  81. package/dist/editor/features/search/searchSnapshot.d.ts.map +1 -0
  82. package/dist/editor/features/search/searchSnapshot.js +17 -0
  83. package/dist/editor/features/search/searchSnapshot.js.map +1 -0
  84. package/dist/editor/features/search/searchTool.d.ts +3 -0
  85. package/dist/editor/features/search/searchTool.d.ts.map +1 -1
  86. package/dist/editor/features/search/searchTool.js +30 -1
  87. package/dist/editor/features/search/searchTool.js.map +1 -1
  88. package/dist/editor/features/toc/RichTextToc.d.ts +91 -0
  89. package/dist/editor/features/toc/RichTextToc.d.ts.map +1 -0
  90. package/dist/editor/features/toc/RichTextToc.js +417 -0
  91. package/dist/editor/features/toc/RichTextToc.js.map +1 -0
  92. package/dist/editor/features/toc/documentHeadings.d.ts +30 -0
  93. package/dist/editor/features/toc/documentHeadings.d.ts.map +1 -0
  94. package/dist/editor/features/toc/documentHeadings.js +20 -0
  95. package/dist/editor/features/toc/documentHeadings.js.map +1 -0
  96. package/dist/editor/features/toc/headings.d.ts +35 -0
  97. package/dist/editor/features/toc/headings.d.ts.map +1 -0
  98. package/dist/editor/features/toc/headings.js +27 -0
  99. package/dist/editor/features/toc/headings.js.map +1 -0
  100. package/dist/editor/features/toc/stickyOffset.d.ts +3 -0
  101. package/dist/editor/features/toc/stickyOffset.d.ts.map +1 -0
  102. package/dist/editor/features/toc/stickyOffset.js +14 -0
  103. package/dist/editor/features/toc/stickyOffset.js.map +1 -0
  104. package/dist/editor/features/toc/tocSnapshot.d.ts +10 -0
  105. package/dist/editor/features/toc/tocSnapshot.d.ts.map +1 -0
  106. package/dist/editor/features/toc/tocSnapshot.js +8 -0
  107. package/dist/editor/features/toc/tocSnapshot.js.map +1 -0
  108. package/dist/editor/features/toc/tocTool.d.ts +6 -0
  109. package/dist/editor/features/toc/tocTool.d.ts.map +1 -0
  110. package/dist/editor/features/toc/tocTool.js +21 -0
  111. package/dist/editor/features/toc/tocTool.js.map +1 -0
  112. package/dist/editor/hooks/useRichTextToolState.d.ts +43 -0
  113. package/dist/editor/hooks/useRichTextToolState.d.ts.map +1 -0
  114. package/dist/editor/hooks/useRichTextToolState.js +77 -0
  115. package/dist/editor/hooks/useRichTextToolState.js.map +1 -0
  116. package/dist/editor/index.d.ts +10 -0
  117. package/dist/editor/index.d.ts.map +1 -1
  118. package/dist/editor/index.js +4 -0
  119. package/dist/editor/index.js.map +1 -1
  120. package/dist/i18n/defaults.d.ts.map +1 -1
  121. package/dist/i18n/defaults.js +1 -0
  122. package/dist/i18n/defaults.js.map +1 -1
  123. package/dist/i18n/messages.d.ts +5 -0
  124. package/dist/i18n/messages.d.ts.map +1 -1
  125. package/dist/locales/ru.d.ts.map +1 -1
  126. package/dist/locales/ru.js +1 -0
  127. package/dist/locales/ru.js.map +1 -1
  128. package/docs/api/editor-extension.en.md +223 -23
  129. package/docs/api/editor-extension.md +224 -22
  130. package/docs/api/messages.en.md +743 -0
  131. package/docs/api/messages.md +1 -0
  132. package/docs/api/migration-notes.en.md +36 -1
  133. package/docs/api/migration-notes.md +35 -0
  134. package/docs/api/utilities.en.md +233 -0
  135. package/docs/components/content/rich-text-toc.en.md +132 -0
  136. package/docs/components/content/rich-text-toc.md +131 -0
  137. package/docs/components/index.en.md +3 -2
  138. package/docs/components/index.md +2 -1
  139. package/docs/components/inputs/rich-text-editor.en.md +277 -43
  140. package/docs/components/inputs/rich-text-editor.md +247 -11
  141. package/docs/conventions.en.md +318 -0
  142. package/docs/getting-started.en.md +7 -7
  143. package/docs/getting-started.md +6 -6
  144. package/docs/glossary.en.md +52 -47
  145. package/docs/glossary.md +51 -46
  146. package/docs/index.en.md +53 -0
  147. package/llms.txt +1 -0
  148. package/package.json +1 -1
  149. package/src/editor/RichTextEditor.tsx +197 -38
  150. package/src/editor/RichTextView.tsx +2 -1
  151. package/src/editor/core/containers.ts +8 -6
  152. package/src/editor/core/customTool.ts +8 -2
  153. package/src/editor/core/customToolEngine.ts +20 -1
  154. package/src/editor/core/extensions.ts +2 -1
  155. package/src/editor/core/handle.ts +26 -23
  156. package/src/editor/core/handleWatchers.ts +72 -0
  157. package/src/editor/core/nodeNames.ts +7 -0
  158. package/src/editor/core/schema.ts +5 -2
  159. package/src/editor/core/toolCommands.ts +85 -0
  160. package/src/editor/core/toolDefinition.ts +208 -0
  161. package/src/editor/core/toolDescriptor.ts +94 -3
  162. package/src/editor/core/toolOptions.ts +211 -0
  163. package/src/editor/core/toolState.ts +56 -0
  164. package/src/editor/core/tools.ts +12 -3
  165. package/src/editor/core/viewIsland.ts +5 -4
  166. package/src/editor/features/builtins.ts +72 -13
  167. package/src/editor/features/search/SearchPanel.tsx +38 -14
  168. package/src/editor/features/search/revealMatch.ts +45 -0
  169. package/src/editor/features/search/searchPlugin.ts +40 -11
  170. package/src/editor/features/search/searchSnapshot.ts +29 -0
  171. package/src/editor/features/search/searchTool.ts +51 -1
  172. package/src/editor/features/toc/RichTextToc.tsx +810 -0
  173. package/src/editor/features/toc/documentHeadings.ts +59 -0
  174. package/src/editor/features/toc/headings.ts +77 -0
  175. package/src/editor/features/toc/stickyOffset.ts +21 -0
  176. package/src/editor/features/toc/tocSnapshot.ts +24 -0
  177. package/src/editor/features/toc/tocTool.ts +41 -0
  178. package/src/editor/hooks/useRichTextToolState.ts +153 -0
  179. package/src/editor/index.ts +21 -0
  180. package/src/i18n/defaults.ts +1 -0
  181. package/src/i18n/messages.ts +5 -0
  182. package/src/locales/ru.ts +1 -0
@@ -0,0 +1,318 @@
1
+ ---
2
+ sourceHash: 84104315608a
3
+ ---
4
+
5
+ # General conventions for `@olddevs/ui`
6
+
7
+ ## Public contract
8
+
9
+ The public API is the runtime exports from [`src/index.ts`](../src/index.ts), the types, the entry points
10
+ described in the documentation, and `styles.css`. The sources in `src/components/` prove the behavior, but they
11
+ are not an import path that consumers may use.
12
+
13
+ ## Semantic prop vocabulary
14
+
15
+ Unless a specific page says otherwise, primitives use three independent axes:
16
+
17
+ | Prop | Values | Question it answers |
18
+ | --------- | --------------------------------------------------- | ---------------------------- |
19
+ | `size` | `sm`, `md`, `lg` | How large is the element? |
20
+ | `tone` | `neutral`, `accent`, `success`, `warning`, `danger` | What does the state mean? |
21
+ | `variant` | `solid`, `soft`, `outline`, `ghost` | How prominent is this value? |
22
+
23
+ States are named positively: `selected`, `disabled`, `loading`. A native ARIA state may use its own correct name,
24
+ for example `checked` on a switch. Do not create alternatives such as `kind`, `status` or negative boolean props
25
+ without a separate public decision. `active` already has one meaning, see below.
26
+
27
+ Props outside the three axes repeat across several components. One concept, one name: a second component with the
28
+ same meaning takes the name from here, and does not invent its own.
29
+
30
+ | Prop | Meaning | Where |
31
+ | ------------ | ------------------------------------------------------------------------------------------- | ------------------------------------- |
32
+ | `appearance` | The form of a variant: the markup changes, not the prominence of the tone | `RadioGroup`, `CheckboxGroup` |
33
+ | `active` | The current navigation item: `aria-current="page"` | `NavItem`, `MobileTabBarItem` |
34
+ | `activeKey` | The same for a data collection: the key of the current node, `aria-current="page"` | `TreeView` |
35
+ | `level` | Heading level, `2`–`4`: the component renders a real `<h2>`–`<h4>` | `SectionHeader`, `NotificationCenter` |
36
+ | `kind` | The kind of **data**, not of an element: what value the field describes and how to enter it | `FilterField` (ADR-0063) |
37
+
38
+ - **`kind` on `FilterField` is a public decision (ADR-0063), not a workaround for the axis.**
39
+ `FilterField` describes a data field, not a UI element: `"values"`, `"number-range"` and `"date-range"` say
40
+ what value the field holds and what input sets it. It is not a presentation (`variant`) or a variant form
41
+ (`appearance`): a description of data has no tone and no markup. Components that draw an element still do not
42
+ get `kind`.
43
+ - **`appearance`, not `variant`.** `variant` is the axis for how a tone is presented, with a closed set
44
+ `solid|soft|outline|ghost` across the whole kit. `appearance="card"` changes the form of the variant itself:
45
+ the border, the indicator and the description inside the label. Under `variant`, such a value would give the
46
+ axis a second meaning: `variant="card"` on one component would sit next to `variant="soft"` on another.
47
+ - **`active`, not `selected`.** `selected` is a choice in a set (`aria-selected`, `aria-checked`): the user
48
+ changes it. The current section is `aria-current`: the route sets it, and the item stays a link. `active` has
49
+ no other values, and it does not become a synonym for `selected`. Where there are no item elements but keyed
50
+ data (`TreeView`), the same concept is called `activeKey`, after `selectedKeys` and `expandedKeys`: one value
51
+ instead of a set.
52
+ - **`level`, not `headingLevel`.** The level depends on where the component is placed, and the consumer knows
53
+ that. `SectionHeader` already had a prop called `level`. A second name for the same concept would silently
54
+ drift from it.
55
+
56
+ ## Accessible name
57
+
58
+ The rule comes from `Toolbar`, `Carousel`, `MobileTabBar` and `TagGroup`. Before it, these components handled the
59
+ required name in different ways: in one, `label` was shown on screen, and in another it went into an attribute.
60
+
61
+ - **`label` is only a visible caption.** A prop with this name draws text on screen. A name that is not drawn is
62
+ set with its own ARIA attribute.
63
+ - **Without a visible caption, use exactly one of `aria-label` and `aria-labelledby`.** The type is
64
+ [`AccessibleName`](../src/lib/accessible-name.ts), a union with `never`: TypeScript rejects a call with no name
65
+ and a call with both. Two sources of one name drift apart silently when you edit.
66
+ - **A name with a default from the catalog** uses `Partial<AccessibleName>`: you can pass nothing, but not both
67
+ (`MobileTabBar`). The default is announced even when an empty string is passed.
68
+ - **Visible caption or attribute:** `{ label; "aria-label"?: never;
69
+ "aria-labelledby"?: never } | ({ label?: never } & AccessibleName)` (`TagGroup`). A caption together with an
70
+ attribute is not allowed: the attribute would override the visible text, and a screen reader would give the
71
+ component a different name than the one on screen.
72
+ - The test locks the shape with `@ts-expect-error`: for "no name" and for "both at once".
73
+
74
+ Components released before this rule use `label` as the name string without a visible caption (`SideNav`,
75
+ `TopBar`, `FilterBar`, `ScrollArea` and others). Renaming it there is a breaking change and a separate decision.
76
+ A new component follows the rule.
77
+
78
+ ## Empty value
79
+
80
+ An empty string is neither a caption nor a name. The rule and its history are in
81
+ [`src/lib/empty.ts`](../src/lib/empty.ts). For a prop of type `string`, write the fallback with `||`, not `??`.
82
+ For `ReactNode`, use `hasContent`, not `!= null`. `??` and `!= null` let `""` through: the component renders an
83
+ empty node, stays without a name, or labels a button "Удалить: ". Each such branch is covered by a test with an
84
+ empty string.
85
+
86
+ ## What the tests check
87
+
88
+ A test renders the component as a consumer does (Testing Library and user-event), and checks what can be observed:
89
+ roles, names, ARIA attributes, focus, handler calls and text. Internal state and hooks are not tested.
90
+
91
+ **A class is tested only when it is the observable contract that jsdom would not otherwise see**: jsdom does not
92
+ compute the cascade, the layout or `env()`. Examples are the tone from the dictionary (`TONE_TOKENS`,
93
+ `CHOICE_CARD_SELECTED_BORDER`), the slide width and column count from the component dictionary, and the bar
94
+ background inside and outside the slot (`BACKDROP_CLASS` of `MobileTabBar`). Take the expected value **from the
95
+ dictionary, not as a literal**: a literal checks that nobody touched the string, while the dictionary checks that
96
+ the component picked the right cell.
97
+
98
+ Arbitrary layout classes (`h-7`, `rounded-full`, `ring-bg-1`) are not locked by a test. Visual snapshots guard
99
+ them, and [`tests/class-coverage.test.ts`](../tests/class-coverage.test.ts) checks through a matrix that each
100
+ class reaches the CSS. If such a class has an observable effect (a description inside the label, a node in the
101
+ DOM, an attribute), test the effect.
102
+
103
+ ## State management
104
+
105
+ Components mostly take the value and the callback from the consumer. The consumer is the only owner of domain
106
+ state, the request, the route and validation. On a component page, always state:
107
+
108
+ - the shape of the value and of the empty value (`null`, an array, a string, and so on);
109
+ - controlled props and the callback;
110
+ - allowed state transitions;
111
+ - the behavior of disabled and loading;
112
+ - the integration with a native form, if there is one.
113
+
114
+ Do not infer the API from how a story looks: check the type and the source.
115
+
116
+ ## How to read a component page
117
+
118
+ Every page must contain:
119
+
120
+ 1. A short answer for AI and the limits of use.
121
+ 2. The closest alternatives and what not to do.
122
+ 3. A minimal working example with the correct import.
123
+ 4. An explanation of the public props, states and events.
124
+ 5. Accessibility, the keyboard, focus and the boundary with forms.
125
+ 6. Anti-patterns, special cases and links to the source and Storybook.
126
+
127
+ ## Source layout
128
+
129
+ A component folder in `src/components/<category>/` matches its page folder in `docs/components/<category>/`.
130
+ There are eight categories (`actions`, `inputs`, `date-time`, `overlays`, `navigation`, `content`, `table`,
131
+ `app-shell`). There are no files at the root of `src/components/`. Inside a category, files are flat: an internal
132
+ part lives next to its owner. When a component changes its category, its page changes with it.
133
+
134
+ In `src/editor/`, only the entry points stay at the root: `RichTextEditor`, `RichTextView` and their stories.
135
+ Everything else is in subfolders: `core/` holds the engine and the Tool registry, `ui/` holds the shared glue code,
136
+ `styles/` holds the styling, `hooks/` holds the hooks, and `features/<feature>/` holds the node, commands, Tool and
137
+ UI of one feature.
138
+
139
+ `index.ts` files are not allowed in categories, layers or features: imports go directly to the file. Barrel files
140
+ are allowed only as entry points of the package. `tests/structure.test.ts` checks the layout.
141
+
142
+ ## Changing the API
143
+
144
+ When you add or change a runtime export, update its page, its entry in the catalog and the registry in the same
145
+ change set. A breaking change also needs an entry in `docs/api/migration-notes.md` with the action for the
146
+ consumer. Do not hide a change behind behavior that looks similar.
147
+
148
+ ## User-visible strings
149
+
150
+ Every label that a user sees or hears goes through the message catalog: `prop ?? messages.family.key`. The prop
151
+ stays: it is a targeted override, not the only way.
152
+
153
+ A string literal in JSX or in a prop default fails `npm test`
154
+ ([`tests/i18n-gate.test.ts`](../tests/i18n-gate.test.ts)). The exceptions are an explicit list, with the reason
155
+ given in the gate itself (three reasons are described below), not a regexp.
156
+
157
+ To add a key, see [i18n.md](i18n.md).
158
+
159
+ ## Language: what is in Russian, what is in English
160
+
161
+ The boundary follows the audience, not the file type: this is the decision in ADR-0034.
162
+
163
+ | In Russian: the kit explains to itself | In English: the kit shows to the outside |
164
+ | -------------------------------------- | ----------------------------------------------------------- |
165
+ | Comments in the sources | Showcase: sections, story names, demo content, `aria-label` |
166
+ | This documentation and the ADRs | Tests: names, fixtures, data |
167
+ | Text in the Figma mockups | Script output to the console |
168
+
169
+ Comments stay in Russian on purpose, and the i18n gate does not check them. A Russian comment above an English
170
+ story is normal, not unfinished work.
171
+
172
+ Russian is legitimate in tests where it is the **subject** of the check: the plural and case forms and the catalog
173
+ words live in `tests/locale-ru.test.tsx`. In other tests, the expected values come from the English fallback.
174
+
175
+ The gate's allowlist covers **three reasons, not three files**: the keyboard layout as input data (`Keyboard.tsx`
176
+ and its test), the Russian catalog (`locales/ru.ts`), and a Russian word as the subject of a check
177
+ (`locale-ru.test.tsx`, plus the `SortableList` scenario in `tests-visual/interactions.spec.ts`, where react-aria
178
+ localizes the announcements itself). A fourth reason is a case for review, not a reason to change the number.
179
+
180
+ ## Showcase story names
181
+
182
+ Section and story names are in Latin script. They make up the story `id`, and the `id` gives the name of the
183
+ snapshot file and its address in `tests-visual`. Renaming costs a baseline regeneration, so pick the name once.
184
+
185
+ A component's section is its category: the folder of its page in the Reference and of its source (ADR-0060). The
186
+ story `src/components/overlays/Dialog.stories.tsx` is named `Overlays/Dialog`. Editor stories take their section
187
+ from the folder of their own page: `Inputs/RichTextEditor`, `Content/RichTextView`.
188
+
189
+ | Section | What is inside |
190
+ | ---------------- | ----------------------------------------------------------- |
191
+ | `Actions/` | Actions and feedback: `Actions/Button`, `Actions/Alert` |
192
+ | `Inputs/` | Fields and choice: `Inputs/Select`, `Inputs/RichTextEditor` |
193
+ | `Date and time/` | Calendars and date and time pickers |
194
+ | `Overlays/` | Dialogs, drawers, popovers, menus, tooltips |
195
+ | `Navigation/` | Tabs, breadcrumbs, page and section headers |
196
+ | `Content/` | Typography, cards, data, states, layout |
197
+ | `Table/` | The data table and its panels |
198
+ | `App shell/` | App shell: `AppShell`, `SideNav`, `TopBar` |
199
+ | `Foundations/` | Basics: icons, motion |
200
+ | `Screens/` | Assembled screens |
201
+
202
+ **Dictionary of repeated names.** A name that appears a second time must be added here. Otherwise the same case
203
+ gets two different names in two files, and they drift apart silently.
204
+
205
+ | Name | When |
206
+ | ----------------- | ------------------------------------------------------ |
207
+ | `Playground` | The main story with controls |
208
+ | `Matrix` | All variants side by side; captured by the visual test |
209
+ | `Empty` | Empty state |
210
+ | `Disabled` | Disabled state |
211
+ | `WithError` | Validation error |
212
+ | `WithAction` | With an action button |
213
+ | `WithSelection` | With a selection |
214
+ | `Sizes` | Size scale |
215
+ | `Horizontal` | Horizontal layout |
216
+ | `Compact` | Dense layout |
217
+ | `SizesAndStates` | Sizes and states side by side |
218
+ | `Collapsed` | Collapsed navigation state |
219
+ | `Submitting` | The form is being submitted |
220
+ | `InForm` | The component inside a form |
221
+ | `AsPage` | Full-screen variant |
222
+ | `WithoutTitle` | Without a title |
223
+ | `States` | A set of states side by side |
224
+ | `Filled` | The field is completely filled |
225
+ | `Open` | An expanded list or calendar |
226
+ | `Tones` | Tone scale |
227
+ | `WithLabel` | With a visible label |
228
+ | `Cards` | Choice variants shown as cards (`appearance="card"`) |
229
+ | `CardsHorizontal` | Choice cards in a row |
230
+ | `CardsStates` | States of choice cards side by side |
231
+ | `CardsTones` | Tone scale of choice cards |
232
+ | `Controlled` | Controlled mode: the story holds the value |
233
+ | `Wrapping` | Wrapping at a narrow width |
234
+ | `Vertical` | Vertical layout |
235
+ | `Badges` | All kinds of badge side by side |
236
+
237
+ Single-word names describe the **case**, not translate a word: a story about how a component differs from its
238
+ neighbors is called `VersusNeighbours`, not a transliteration of a Russian heading.
239
+
240
+ **Snapshot width is set by a story tag.** The visual test takes a snapshot of a story at 1280×900. A story that
241
+ shows a narrow screen sets the tag `tags: ["viewport-phone"]` on itself, and it is captured at 375×812 with an
242
+ emulated screen cutout at the bottom (34px). It is a tag, not a parameter: the spec reads stories from
243
+ `storybook-static/index.json`, which has no parameters. The first such story is `AppShell` → `WithBottomBar`: at
244
+ 1280px the slot hides the bar.
245
+
246
+ **A lazy chunk that loads after the story is shown is also set by a tag.** The snapshot waits for two identical
247
+ frames in a row. While a chunk is in transit, the screen does not change, and the snapshot takes a story that is
248
+ not fully loaded for a finished one. A story part of which arrives as a lazy chunk after it is shown sets the tag
249
+ `tags: ["network-idle"]`: the snapshot and the axe audit wait for the network to be quiet. The first such stories
250
+ are `RichTextEditor` → `TextCardOpen` and `TextCardRussian`: the chunk of the Text card is requested only after the
251
+ engine reaches Readiness.
252
+
253
+ **A story name must not match the name of the imported component.** A story about `Separator` in
254
+ `Separator.stories.tsx` must not be called `Separator`: the export shadows the import, and `tsc` fails with
255
+ `cannot be used as a JSX component`. It fails only where the component is used as JSX. Hence the names
256
+ `Separators`, `Scrolling`, `HiddenContent`, `BasicTable`.
257
+
258
+ ## Warnings in development mode
259
+
260
+ The first such warning is on `NotificationItem`, which gets both `onAction` and `actions`. The rules come from it,
261
+ and they are written here so the next author does not invent their own.
262
+
263
+ **When to warn.** Only when a contract is broken that **types catch but a consumer in JS does not**, and the
264
+ component silently picks one reading of it (mutually exclusive props, where one of them is ignored). Do not warn
265
+ about style, non-optimal use, deprecation (for that, use `migration-notes.md`), or what React or `react-aria`
266
+ already catches itself. A violation after which the component cannot work is not a warning but an exception (like
267
+ `HoverCardContent` outside `HoverCard`).
268
+
269
+ **How.**
270
+
271
+ - Through [`devWarn(message)`](../src/lib/dev-warn.ts), not a bare `console.warn`. The helper shows the message
272
+ **once** (the key is the text) and **does not write in production**. The call sits in the render body, so
273
+ showing once is required: a list row renders hundreds of times, and `StrictMode` calls the render twice.
274
+ - The text is **in English**, because it is output to the consumer's console (ADR-0034). It does not go into the
275
+ message catalog, because it is not an interface string. The text names the component, the value that was
276
+ passed, the value that won, and how to fix it. Keep changing data out of it: the deduplication key is the text
277
+ itself.
278
+ - The mode is read as `process.env.NODE_ENV !== "production"` inside `try/catch`, on every call. The package
279
+ ships as plain `tsc` output, and the value is not baked into `dist`. The consumer's build sets it (Vite, Next,
280
+ webpack) and removes the dead branch in production. Without a bundler, `process` is not defined: a reference to
281
+ it throws a `ReferenceError`, and the helper stays silent. The check `typeof process !== "undefined"` does not
282
+ work: the bundler does not replace it, so in a Vite browser build the warning would never fire.
283
+ - Test: `vi.spyOn(console, "warn")`, `resetDevWarnings()` in `beforeEach` (the "already shown" state lives at
284
+ module level), and four cases: it warns in development, it warns once, it stays silent in production
285
+ (`vi.stubEnv`), and it stays silent on a correct call.
286
+
287
+ **An error instead of a warning: `devError(message, cause?)`.** Use it when a violation breaks the whole component
288
+ but not the page: the Tool set of `RichTextEditor` is built incorrectly, the loader of an engine part failed,
289
+ `rangeAt` of the Text card threw an exception. The field shows the author a catalog string on its own, and the
290
+ cause goes to the developer through `console.error`, with the same rules as `devWarn`: in development, once per
291
+ text, in English.
292
+
293
+ - The text starts with the component name (`RichTextEditor: …`), because the console is shared by the whole page.
294
+ A Tool name or an entry name in the text is allowed: it comes from the Tool's description and does not change
295
+ from call to call.
296
+ - A caught exception goes in as the second argument (`cause`), not in the text. Its message would make every error
297
+ new for the once-per-text key. The console also shows the stack for an `Error` object.
298
+ - The test follows the same pattern with `vi.spyOn(console, "error")`: an error in development with the exception
299
+ as a separate argument, once; silence in production (`vi.stubEnv`); and silence on a correct call
300
+ (`tests/rich-text-editor-dev-errors.test.tsx`).
301
+
302
+ ## `readOnly` and `disabled` are separate axes, not shades of one
303
+
304
+ The rule comes from `Rating` (wave 25). It is written here because the next author would not find it in one
305
+ component's docstring, and a second `readOnly` in the kit would then mean something of its own.
306
+
307
+ - **`readOnly` is data on display.** The value is visible, and it cannot be changed. Such a value must not be
308
+ dimmed: that would hide what is shown on purpose. The control leaves the tab order, but stays at full strength.
309
+ - **`disabled` is an action that is unavailable right now.** It is dimmed, because dimming is the signal that it
310
+ is unavailable.
311
+
312
+ **The role does not change.** A disabled control stays what it was and is marked with `aria-disabled`. Swapping the
313
+ role (for example, to `img`) would be read as a picture, not as an unavailable action. The role is a property of
314
+ the control, not of its availability right now.
315
+
316
+ ## Editor glossary
317
+
318
+ The Editor terms moved to the kit's shared [glossary](glossary.md#редактор).
@@ -1,5 +1,5 @@
1
1
  ---
2
- sourceHash: fdfa8cd8c38b
2
+ sourceHash: 340e1ec512cb
3
3
  ---
4
4
 
5
5
  # Setting up `@olddevs/ui`
@@ -133,11 +133,11 @@ peer dependencies are treated as external, and the metric is gzip:
133
133
  | one `Button` | 1.6 kB |
134
134
  | `DataTable` | 9.0 kB |
135
135
  | the whole kit (`import *`) | 77.1 kB |
136
- | `RichTextEditor` from `@olddevs/ui/editor`, without the engine, "start" | 70.6 kB |
137
- | `RichTextEditor` with the Tiptap engine, "start" (static graph, `basic`) | 295.3 kB |
138
- | `RichTextEditor` with the Tiptap engine, "full" (with all lazy chunks, `full`) | 339.1 kB |
139
- | `RichTextView` from `@olddevs/ui/editor`, Tiptap engine included in the measurement | 12.0 kB |
140
- | `sanitizeRichText` and `richTextSanitizeConfig` from `@olddevs/ui/editor/schema`, Tiptap engine included | 3.6 kB |
136
+ | `RichTextEditor` from `@olddevs/ui/editor`, without the engine, "start" | 72.7 kB |
137
+ | `RichTextEditor` with the Tiptap engine, "start" (static graph, `basic`) | 297.5 kB |
138
+ | `RichTextEditor` with the Tiptap engine, "full" (with all lazy chunks, `full`) | 346.6 kB |
139
+ | `RichTextView` from `@olddevs/ui/editor`, Tiptap engine included in the measurement | 12.3 kB |
140
+ | `sanitizeRichText` and `richTextSanitizeConfig` from `@olddevs/ui/editor/schema`, Tiptap engine included | 3.9 kB |
141
141
  | `highlightCodeBlocks` from `@olddevs/ui/editor/highlight`, with lowlight | 22.6 kB |
142
142
  | `highlightCodeNodes` from `@olddevs/ui/editor/highlight`, with lowlight | 22.6 kB |
143
143
 
@@ -158,7 +158,7 @@ an app bundler with full minification produced about 133 kB (measured before the
158
158
  For `RichTextView`, Tiptap and lowlight are included in the measurement as a check. Its row contains only
159
159
  typography and the Island of the "Copy" button on code Blocks (the kit's button and tooltip, the message catalog),
160
160
  and the measurement proves that its graph contains neither the engine nor a highlighter. The lightweight
161
- `@olddevs/ui/editor/schema` entry point is measured the same way (3.6 kB): the Schema, `richTextSchema`, the
161
+ `@olddevs/ui/editor/schema` entry point is measured the same way (3.9 kB): the Schema, `richTextSchema`, the
162
162
  `sanitizeRichText` sanitizer with hooks for DOMPurify, including for the rules of custom Tools, and `markRange`.
163
163
 
164
164
  Server-side highlighting in `@olddevs/ui/editor/highlight` (22.6 kB — almost entirely the highlight.js core and
@@ -130,11 +130,11 @@ peer-зависимости внешние, метрика gzip:
130
130
  | одна `Button` | 1,6 кБ |
131
131
  | `DataTable` | 9,0 кБ |
132
132
  | весь кит целиком (`import *`) | 77,1 кБ |
133
- | `RichTextEditor` из `@olddevs/ui/editor`, без движка, «старт» | 70,6 кБ |
134
- | `RichTextEditor` вместе с движком Tiptap, «старт» (статический граф, `basic`) | 295,3 кБ |
135
- | `RichTextEditor` вместе с движком Tiptap, «полный» (со всеми ленивыми чанками, `full`) | 339,1 кБ |
136
- | `RichTextView` из `@olddevs/ui/editor`, движок Tiptap в замер включён | 12,0 кБ |
137
- | `sanitizeRichText` и `richTextSanitizeConfig` из `@olddevs/ui/editor/schema`, движок Tiptap в замер включён | 3,6 кБ |
133
+ | `RichTextEditor` из `@olddevs/ui/editor`, без движка, «старт» | 72,7 кБ |
134
+ | `RichTextEditor` вместе с движком Tiptap, «старт» (статический граф, `basic`) | 297,5 кБ |
135
+ | `RichTextEditor` вместе с движком Tiptap, «полный» (со всеми ленивыми чанками, `full`) | 346,6 кБ |
136
+ | `RichTextView` из `@olddevs/ui/editor`, движок Tiptap в замер включён | 12,3 кБ |
137
+ | `sanitizeRichText` и `richTextSanitizeConfig` из `@olddevs/ui/editor/schema`, движок Tiptap в замер включён | 3,9 кБ |
138
138
  | `highlightCodeBlocks` из `@olddevs/ui/editor/highlight`, вместе с lowlight | 22,6 кБ |
139
139
  | `highlightCodeNodes` из `@olddevs/ui/editor/highlight`, вместе с lowlight | 22,6 кБ |
140
140
 
@@ -155,7 +155,7 @@ peer-зависимости внешние, метрика gzip:
155
155
  У `RichTextView` Tiptap и lowlight внесены в замер ради проверки. В его строку входят только
156
156
  типографика и Остров кнопки «Копировать» у Блоков кода (кнопка и подсказка кита, каталог сообщений),
157
157
  и замер доказывает, что в графе нет ни движка, ни подсветчика. Так же меряется лёгкий вход
158
- `@olddevs/ui/editor/schema` (3,6 кБ): Схема, `richTextSchema`, очистка `sanitizeRichText` с хуками для
158
+ `@olddevs/ui/editor/schema` (3,9 кБ): Схема, `richTextSchema`, очистка `sanitizeRichText` с хуками для
159
159
  DOMPurify, в том числе по правилам своих Инструментов, и `markRange`.
160
160
 
161
161
  Серверная подсветка `@olddevs/ui/editor/highlight` (22,6 кБ — почти целиком ядро highlight.js и