@olddevs/ui 0.0.0-stage → 4.0.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/LICENSE +21 -0
- package/NOTICE +100 -0
- package/README.md +145 -2
- package/README.ru.md +146 -0
- package/dist/color.d.ts +22 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +2 -0
- package/dist/color.js.map +1 -0
- package/dist/components/actions/Alert.d.ts +46 -0
- package/dist/components/actions/Alert.d.ts.map +1 -0
- package/dist/components/actions/Alert.js +20 -0
- package/dist/components/actions/Alert.js.map +1 -0
- package/dist/components/actions/Banner.d.ts +58 -0
- package/dist/components/actions/Banner.d.ts.map +1 -0
- package/dist/components/actions/Banner.js +20 -0
- package/dist/components/actions/Banner.js.map +1 -0
- package/dist/components/actions/Button.d.ts +79 -0
- package/dist/components/actions/Button.d.ts.map +1 -0
- package/dist/components/actions/Button.js +29 -0
- package/dist/components/actions/Button.js.map +1 -0
- package/dist/components/actions/ButtonGroup.d.ts +32 -0
- package/dist/components/actions/ButtonGroup.d.ts.map +1 -0
- package/dist/components/actions/ButtonGroup.js +10 -0
- package/dist/components/actions/ButtonGroup.js.map +1 -0
- package/dist/components/actions/Chip.d.ts +36 -0
- package/dist/components/actions/Chip.d.ts.map +1 -0
- package/dist/components/actions/Chip.js +16 -0
- package/dist/components/actions/Chip.js.map +1 -0
- package/dist/components/actions/CopyButton.d.ts +32 -0
- package/dist/components/actions/CopyButton.d.ts.map +1 -0
- package/dist/components/actions/CopyButton.js +41 -0
- package/dist/components/actions/CopyButton.js.map +1 -0
- package/dist/components/actions/Progress.d.ts +36 -0
- package/dist/components/actions/Progress.d.ts.map +1 -0
- package/dist/components/actions/Progress.js +14 -0
- package/dist/components/actions/Progress.js.map +1 -0
- package/dist/components/actions/Segmented.d.ts +64 -0
- package/dist/components/actions/Segmented.d.ts.map +1 -0
- package/dist/components/actions/Segmented.js +14 -0
- package/dist/components/actions/Segmented.js.map +1 -0
- package/dist/components/actions/Spinner.d.ts +46 -0
- package/dist/components/actions/Spinner.d.ts.map +1 -0
- package/dist/components/actions/Spinner.js +17 -0
- package/dist/components/actions/Spinner.js.map +1 -0
- package/dist/components/actions/SplitButton.d.ts +92 -0
- package/dist/components/actions/SplitButton.d.ts.map +1 -0
- package/dist/components/actions/SplitButton.js +21 -0
- package/dist/components/actions/SplitButton.js.map +1 -0
- package/dist/components/actions/StatusBadge.d.ts +26 -0
- package/dist/components/actions/StatusBadge.d.ts.map +1 -0
- package/dist/components/actions/StatusBadge.js +14 -0
- package/dist/components/actions/StatusBadge.js.map +1 -0
- package/dist/components/actions/Toaster.d.ts +83 -0
- package/dist/components/actions/Toaster.d.ts.map +1 -0
- package/dist/components/actions/Toaster.js +38 -0
- package/dist/components/actions/Toaster.js.map +1 -0
- package/dist/components/actions/Toggle.d.ts +75 -0
- package/dist/components/actions/Toggle.d.ts.map +1 -0
- package/dist/components/actions/Toggle.js +39 -0
- package/dist/components/actions/Toggle.js.map +1 -0
- package/dist/components/actions/ToggleGroup.d.ts +63 -0
- package/dist/components/actions/ToggleGroup.d.ts.map +1 -0
- package/dist/components/actions/ToggleGroup.js +30 -0
- package/dist/components/actions/ToggleGroup.js.map +1 -0
- package/dist/components/actions/Toolbar.d.ts +52 -0
- package/dist/components/actions/Toolbar.d.ts.map +1 -0
- package/dist/components/actions/Toolbar.js +98 -0
- package/dist/components/actions/Toolbar.js.map +1 -0
- package/dist/components/actions/Truncate.d.ts +31 -0
- package/dist/components/actions/Truncate.d.ts.map +1 -0
- package/dist/components/actions/Truncate.js +32 -0
- package/dist/components/actions/Truncate.js.map +1 -0
- package/dist/components/app-shell/AppShell.d.ts +106 -0
- package/dist/components/app-shell/AppShell.d.ts.map +1 -0
- package/dist/components/app-shell/AppShell.js +29 -0
- package/dist/components/app-shell/AppShell.js.map +1 -0
- package/dist/components/app-shell/MobileTabBar.d.ts +130 -0
- package/dist/components/app-shell/MobileTabBar.d.ts.map +1 -0
- package/dist/components/app-shell/MobileTabBar.js +50 -0
- package/dist/components/app-shell/MobileTabBar.js.map +1 -0
- package/dist/components/app-shell/NavItem.d.ts +72 -0
- package/dist/components/app-shell/NavItem.d.ts.map +1 -0
- package/dist/components/app-shell/NavItem.js +22 -0
- package/dist/components/app-shell/NavItem.js.map +1 -0
- package/dist/components/app-shell/SideNav.d.ts +65 -0
- package/dist/components/app-shell/SideNav.d.ts.map +1 -0
- package/dist/components/app-shell/SideNav.js +31 -0
- package/dist/components/app-shell/SideNav.js.map +1 -0
- package/dist/components/app-shell/SplitPanel.d.ts +121 -0
- package/dist/components/app-shell/SplitPanel.d.ts.map +1 -0
- package/dist/components/app-shell/SplitPanel.js +88 -0
- package/dist/components/app-shell/SplitPanel.js.map +1 -0
- package/dist/components/app-shell/TopBar.d.ts +43 -0
- package/dist/components/app-shell/TopBar.d.ts.map +1 -0
- package/dist/components/app-shell/TopBar.js +24 -0
- package/dist/components/app-shell/TopBar.js.map +1 -0
- package/dist/components/content/AspectRatio.d.ts +25 -0
- package/dist/components/content/AspectRatio.d.ts.map +1 -0
- package/dist/components/content/AspectRatio.js +7 -0
- package/dist/components/content/AspectRatio.js.map +1 -0
- package/dist/components/content/Avatar.d.ts +22 -0
- package/dist/components/content/Avatar.d.ts.map +1 -0
- package/dist/components/content/Avatar.js +23 -0
- package/dist/components/content/Avatar.js.map +1 -0
- package/dist/components/content/AvatarGroup.d.ts +42 -0
- package/dist/components/content/AvatarGroup.d.ts.map +1 -0
- package/dist/components/content/AvatarGroup.js +11 -0
- package/dist/components/content/AvatarGroup.js.map +1 -0
- package/dist/components/content/Badge.d.ts +41 -0
- package/dist/components/content/Badge.d.ts.map +1 -0
- package/dist/components/content/Badge.js +13 -0
- package/dist/components/content/Badge.js.map +1 -0
- package/dist/components/content/Card.d.ts +59 -0
- package/dist/components/content/Card.d.ts.map +1 -0
- package/dist/components/content/Card.js +20 -0
- package/dist/components/content/Card.js.map +1 -0
- package/dist/components/content/Carousel.d.ts +79 -0
- package/dist/components/content/Carousel.d.ts.map +1 -0
- package/dist/components/content/Carousel.js +172 -0
- package/dist/components/content/Carousel.js.map +1 -0
- package/dist/components/content/CodeView.d.ts +76 -0
- package/dist/components/content/CodeView.d.ts.map +1 -0
- package/dist/components/content/CodeView.js +12 -0
- package/dist/components/content/CodeView.js.map +1 -0
- package/dist/components/content/DescriptionList.d.ts +31 -0
- package/dist/components/content/DescriptionList.d.ts.map +1 -0
- package/dist/components/content/DescriptionList.js +10 -0
- package/dist/components/content/DescriptionList.js.map +1 -0
- package/dist/components/content/EditableList.d.ts +162 -0
- package/dist/components/content/EditableList.d.ts.map +1 -0
- package/dist/components/content/EditableList.js +241 -0
- package/dist/components/content/EditableList.js.map +1 -0
- package/dist/components/content/EmptyState.d.ts +31 -0
- package/dist/components/content/EmptyState.d.ts.map +1 -0
- package/dist/components/content/EmptyState.js +9 -0
- package/dist/components/content/EmptyState.js.map +1 -0
- package/dist/components/content/ErrorState.d.ts +52 -0
- package/dist/components/content/ErrorState.d.ts.map +1 -0
- package/dist/components/content/ErrorState.js +16 -0
- package/dist/components/content/ErrorState.js.map +1 -0
- package/dist/components/content/Grid.d.ts +57 -0
- package/dist/components/content/Grid.d.ts.map +1 -0
- package/dist/components/content/Grid.js +85 -0
- package/dist/components/content/Grid.js.map +1 -0
- package/dist/components/content/Heading.d.ts +43 -0
- package/dist/components/content/Heading.d.ts.map +1 -0
- package/dist/components/content/Heading.js +21 -0
- package/dist/components/content/Heading.js.map +1 -0
- package/dist/components/content/Image.d.ts +94 -0
- package/dist/components/content/Image.d.ts.map +1 -0
- package/dist/components/content/Image.js +41 -0
- package/dist/components/content/Image.js.map +1 -0
- package/dist/components/content/Kbd.d.ts +31 -0
- package/dist/components/content/Kbd.d.ts.map +1 -0
- package/dist/components/content/Kbd.js +13 -0
- package/dist/components/content/Kbd.js.map +1 -0
- package/dist/components/content/Keyboard.d.ts +32 -0
- package/dist/components/content/Keyboard.d.ts.map +1 -0
- package/dist/components/content/Keyboard.js +381 -0
- package/dist/components/content/Keyboard.js.map +1 -0
- package/dist/components/content/Link.d.ts +46 -0
- package/dist/components/content/Link.d.ts.map +1 -0
- package/dist/components/content/Link.js +38 -0
- package/dist/components/content/Link.js.map +1 -0
- package/dist/components/content/MediaCard.d.ts +56 -0
- package/dist/components/content/MediaCard.d.ts.map +1 -0
- package/dist/components/content/MediaCard.js +15 -0
- package/dist/components/content/MediaCard.js.map +1 -0
- package/dist/components/content/NotificationCenter.d.ts +68 -0
- package/dist/components/content/NotificationCenter.d.ts.map +1 -0
- package/dist/components/content/NotificationCenter.js +14 -0
- package/dist/components/content/NotificationCenter.js.map +1 -0
- package/dist/components/content/NotificationList.d.ts +111 -0
- package/dist/components/content/NotificationList.d.ts.map +1 -0
- package/dist/components/content/NotificationList.js +48 -0
- package/dist/components/content/NotificationList.js.map +1 -0
- package/dist/components/content/ScrollArea.d.ts +30 -0
- package/dist/components/content/ScrollArea.d.ts.map +1 -0
- package/dist/components/content/ScrollArea.js +7 -0
- package/dist/components/content/ScrollArea.js.map +1 -0
- package/dist/components/content/Separator.d.ts +19 -0
- package/dist/components/content/Separator.d.ts.map +1 -0
- package/dist/components/content/Separator.js +14 -0
- package/dist/components/content/Separator.js.map +1 -0
- package/dist/components/content/Skeleton.d.ts +24 -0
- package/dist/components/content/Skeleton.d.ts.map +1 -0
- package/dist/components/content/Skeleton.js +8 -0
- package/dist/components/content/Skeleton.js.map +1 -0
- package/dist/components/content/SortableList.d.ts +98 -0
- package/dist/components/content/SortableList.d.ts.map +1 -0
- package/dist/components/content/SortableList.js +46 -0
- package/dist/components/content/SortableList.js.map +1 -0
- package/dist/components/content/Stat.d.ts +46 -0
- package/dist/components/content/Stat.d.ts.map +1 -0
- package/dist/components/content/Stat.js +12 -0
- package/dist/components/content/Stat.js.map +1 -0
- package/dist/components/content/Table.d.ts +110 -0
- package/dist/components/content/Table.d.ts.map +1 -0
- package/dist/components/content/Table.js +66 -0
- package/dist/components/content/Table.js.map +1 -0
- package/dist/components/content/TagGroup.d.ts +120 -0
- package/dist/components/content/TagGroup.d.ts.map +1 -0
- package/dist/components/content/TagGroup.js +20 -0
- package/dist/components/content/TagGroup.js.map +1 -0
- package/dist/components/content/Text.d.ts +55 -0
- package/dist/components/content/Text.d.ts.map +1 -0
- package/dist/components/content/Text.js +27 -0
- package/dist/components/content/Text.js.map +1 -0
- package/dist/components/content/Timeline.d.ts +50 -0
- package/dist/components/content/Timeline.d.ts.map +1 -0
- package/dist/components/content/Timeline.js +15 -0
- package/dist/components/content/Timeline.js.map +1 -0
- package/dist/components/content/VirtualGrid.d.ts +171 -0
- package/dist/components/content/VirtualGrid.d.ts.map +1 -0
- package/dist/components/content/VirtualGrid.js +267 -0
- package/dist/components/content/VirtualGrid.js.map +1 -0
- package/dist/components/content/VisuallyHidden.d.ts +24 -0
- package/dist/components/content/VisuallyHidden.d.ts.map +1 -0
- package/dist/components/content/VisuallyHidden.js +3 -0
- package/dist/components/content/VisuallyHidden.js.map +1 -0
- package/dist/components/content/keyboardEditing.d.ts +15 -0
- package/dist/components/content/keyboardEditing.d.ts.map +1 -0
- package/dist/components/content/keyboardEditing.js +28 -0
- package/dist/components/content/keyboardEditing.js.map +1 -0
- package/dist/components/content/useDragToSelect.d.ts +41 -0
- package/dist/components/content/useDragToSelect.d.ts.map +1 -0
- package/dist/components/content/useDragToSelect.js +190 -0
- package/dist/components/content/useDragToSelect.js.map +1 -0
- package/dist/components/date-time/Calendar.d.ts +67 -0
- package/dist/components/date-time/Calendar.d.ts.map +1 -0
- package/dist/components/date-time/Calendar.js +28 -0
- package/dist/components/date-time/Calendar.js.map +1 -0
- package/dist/components/date-time/DatePicker.d.ts +57 -0
- package/dist/components/date-time/DatePicker.d.ts.map +1 -0
- package/dist/components/date-time/DatePicker.js +37 -0
- package/dist/components/date-time/DatePicker.js.map +1 -0
- package/dist/components/date-time/DateRangePicker.d.ts +91 -0
- package/dist/components/date-time/DateRangePicker.d.ts.map +1 -0
- package/dist/components/date-time/DateRangePicker.js +72 -0
- package/dist/components/date-time/DateRangePicker.js.map +1 -0
- package/dist/components/date-time/DateTimePicker.d.ts +55 -0
- package/dist/components/date-time/DateTimePicker.d.ts.map +1 -0
- package/dist/components/date-time/DateTimePicker.js +9 -0
- package/dist/components/date-time/DateTimePicker.js.map +1 -0
- package/dist/components/date-time/RangeCalendar.d.ts +50 -0
- package/dist/components/date-time/RangeCalendar.d.ts.map +1 -0
- package/dist/components/date-time/RangeCalendar.js +17 -0
- package/dist/components/date-time/RangeCalendar.js.map +1 -0
- package/dist/components/date-time/TimePicker.d.ts +47 -0
- package/dist/components/date-time/TimePicker.d.ts.map +1 -0
- package/dist/components/date-time/TimePicker.js +148 -0
- package/dist/components/date-time/TimePicker.js.map +1 -0
- package/dist/components/inputs/Checkbox.d.ts +140 -0
- package/dist/components/inputs/Checkbox.d.ts.map +1 -0
- package/dist/components/inputs/Checkbox.js +29 -0
- package/dist/components/inputs/Checkbox.js.map +1 -0
- package/dist/components/inputs/CheckboxCard.d.ts +17 -0
- package/dist/components/inputs/CheckboxCard.d.ts.map +1 -0
- package/dist/components/inputs/CheckboxCard.js +25 -0
- package/dist/components/inputs/CheckboxCard.js.map +1 -0
- package/dist/components/inputs/CheckboxGroup.d.ts +68 -0
- package/dist/components/inputs/CheckboxGroup.d.ts.map +1 -0
- package/dist/components/inputs/CheckboxGroup.js +14 -0
- package/dist/components/inputs/CheckboxGroup.js.map +1 -0
- package/dist/components/inputs/ChoiceCardBody.d.ts +16 -0
- package/dist/components/inputs/ChoiceCardBody.d.ts.map +1 -0
- package/dist/components/inputs/ChoiceCardBody.js +10 -0
- package/dist/components/inputs/ChoiceCardBody.js.map +1 -0
- package/dist/components/inputs/ColorPicker.d.ts +71 -0
- package/dist/components/inputs/ColorPicker.d.ts.map +1 -0
- package/dist/components/inputs/ColorPicker.js +30 -0
- package/dist/components/inputs/ColorPicker.js.map +1 -0
- package/dist/components/inputs/Combobox.d.ts +41 -0
- package/dist/components/inputs/Combobox.d.ts.map +1 -0
- package/dist/components/inputs/Combobox.js +16 -0
- package/dist/components/inputs/Combobox.js.map +1 -0
- package/dist/components/inputs/FileDropzone.d.ts +51 -0
- package/dist/components/inputs/FileDropzone.d.ts.map +1 -0
- package/dist/components/inputs/FileDropzone.js +75 -0
- package/dist/components/inputs/FileDropzone.js.map +1 -0
- package/dist/components/inputs/Form.d.ts +50 -0
- package/dist/components/inputs/Form.d.ts.map +1 -0
- package/dist/components/inputs/Form.js +14 -0
- package/dist/components/inputs/Form.js.map +1 -0
- package/dist/components/inputs/FormField.d.ts +89 -0
- package/dist/components/inputs/FormField.d.ts.map +1 -0
- package/dist/components/inputs/FormField.js +24 -0
- package/dist/components/inputs/FormField.js.map +1 -0
- package/dist/components/inputs/FormLayout.d.ts +47 -0
- package/dist/components/inputs/FormLayout.d.ts.map +1 -0
- package/dist/components/inputs/FormLayout.js +12 -0
- package/dist/components/inputs/FormLayout.js.map +1 -0
- package/dist/components/inputs/InlineEdit.d.ts +50 -0
- package/dist/components/inputs/InlineEdit.d.ts.map +1 -0
- package/dist/components/inputs/InlineEdit.js +70 -0
- package/dist/components/inputs/InlineEdit.js.map +1 -0
- package/dist/components/inputs/MaskedInput.d.ts +66 -0
- package/dist/components/inputs/MaskedInput.d.ts.map +1 -0
- package/dist/components/inputs/MaskedInput.js +73 -0
- package/dist/components/inputs/MaskedInput.js.map +1 -0
- package/dist/components/inputs/NumberInput.d.ts +53 -0
- package/dist/components/inputs/NumberInput.d.ts.map +1 -0
- package/dist/components/inputs/NumberInput.js +17 -0
- package/dist/components/inputs/NumberInput.js.map +1 -0
- package/dist/components/inputs/PasswordInput.d.ts +49 -0
- package/dist/components/inputs/PasswordInput.d.ts.map +1 -0
- package/dist/components/inputs/PasswordInput.js +49 -0
- package/dist/components/inputs/PasswordInput.js.map +1 -0
- package/dist/components/inputs/PhoneInput.d.ts +35 -0
- package/dist/components/inputs/PhoneInput.d.ts.map +1 -0
- package/dist/components/inputs/PhoneInput.js +9 -0
- package/dist/components/inputs/PhoneInput.js.map +1 -0
- package/dist/components/inputs/PinInput.d.ts +50 -0
- package/dist/components/inputs/PinInput.d.ts.map +1 -0
- package/dist/components/inputs/PinInput.js +67 -0
- package/dist/components/inputs/PinInput.js.map +1 -0
- package/dist/components/inputs/RadioGroup.d.ts +86 -0
- package/dist/components/inputs/RadioGroup.d.ts.map +1 -0
- package/dist/components/inputs/RadioGroup.js +51 -0
- package/dist/components/inputs/RadioGroup.js.map +1 -0
- package/dist/components/inputs/Rating.d.ts +69 -0
- package/dist/components/inputs/Rating.d.ts.map +1 -0
- package/dist/components/inputs/Rating.js +51 -0
- package/dist/components/inputs/Rating.js.map +1 -0
- package/dist/components/inputs/SearchControl.d.ts +85 -0
- package/dist/components/inputs/SearchControl.d.ts.map +1 -0
- package/dist/components/inputs/SearchControl.js +62 -0
- package/dist/components/inputs/SearchControl.js.map +1 -0
- package/dist/components/inputs/Select.d.ts +205 -0
- package/dist/components/inputs/Select.d.ts.map +1 -0
- package/dist/components/inputs/Select.js +136 -0
- package/dist/components/inputs/Select.js.map +1 -0
- package/dist/components/inputs/Slider.d.ts +59 -0
- package/dist/components/inputs/Slider.d.ts.map +1 -0
- package/dist/components/inputs/Slider.js +153 -0
- package/dist/components/inputs/Slider.js.map +1 -0
- package/dist/components/inputs/SwatchGroup.d.ts +101 -0
- package/dist/components/inputs/SwatchGroup.d.ts.map +1 -0
- package/dist/components/inputs/SwatchGroup.js +57 -0
- package/dist/components/inputs/SwatchGroup.js.map +1 -0
- package/dist/components/inputs/TagInput.d.ts +45 -0
- package/dist/components/inputs/TagInput.d.ts.map +1 -0
- package/dist/components/inputs/TagInput.js +56 -0
- package/dist/components/inputs/TagInput.js.map +1 -0
- package/dist/components/inputs/TextControl.d.ts +101 -0
- package/dist/components/inputs/TextControl.d.ts.map +1 -0
- package/dist/components/inputs/TextControl.js +31 -0
- package/dist/components/inputs/TextControl.js.map +1 -0
- package/dist/components/inputs/Transfer.d.ts +84 -0
- package/dist/components/inputs/Transfer.d.ts.map +1 -0
- package/dist/components/inputs/Transfer.js +112 -0
- package/dist/components/inputs/Transfer.js.map +1 -0
- package/dist/components/inputs/TreeSelect.d.ts +73 -0
- package/dist/components/inputs/TreeSelect.d.ts.map +1 -0
- package/dist/components/inputs/TreeSelect.js +60 -0
- package/dist/components/inputs/TreeSelect.js.map +1 -0
- package/dist/components/inputs/UploadQueue.d.ts +84 -0
- package/dist/components/inputs/UploadQueue.d.ts.map +1 -0
- package/dist/components/inputs/UploadQueue.js +30 -0
- package/dist/components/inputs/UploadQueue.js.map +1 -0
- package/dist/components/navigation/Accordion.d.ts +44 -0
- package/dist/components/navigation/Accordion.d.ts.map +1 -0
- package/dist/components/navigation/Accordion.js +12 -0
- package/dist/components/navigation/Accordion.js.map +1 -0
- package/dist/components/navigation/Breadcrumbs.d.ts +35 -0
- package/dist/components/navigation/Breadcrumbs.d.ts.map +1 -0
- package/dist/components/navigation/Breadcrumbs.js +17 -0
- package/dist/components/navigation/Breadcrumbs.js.map +1 -0
- package/dist/components/navigation/ContextualSaveBar.d.ts +90 -0
- package/dist/components/navigation/ContextualSaveBar.d.ts.map +1 -0
- package/dist/components/navigation/ContextualSaveBar.js +20 -0
- package/dist/components/navigation/ContextualSaveBar.js.map +1 -0
- package/dist/components/navigation/PageHeader.d.ts +17 -0
- package/dist/components/navigation/PageHeader.d.ts.map +1 -0
- package/dist/components/navigation/PageHeader.js +8 -0
- package/dist/components/navigation/PageHeader.js.map +1 -0
- package/dist/components/navigation/Pagination.d.ts +44 -0
- package/dist/components/navigation/Pagination.d.ts.map +1 -0
- package/dist/components/navigation/Pagination.js +57 -0
- package/dist/components/navigation/Pagination.js.map +1 -0
- package/dist/components/navigation/SectionHeader.d.ts +32 -0
- package/dist/components/navigation/SectionHeader.d.ts.map +1 -0
- package/dist/components/navigation/SectionHeader.js +13 -0
- package/dist/components/navigation/SectionHeader.js.map +1 -0
- package/dist/components/navigation/SelectionBar.d.ts +65 -0
- package/dist/components/navigation/SelectionBar.d.ts.map +1 -0
- package/dist/components/navigation/SelectionBar.js +24 -0
- package/dist/components/navigation/SelectionBar.js.map +1 -0
- package/dist/components/navigation/Stepper.d.ts +45 -0
- package/dist/components/navigation/Stepper.d.ts.map +1 -0
- package/dist/components/navigation/Stepper.js +16 -0
- package/dist/components/navigation/Stepper.js.map +1 -0
- package/dist/components/navigation/Tabs.d.ts +45 -0
- package/dist/components/navigation/Tabs.d.ts.map +1 -0
- package/dist/components/navigation/Tabs.js +51 -0
- package/dist/components/navigation/Tabs.js.map +1 -0
- package/dist/components/navigation/TreeView.d.ts +101 -0
- package/dist/components/navigation/TreeView.d.ts.map +1 -0
- package/dist/components/navigation/TreeView.js +27 -0
- package/dist/components/navigation/TreeView.js.map +1 -0
- package/dist/components/overlays/CommandPalette.d.ts +115 -0
- package/dist/components/overlays/CommandPalette.d.ts.map +1 -0
- package/dist/components/overlays/CommandPalette.js +43 -0
- package/dist/components/overlays/CommandPalette.js.map +1 -0
- package/dist/components/overlays/ConfirmDialog.d.ts +67 -0
- package/dist/components/overlays/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/overlays/ConfirmDialog.js +47 -0
- package/dist/components/overlays/ConfirmDialog.js.map +1 -0
- package/dist/components/overlays/ContextMenu.d.ts +122 -0
- package/dist/components/overlays/ContextMenu.d.ts.map +1 -0
- package/dist/components/overlays/ContextMenu.js +39 -0
- package/dist/components/overlays/ContextMenu.js.map +1 -0
- package/dist/components/overlays/Dialog.d.ts +113 -0
- package/dist/components/overlays/Dialog.d.ts.map +1 -0
- package/dist/components/overlays/Dialog.js +43 -0
- package/dist/components/overlays/Dialog.js.map +1 -0
- package/dist/components/overlays/Drawer.d.ts +53 -0
- package/dist/components/overlays/Drawer.d.ts.map +1 -0
- package/dist/components/overlays/Drawer.js +32 -0
- package/dist/components/overlays/Drawer.js.map +1 -0
- package/dist/components/overlays/HelpTooltip.d.ts +41 -0
- package/dist/components/overlays/HelpTooltip.d.ts.map +1 -0
- package/dist/components/overlays/HelpTooltip.js +10 -0
- package/dist/components/overlays/HelpTooltip.js.map +1 -0
- package/dist/components/overlays/HoverCard.d.ts +62 -0
- package/dist/components/overlays/HoverCard.d.ts.map +1 -0
- package/dist/components/overlays/HoverCard.js +175 -0
- package/dist/components/overlays/HoverCard.js.map +1 -0
- package/dist/components/overlays/Menu.d.ts +113 -0
- package/dist/components/overlays/Menu.d.ts.map +1 -0
- package/dist/components/overlays/Menu.js +26 -0
- package/dist/components/overlays/Menu.js.map +1 -0
- package/dist/components/overlays/ModalHeader.d.ts +77 -0
- package/dist/components/overlays/ModalHeader.d.ts.map +1 -0
- package/dist/components/overlays/ModalHeader.js +11 -0
- package/dist/components/overlays/ModalHeader.js.map +1 -0
- package/dist/components/overlays/ModalOverlay.d.ts +45 -0
- package/dist/components/overlays/ModalOverlay.d.ts.map +1 -0
- package/dist/components/overlays/ModalOverlay.js +29 -0
- package/dist/components/overlays/ModalOverlay.js.map +1 -0
- package/dist/components/overlays/Popover.d.ts +56 -0
- package/dist/components/overlays/Popover.d.ts.map +1 -0
- package/dist/components/overlays/Popover.js +16 -0
- package/dist/components/overlays/Popover.js.map +1 -0
- package/dist/components/overlays/ShortcutsHelp.d.ts +70 -0
- package/dist/components/overlays/ShortcutsHelp.d.ts.map +1 -0
- package/dist/components/overlays/ShortcutsHelp.js +20 -0
- package/dist/components/overlays/ShortcutsHelp.js.map +1 -0
- package/dist/components/overlays/Tooltip.d.ts +35 -0
- package/dist/components/overlays/Tooltip.d.ts.map +1 -0
- package/dist/components/overlays/Tooltip.js +18 -0
- package/dist/components/overlays/Tooltip.js.map +1 -0
- package/dist/components/table/DataTable.d.ts +60 -0
- package/dist/components/table/DataTable.d.ts.map +1 -0
- package/dist/components/table/DataTable.js +20 -0
- package/dist/components/table/DataTable.js.map +1 -0
- package/dist/components/table/FilterBar.d.ts +35 -0
- package/dist/components/table/FilterBar.d.ts.map +1 -0
- package/dist/components/table/FilterBar.js +17 -0
- package/dist/components/table/FilterBar.js.map +1 -0
- package/dist/components/table/FilterBarAdd.d.ts +57 -0
- package/dist/components/table/FilterBarAdd.d.ts.map +1 -0
- package/dist/components/table/FilterBarAdd.js +83 -0
- package/dist/components/table/FilterBarAdd.js.map +1 -0
- package/dist/components/table/SavedViews.d.ts +83 -0
- package/dist/components/table/SavedViews.d.ts.map +1 -0
- package/dist/components/table/SavedViews.js +12 -0
- package/dist/components/table/SavedViews.js.map +1 -0
- package/dist/components/table/TablePreferences.d.ts +45 -0
- package/dist/components/table/TablePreferences.d.ts.map +1 -0
- package/dist/components/table/TablePreferences.js +41 -0
- package/dist/components/table/TablePreferences.js.map +1 -0
- package/dist/components/table/TableToolbar.d.ts +38 -0
- package/dist/components/table/TableToolbar.d.ts.map +1 -0
- package/dist/components/table/TableToolbar.js +11 -0
- package/dist/components/table/TableToolbar.js.map +1 -0
- package/dist/editor/RichTextEditor.d.ts +274 -0
- package/dist/editor/RichTextEditor.d.ts.map +1 -0
- package/dist/editor/RichTextEditor.js +671 -0
- package/dist/editor/RichTextEditor.js.map +1 -0
- package/dist/editor/RichTextView.d.ts +70 -0
- package/dist/editor/RichTextView.d.ts.map +1 -0
- package/dist/editor/RichTextView.js +24 -0
- package/dist/editor/RichTextView.js.map +1 -0
- package/dist/editor/core/atomText.d.ts +21 -0
- package/dist/editor/core/atomText.d.ts.map +1 -0
- package/dist/editor/core/atomText.js +77 -0
- package/dist/editor/core/atomText.js.map +1 -0
- package/dist/editor/core/blockCommands.d.ts +94 -0
- package/dist/editor/core/blockCommands.d.ts.map +1 -0
- package/dist/editor/core/blockCommands.js +188 -0
- package/dist/editor/core/blockCommands.js.map +1 -0
- package/dist/editor/core/blockTypes.d.ts +89 -0
- package/dist/editor/core/blockTypes.d.ts.map +1 -0
- package/dist/editor/core/blockTypes.js +121 -0
- package/dist/editor/core/blockTypes.js.map +1 -0
- package/dist/editor/core/containers.d.ts +43 -0
- package/dist/editor/core/containers.d.ts.map +1 -0
- package/dist/editor/core/containers.js +52 -0
- package/dist/editor/core/containers.js.map +1 -0
- package/dist/editor/core/customMarkdown.d.ts +8 -0
- package/dist/editor/core/customMarkdown.d.ts.map +1 -0
- package/dist/editor/core/customMarkdown.js +34 -0
- package/dist/editor/core/customMarkdown.js.map +1 -0
- package/dist/editor/core/customTool.d.ts +407 -0
- package/dist/editor/core/customTool.d.ts.map +1 -0
- package/dist/editor/core/customTool.js +31 -0
- package/dist/editor/core/customTool.js.map +1 -0
- package/dist/editor/core/customToolEngine.d.ts +31 -0
- package/dist/editor/core/customToolEngine.d.ts.map +1 -0
- package/dist/editor/core/customToolEngine.js +324 -0
- package/dist/editor/core/customToolEngine.js.map +1 -0
- package/dist/editor/core/document.d.ts +17 -0
- package/dist/editor/core/document.d.ts.map +1 -0
- package/dist/editor/core/document.js +11 -0
- package/dist/editor/core/document.js.map +1 -0
- package/dist/editor/core/emptiness.d.ts +18 -0
- package/dist/editor/core/emptiness.d.ts.map +1 -0
- package/dist/editor/core/emptiness.js +14 -0
- package/dist/editor/core/emptiness.js.map +1 -0
- package/dist/editor/core/extensions.d.ts +78 -0
- package/dist/editor/core/extensions.d.ts.map +1 -0
- package/dist/editor/core/extensions.js +134 -0
- package/dist/editor/core/extensions.js.map +1 -0
- package/dist/editor/core/fieldNotice.d.ts +48 -0
- package/dist/editor/core/fieldNotice.d.ts.map +1 -0
- package/dist/editor/core/fieldNotice.js +18 -0
- package/dist/editor/core/fieldNotice.js.map +1 -0
- package/dist/editor/core/handle.d.ts +110 -0
- package/dist/editor/core/handle.d.ts.map +1 -0
- package/dist/editor/core/handle.js +149 -0
- package/dist/editor/core/handle.js.map +1 -0
- package/dist/editor/core/insertItems.d.ts +147 -0
- package/dist/editor/core/insertItems.d.ts.map +1 -0
- package/dist/editor/core/insertItems.js +215 -0
- package/dist/editor/core/insertItems.js.map +1 -0
- package/dist/editor/core/jsonDocument.d.ts +42 -0
- package/dist/editor/core/jsonDocument.d.ts.map +1 -0
- package/dist/editor/core/jsonDocument.js +44 -0
- package/dist/editor/core/jsonDocument.js.map +1 -0
- package/dist/editor/core/markdown.d.ts +47 -0
- package/dist/editor/core/markdown.d.ts.map +1 -0
- package/dist/editor/core/markdown.js +169 -0
- package/dist/editor/core/markdown.js.map +1 -0
- package/dist/editor/core/maxLength.d.ts +43 -0
- package/dist/editor/core/maxLength.d.ts.map +1 -0
- package/dist/editor/core/maxLength.js +45 -0
- package/dist/editor/core/maxLength.js.map +1 -0
- package/dist/editor/core/pasteTrim.d.ts +8 -0
- package/dist/editor/core/pasteTrim.d.ts.map +1 -0
- package/dist/editor/core/pasteTrim.js +61 -0
- package/dist/editor/core/pasteTrim.js.map +1 -0
- package/dist/editor/core/schema.d.ts +479 -0
- package/dist/editor/core/schema.d.ts.map +1 -0
- package/dist/editor/core/schema.js +371 -0
- package/dist/editor/core/schema.js.map +1 -0
- package/dist/editor/core/schemaConform.d.ts +30 -0
- package/dist/editor/core/schemaConform.d.ts.map +1 -0
- package/dist/editor/core/schemaConform.js +72 -0
- package/dist/editor/core/schemaConform.js.map +1 -0
- package/dist/editor/core/shortcuts.d.ts +110 -0
- package/dist/editor/core/shortcuts.d.ts.map +1 -0
- package/dist/editor/core/shortcuts.js +299 -0
- package/dist/editor/core/shortcuts.js.map +1 -0
- package/dist/editor/core/skeleton.d.ts +11 -0
- package/dist/editor/core/skeleton.d.ts.map +1 -0
- package/dist/editor/core/skeleton.js +48 -0
- package/dist/editor/core/skeleton.js.map +1 -0
- package/dist/editor/core/slashCommand.d.ts +46 -0
- package/dist/editor/core/slashCommand.d.ts.map +1 -0
- package/dist/editor/core/slashCommand.js +67 -0
- package/dist/editor/core/slashCommand.js.map +1 -0
- package/dist/editor/core/sourceFormat.d.ts +30 -0
- package/dist/editor/core/sourceFormat.d.ts.map +1 -0
- package/dist/editor/core/sourceFormat.js +136 -0
- package/dist/editor/core/sourceFormat.js.map +1 -0
- package/dist/editor/core/toolButtons.d.ts +58 -0
- package/dist/editor/core/toolButtons.d.ts.map +1 -0
- package/dist/editor/core/toolButtons.js +64 -0
- package/dist/editor/core/toolButtons.js.map +1 -0
- package/dist/editor/core/toolDescriptor.d.ts +304 -0
- package/dist/editor/core/toolDescriptor.d.ts.map +1 -0
- package/dist/editor/core/toolDescriptor.js +29 -0
- package/dist/editor/core/toolDescriptor.js.map +1 -0
- package/dist/editor/core/toolEntryShortcuts.d.ts +20 -0
- package/dist/editor/core/toolEntryShortcuts.d.ts.map +1 -0
- package/dist/editor/core/toolEntryShortcuts.js +46 -0
- package/dist/editor/core/toolEntryShortcuts.js.map +1 -0
- package/dist/editor/core/toolLoader.d.ts +14 -0
- package/dist/editor/core/toolLoader.d.ts.map +1 -0
- package/dist/editor/core/toolLoader.js +20 -0
- package/dist/editor/core/toolLoader.js.map +1 -0
- package/dist/editor/core/tools.d.ts +90 -0
- package/dist/editor/core/tools.d.ts.map +1 -0
- package/dist/editor/core/tools.js +56 -0
- package/dist/editor/core/tools.js.map +1 -0
- package/dist/editor/core/viewIsland.d.ts +17 -0
- package/dist/editor/core/viewIsland.d.ts.map +1 -0
- package/dist/editor/core/viewIsland.js +4 -0
- package/dist/editor/core/viewIsland.js.map +1 -0
- package/dist/editor/features/builtins.d.ts +25 -0
- package/dist/editor/features/builtins.d.ts.map +1 -0
- package/dist/editor/features/builtins.js +88 -0
- package/dist/editor/features/builtins.js.map +1 -0
- package/dist/editor/features/code/CodeBlockView.d.ts +38 -0
- package/dist/editor/features/code/CodeBlockView.d.ts.map +1 -0
- package/dist/editor/features/code/CodeBlockView.js +98 -0
- package/dist/editor/features/code/CodeBlockView.js.map +1 -0
- package/dist/editor/features/code/CodeCopyButton.d.ts +20 -0
- package/dist/editor/features/code/CodeCopyButton.d.ts.map +1 -0
- package/dist/editor/features/code/CodeCopyButton.js +29 -0
- package/dist/editor/features/code/CodeCopyButton.js.map +1 -0
- package/dist/editor/features/code/CodeCopyIsland.d.ts +37 -0
- package/dist/editor/features/code/CodeCopyIsland.d.ts.map +1 -0
- package/dist/editor/features/code/CodeCopyIsland.js +21 -0
- package/dist/editor/features/code/CodeCopyIsland.js.map +1 -0
- package/dist/editor/features/code/codeBlock.d.ts +25 -0
- package/dist/editor/features/code/codeBlock.d.ts.map +1 -0
- package/dist/editor/features/code/codeBlock.js +116 -0
- package/dist/editor/features/code/codeBlock.js.map +1 -0
- package/dist/editor/features/code/codeBlockTool.d.ts +24 -0
- package/dist/editor/features/code/codeBlockTool.d.ts.map +1 -0
- package/dist/editor/features/code/codeBlockTool.js +66 -0
- package/dist/editor/features/code/codeBlockTool.js.map +1 -0
- package/dist/editor/features/code/codeHeader.d.ts +41 -0
- package/dist/editor/features/code/codeHeader.d.ts.map +1 -0
- package/dist/editor/features/code/codeHeader.js +25 -0
- package/dist/editor/features/code/codeHeader.js.map +1 -0
- package/dist/editor/features/code/codeLanguages.d.ts +76 -0
- package/dist/editor/features/code/codeLanguages.d.ts.map +1 -0
- package/dist/editor/features/code/codeLanguages.js +24 -0
- package/dist/editor/features/code/codeLanguages.js.map +1 -0
- package/dist/editor/features/code/highlight.d.ts +20 -0
- package/dist/editor/features/code/highlight.d.ts.map +1 -0
- package/dist/editor/features/code/highlight.js +51 -0
- package/dist/editor/features/code/highlight.js.map +1 -0
- package/dist/editor/features/code/highlightNodes.d.ts +31 -0
- package/dist/editor/features/code/highlightNodes.d.ts.map +1 -0
- package/dist/editor/features/code/highlightNodes.js +25 -0
- package/dist/editor/features/code/highlightNodes.js.map +1 -0
- package/dist/editor/features/code/lowlight.d.ts +20 -0
- package/dist/editor/features/code/lowlight.d.ts.map +1 -0
- package/dist/editor/features/code/lowlight.js +26 -0
- package/dist/editor/features/code/lowlight.js.map +1 -0
- package/dist/editor/features/color/CustomColorPopover.d.ts +40 -0
- package/dist/editor/features/color/CustomColorPopover.d.ts.map +1 -0
- package/dist/editor/features/color/CustomColorPopover.js +70 -0
- package/dist/editor/features/color/CustomColorPopover.js.map +1 -0
- package/dist/editor/features/color/contrast.d.ts +26 -0
- package/dist/editor/features/color/contrast.d.ts.map +1 -0
- package/dist/editor/features/color/contrast.js +27 -0
- package/dist/editor/features/color/contrast.js.map +1 -0
- package/dist/editor/features/color/customColor.d.ts +65 -0
- package/dist/editor/features/color/customColor.d.ts.map +1 -0
- package/dist/editor/features/color/customColor.js +29 -0
- package/dist/editor/features/color/customColor.js.map +1 -0
- package/dist/editor/features/color/styleExtensions.d.ts +28 -0
- package/dist/editor/features/color/styleExtensions.d.ts.map +1 -0
- package/dist/editor/features/color/styleExtensions.js +172 -0
- package/dist/editor/features/color/styleExtensions.js.map +1 -0
- package/dist/editor/features/emoji/EmojiGrid.d.ts +34 -0
- package/dist/editor/features/emoji/EmojiGrid.d.ts.map +1 -0
- package/dist/editor/features/emoji/EmojiGrid.js +71 -0
- package/dist/editor/features/emoji/EmojiGrid.js.map +1 -0
- package/dist/editor/features/emoji/emoji.d.ts +47 -0
- package/dist/editor/features/emoji/emoji.d.ts.map +1 -0
- package/dist/editor/features/emoji/emoji.js +108 -0
- package/dist/editor/features/emoji/emoji.js.map +1 -0
- package/dist/editor/features/emoji/emojiTool.d.ts +32 -0
- package/dist/editor/features/emoji/emojiTool.d.ts.map +1 -0
- package/dist/editor/features/emoji/emojiTool.js +34 -0
- package/dist/editor/features/emoji/emojiTool.js.map +1 -0
- package/dist/editor/features/image/ImagePopover.d.ts +29 -0
- package/dist/editor/features/image/ImagePopover.d.ts.map +1 -0
- package/dist/editor/features/image/ImagePopover.js +89 -0
- package/dist/editor/features/image/ImagePopover.js.map +1 -0
- package/dist/editor/features/image/ImageToolbar.d.ts +25 -0
- package/dist/editor/features/image/ImageToolbar.d.ts.map +1 -0
- package/dist/editor/features/image/ImageToolbar.js +49 -0
- package/dist/editor/features/image/ImageToolbar.js.map +1 -0
- package/dist/editor/features/image/image.d.ts +115 -0
- package/dist/editor/features/image/image.d.ts.map +1 -0
- package/dist/editor/features/image/image.js +539 -0
- package/dist/editor/features/image/image.js.map +1 -0
- package/dist/editor/features/image/imageNotice.d.ts +6 -0
- package/dist/editor/features/image/imageNotice.d.ts.map +1 -0
- package/dist/editor/features/image/imageNotice.js +16 -0
- package/dist/editor/features/image/imageNotice.js.map +1 -0
- package/dist/editor/features/layout/columnLayouts.d.ts +36 -0
- package/dist/editor/features/layout/columnLayouts.d.ts.map +1 -0
- package/dist/editor/features/layout/columnLayouts.js +33 -0
- package/dist/editor/features/layout/columnLayouts.js.map +1 -0
- package/dist/editor/features/layout/layout.d.ts +81 -0
- package/dist/editor/features/layout/layout.d.ts.map +1 -0
- package/dist/editor/features/layout/layout.js +318 -0
- package/dist/editor/features/layout/layout.js.map +1 -0
- package/dist/editor/features/link/LinkPopover.d.ts +21 -0
- package/dist/editor/features/link/LinkPopover.d.ts.map +1 -0
- package/dist/editor/features/link/LinkPopover.js +54 -0
- package/dist/editor/features/link/LinkPopover.js.map +1 -0
- package/dist/editor/features/link/link.d.ts +41 -0
- package/dist/editor/features/link/link.d.ts.map +1 -0
- package/dist/editor/features/link/link.js +66 -0
- package/dist/editor/features/link/link.js.map +1 -0
- package/dist/editor/features/link/linkTool.d.ts +13 -0
- package/dist/editor/features/link/linkTool.d.ts.map +1 -0
- package/dist/editor/features/link/linkTool.js +25 -0
- package/dist/editor/features/link/linkTool.js.map +1 -0
- package/dist/editor/features/search/SearchPanel.d.ts +31 -0
- package/dist/editor/features/search/SearchPanel.d.ts.map +1 -0
- package/dist/editor/features/search/SearchPanel.js +101 -0
- package/dist/editor/features/search/SearchPanel.js.map +1 -0
- package/dist/editor/features/search/revealMatch.d.ts +25 -0
- package/dist/editor/features/search/revealMatch.d.ts.map +1 -0
- package/dist/editor/features/search/revealMatch.js +41 -0
- package/dist/editor/features/search/revealMatch.js.map +1 -0
- package/dist/editor/features/search/searchMatch.d.ts +53 -0
- package/dist/editor/features/search/searchMatch.d.ts.map +1 -0
- package/dist/editor/features/search/searchMatch.js +67 -0
- package/dist/editor/features/search/searchMatch.js.map +1 -0
- package/dist/editor/features/search/searchPlugin.d.ts +48 -0
- package/dist/editor/features/search/searchPlugin.d.ts.map +1 -0
- package/dist/editor/features/search/searchPlugin.js +151 -0
- package/dist/editor/features/search/searchPlugin.js.map +1 -0
- package/dist/editor/features/search/searchTool.d.ts +13 -0
- package/dist/editor/features/search/searchTool.d.ts.map +1 -0
- package/dist/editor/features/search/searchTool.js +18 -0
- package/dist/editor/features/search/searchTool.js.map +1 -0
- package/dist/editor/features/table/TableInsertDialog.d.ts +29 -0
- package/dist/editor/features/table/TableInsertDialog.d.ts.map +1 -0
- package/dist/editor/features/table/TableInsertDialog.js +37 -0
- package/dist/editor/features/table/TableInsertDialog.js.map +1 -0
- package/dist/editor/features/table/TableMenuButton.d.ts +40 -0
- package/dist/editor/features/table/TableMenuButton.d.ts.map +1 -0
- package/dist/editor/features/table/TableMenuButton.js +97 -0
- package/dist/editor/features/table/TableMenuButton.js.map +1 -0
- package/dist/editor/features/table/TableSizeGrid.d.ts +60 -0
- package/dist/editor/features/table/TableSizeGrid.d.ts.map +1 -0
- package/dist/editor/features/table/TableSizeGrid.js +88 -0
- package/dist/editor/features/table/TableSizeGrid.js.map +1 -0
- package/dist/editor/features/table/tableActions.d.ts +110 -0
- package/dist/editor/features/table/tableActions.d.ts.map +1 -0
- package/dist/editor/features/table/tableActions.js +254 -0
- package/dist/editor/features/table/tableActions.js.map +1 -0
- package/dist/editor/features/table/tableCells.d.ts +41 -0
- package/dist/editor/features/table/tableCells.d.ts.map +1 -0
- package/dist/editor/features/table/tableCells.js +38 -0
- package/dist/editor/features/table/tableCells.js.map +1 -0
- package/dist/editor/features/table/tableChunk.d.ts +4 -0
- package/dist/editor/features/table/tableChunk.d.ts.map +1 -0
- package/dist/editor/features/table/tableChunk.js +5 -0
- package/dist/editor/features/table/tableChunk.js.map +1 -0
- package/dist/editor/features/table/tableTool.d.ts +22 -0
- package/dist/editor/features/table/tableTool.d.ts.map +1 -0
- package/dist/editor/features/table/tableTool.js +42 -0
- package/dist/editor/features/table/tableTool.js.map +1 -0
- package/dist/editor/features/table/tables.d.ts +79 -0
- package/dist/editor/features/table/tables.d.ts.map +1 -0
- package/dist/editor/features/table/tables.js +118 -0
- package/dist/editor/features/table/tables.js.map +1 -0
- package/dist/editor/features/wrapper/wrapper.d.ts +73 -0
- package/dist/editor/features/wrapper/wrapper.d.ts.map +1 -0
- package/dist/editor/features/wrapper/wrapper.js +198 -0
- package/dist/editor/features/wrapper/wrapper.js.map +1 -0
- package/dist/editor/features/wrapper/wrapperTones.d.ts +39 -0
- package/dist/editor/features/wrapper/wrapperTones.d.ts.map +1 -0
- package/dist/editor/features/wrapper/wrapperTones.js +19 -0
- package/dist/editor/features/wrapper/wrapperTones.js.map +1 -0
- package/dist/editor/highlight-entry.d.ts +24 -0
- package/dist/editor/highlight-entry.d.ts.map +1 -0
- package/dist/editor/highlight-entry.js +3 -0
- package/dist/editor/highlight-entry.js.map +1 -0
- package/dist/editor/hooks/useChangeThrottle.d.ts +24 -0
- package/dist/editor/hooks/useChangeThrottle.d.ts.map +1 -0
- package/dist/editor/hooks/useChangeThrottle.js +35 -0
- package/dist/editor/hooks/useChangeThrottle.js.map +1 -0
- package/dist/editor/hooks/useCoarsePointer.d.ts +17 -0
- package/dist/editor/hooks/useCoarsePointer.d.ts.map +1 -0
- package/dist/editor/hooks/useCoarsePointer.js +19 -0
- package/dist/editor/hooks/useCoarsePointer.js.map +1 -0
- package/dist/editor/hooks/useFieldsetDisabled.d.ts +22 -0
- package/dist/editor/hooks/useFieldsetDisabled.d.ts.map +1 -0
- package/dist/editor/hooks/useFieldsetDisabled.js +29 -0
- package/dist/editor/hooks/useFieldsetDisabled.js.map +1 -0
- package/dist/editor/hooks/useRichTextEditorState.d.ts +25 -0
- package/dist/editor/hooks/useRichTextEditorState.d.ts.map +1 -0
- package/dist/editor/hooks/useRichTextEditorState.js +17 -0
- package/dist/editor/hooks/useRichTextEditorState.js.map +1 -0
- package/dist/editor/hooks/useSubmenuPlacement.d.ts +19 -0
- package/dist/editor/hooks/useSubmenuPlacement.d.ts.map +1 -0
- package/dist/editor/hooks/useSubmenuPlacement.js +20 -0
- package/dist/editor/hooks/useSubmenuPlacement.js.map +1 -0
- package/dist/editor/index.d.ts +32 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +7 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/schema-entry.d.ts +29 -0
- package/dist/editor/schema-entry.d.ts.map +1 -0
- package/dist/editor/schema-entry.js +3 -0
- package/dist/editor/schema-entry.js.map +1 -0
- package/dist/editor/styles/codeTheme.d.ts +49 -0
- package/dist/editor/styles/codeTheme.d.ts.map +1 -0
- package/dist/editor/styles/codeTheme.js +11 -0
- package/dist/editor/styles/codeTheme.js.map +1 -0
- package/dist/editor/styles/glass.d.ts +40 -0
- package/dist/editor/styles/glass.d.ts.map +1 -0
- package/dist/editor/styles/glass.js +15 -0
- package/dist/editor/styles/glass.js.map +1 -0
- package/dist/editor/styles/palette.d.ts +181 -0
- package/dist/editor/styles/palette.d.ts.map +1 -0
- package/dist/editor/styles/palette.js +57 -0
- package/dist/editor/styles/palette.js.map +1 -0
- package/dist/editor/styles/prose.d.ts +56 -0
- package/dist/editor/styles/prose.d.ts.map +1 -0
- package/dist/editor/styles/prose.js +154 -0
- package/dist/editor/styles/prose.js.map +1 -0
- package/dist/editor/styles/surface.d.ts +22 -0
- package/dist/editor/styles/surface.d.ts.map +1 -0
- package/dist/editor/styles/surface.js +4 -0
- package/dist/editor/styles/surface.js.map +1 -0
- package/dist/editor/styles/tokens.d.ts +136 -0
- package/dist/editor/styles/tokens.d.ts.map +1 -0
- package/dist/editor/styles/tokens.js +38 -0
- package/dist/editor/styles/tokens.js.map +1 -0
- package/dist/editor/ui/BlockHandle.d.ts +40 -0
- package/dist/editor/ui/BlockHandle.d.ts.map +1 -0
- package/dist/editor/ui/BlockHandle.js +394 -0
- package/dist/editor/ui/BlockHandle.js.map +1 -0
- package/dist/editor/ui/BlockIsland.d.ts +3 -0
- package/dist/editor/ui/BlockIsland.d.ts.map +1 -0
- package/dist/editor/ui/BlockIsland.js +8 -0
- package/dist/editor/ui/BlockIsland.js.map +1 -0
- package/dist/editor/ui/BlockIslandSlots.d.ts +31 -0
- package/dist/editor/ui/BlockIslandSlots.d.ts.map +1 -0
- package/dist/editor/ui/BlockIslandSlots.js +61 -0
- package/dist/editor/ui/BlockIslandSlots.js.map +1 -0
- package/dist/editor/ui/ColorMenus.d.ts +18 -0
- package/dist/editor/ui/ColorMenus.d.ts.map +1 -0
- package/dist/editor/ui/ColorMenus.js +281 -0
- package/dist/editor/ui/ColorMenus.js.map +1 -0
- package/dist/editor/ui/EditorToolbar.d.ts +71 -0
- package/dist/editor/ui/EditorToolbar.d.ts.map +1 -0
- package/dist/editor/ui/EditorToolbar.js +197 -0
- package/dist/editor/ui/EditorToolbar.js.map +1 -0
- package/dist/editor/ui/FieldResizer.d.ts +22 -0
- package/dist/editor/ui/FieldResizer.d.ts.map +1 -0
- package/dist/editor/ui/FieldResizer.js +75 -0
- package/dist/editor/ui/FieldResizer.js.map +1 -0
- package/dist/editor/ui/FieldStatus.d.ts +32 -0
- package/dist/editor/ui/FieldStatus.d.ts.map +1 -0
- package/dist/editor/ui/FieldStatus.js +15 -0
- package/dist/editor/ui/FieldStatus.js.map +1 -0
- package/dist/editor/ui/FloatingToolbar.d.ts +32 -0
- package/dist/editor/ui/FloatingToolbar.d.ts.map +1 -0
- package/dist/editor/ui/FloatingToolbar.js +17 -0
- package/dist/editor/ui/FloatingToolbar.js.map +1 -0
- package/dist/editor/ui/FormatMenus.d.ts +210 -0
- package/dist/editor/ui/FormatMenus.d.ts.map +1 -0
- package/dist/editor/ui/FormatMenus.js +296 -0
- package/dist/editor/ui/FormatMenus.js.map +1 -0
- package/dist/editor/ui/InsertMenuButton.d.ts +25 -0
- package/dist/editor/ui/InsertMenuButton.d.ts.map +1 -0
- package/dist/editor/ui/InsertMenuButton.js +28 -0
- package/dist/editor/ui/InsertMenuButton.js.map +1 -0
- package/dist/editor/ui/SelectionBubble.d.ts +46 -0
- package/dist/editor/ui/SelectionBubble.d.ts.map +1 -0
- package/dist/editor/ui/SelectionBubble.js +90 -0
- package/dist/editor/ui/SelectionBubble.js.map +1 -0
- package/dist/editor/ui/ShortcutsButton.d.ts +29 -0
- package/dist/editor/ui/ShortcutsButton.d.ts.map +1 -0
- package/dist/editor/ui/ShortcutsButton.js +34 -0
- package/dist/editor/ui/ShortcutsButton.js.map +1 -0
- package/dist/editor/ui/SlashMenu.d.ts +42 -0
- package/dist/editor/ui/SlashMenu.d.ts.map +1 -0
- package/dist/editor/ui/SlashMenu.js +114 -0
- package/dist/editor/ui/SlashMenu.js.map +1 -0
- package/dist/editor/ui/SuggestionList.d.ts +60 -0
- package/dist/editor/ui/SuggestionList.d.ts.map +1 -0
- package/dist/editor/ui/SuggestionList.js +61 -0
- package/dist/editor/ui/SuggestionList.js.map +1 -0
- package/dist/editor/ui/TextCard.d.ts +20 -0
- package/dist/editor/ui/TextCard.d.ts.map +1 -0
- package/dist/editor/ui/TextCard.js +192 -0
- package/dist/editor/ui/TextCard.js.map +1 -0
- package/dist/editor/ui/ToolButtonView.d.ts +58 -0
- package/dist/editor/ui/ToolButtonView.d.ts.map +1 -0
- package/dist/editor/ui/ToolButtonView.js +86 -0
- package/dist/editor/ui/ToolButtonView.js.map +1 -0
- package/dist/editor/ui/TriggerMenu.d.ts +25 -0
- package/dist/editor/ui/TriggerMenu.d.ts.map +1 -0
- package/dist/editor/ui/TriggerMenu.js +180 -0
- package/dist/editor/ui/TriggerMenu.js.map +1 -0
- package/dist/editor/ui/focusWithin.d.ts +22 -0
- package/dist/editor/ui/focusWithin.d.ts.map +1 -0
- package/dist/editor/ui/focusWithin.js +5 -0
- package/dist/editor/ui/focusWithin.js.map +1 -0
- package/dist/editor/ui/lockableMenu.d.ts +28 -0
- package/dist/editor/ui/lockableMenu.d.ts.map +1 -0
- package/dist/editor/ui/lockableMenu.js +17 -0
- package/dist/editor/ui/lockableMenu.js.map +1 -0
- package/dist/editor/ui/rowMarks.d.ts +21 -0
- package/dist/editor/ui/rowMarks.d.ts.map +1 -0
- package/dist/editor/ui/rowMarks.js +35 -0
- package/dist/editor/ui/rowMarks.js.map +1 -0
- package/dist/editor/ui/selectionMenu.d.ts +61 -0
- package/dist/editor/ui/selectionMenu.d.ts.map +1 -0
- package/dist/editor/ui/selectionMenu.js +32 -0
- package/dist/editor/ui/selectionMenu.js.map +1 -0
- package/dist/hooks/useFieldDescription.d.ts +45 -0
- package/dist/hooks/useFieldDescription.d.ts.map +1 -0
- package/dist/hooks/useFieldDescription.js +23 -0
- package/dist/hooks/useFieldDescription.js.map +1 -0
- package/dist/hooks/useMediaQuery.d.ts +25 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/useMediaQuery.js +14 -0
- package/dist/hooks/useMediaQuery.js.map +1 -0
- package/dist/hooks/useOpenerFocus.d.ts +10 -0
- package/dist/hooks/useOpenerFocus.d.ts.map +1 -0
- package/dist/hooks/useOpenerFocus.js +35 -0
- package/dist/hooks/useOpenerFocus.js.map +1 -0
- package/dist/hooks/useOverlayLayer.d.ts +23 -0
- package/dist/hooks/useOverlayLayer.d.ts.map +1 -0
- package/dist/hooks/useOverlayLayer.js +19 -0
- package/dist/hooks/useOverlayLayer.js.map +1 -0
- package/dist/hooks/useScrollLock.d.ts +3 -0
- package/dist/hooks/useScrollLock.d.ts.map +1 -0
- package/dist/hooks/useScrollLock.js +52 -0
- package/dist/hooks/useScrollLock.js.map +1 -0
- package/dist/hooks/useStickyOffset.d.ts +53 -0
- package/dist/hooks/useStickyOffset.d.ts.map +1 -0
- package/dist/hooks/useStickyOffset.js +91 -0
- package/dist/hooks/useStickyOffset.js.map +1 -0
- package/dist/hooks/useTableState.d.ts +35 -0
- package/dist/hooks/useTableState.d.ts.map +1 -0
- package/dist/hooks/useTableState.js +23 -0
- package/dist/hooks/useTableState.js.map +1 -0
- package/dist/i18n/UiProvider.d.ts +53 -0
- package/dist/i18n/UiProvider.d.ts.map +1 -0
- package/dist/i18n/UiProvider.js +63 -0
- package/dist/i18n/UiProvider.js.map +1 -0
- package/dist/i18n/context.d.ts +26 -0
- package/dist/i18n/context.d.ts.map +1 -0
- package/dist/i18n/context.js +4 -0
- package/dist/i18n/context.js.map +1 -0
- package/dist/i18n/defaults.d.ts +19 -0
- package/dist/i18n/defaults.d.ts.map +1 -0
- package/dist/i18n/defaults.js +574 -0
- package/dist/i18n/defaults.js.map +1 -0
- package/dist/i18n/messages.d.ts +1178 -0
- package/dist/i18n/messages.d.ts.map +1 -0
- package/dist/i18n/messages.js +2 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/i18n/useMessages.d.ts +26 -0
- package/dist/i18n/useMessages.d.ts.map +1 -0
- package/dist/i18n/useMessages.js +22 -0
- package/dist/i18n/useMessages.js.map +1 -0
- package/dist/icons.d.ts +437 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +2 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +250 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/accessible-name.d.ts +34 -0
- package/dist/lib/accessible-name.d.ts.map +1 -0
- package/dist/lib/accessible-name.js +2 -0
- package/dist/lib/accessible-name.js.map +1 -0
- package/dist/lib/bottom-bar-slot.d.ts +24 -0
- package/dist/lib/bottom-bar-slot.d.ts.map +1 -0
- package/dist/lib/bottom-bar-slot.js +4 -0
- package/dist/lib/bottom-bar-slot.js.map +1 -0
- package/dist/lib/button.d.ts +36 -0
- package/dist/lib/button.d.ts.map +1 -0
- package/dist/lib/button.js +35 -0
- package/dist/lib/button.js.map +1 -0
- package/dist/lib/choice-card.d.ts +64 -0
- package/dist/lib/choice-card.d.ts.map +1 -0
- package/dist/lib/choice-card.js +15 -0
- package/dist/lib/choice-card.js.map +1 -0
- package/dist/lib/clamp.d.ts +17 -0
- package/dist/lib/clamp.d.ts.map +1 -0
- package/dist/lib/clamp.js +4 -0
- package/dist/lib/clamp.js.map +1 -0
- package/dist/lib/cn.d.ts +12 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +14 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/date.d.ts +61 -0
- package/dist/lib/date.d.ts.map +1 -0
- package/dist/lib/date.js +30 -0
- package/dist/lib/date.js.map +1 -0
- package/dist/lib/dev-warn.d.ts +26 -0
- package/dist/lib/dev-warn.d.ts.map +1 -0
- package/dist/lib/dev-warn.js +28 -0
- package/dist/lib/dev-warn.js.map +1 -0
- package/dist/lib/empty.d.ts +35 -0
- package/dist/lib/empty.d.ts.map +1 -0
- package/dist/lib/empty.js +11 -0
- package/dist/lib/empty.js.map +1 -0
- package/dist/lib/field.d.ts +105 -0
- package/dist/lib/field.d.ts.map +1 -0
- package/dist/lib/field.js +18 -0
- package/dist/lib/field.js.map +1 -0
- package/dist/lib/filter-range.d.ts +24 -0
- package/dist/lib/filter-range.d.ts.map +1 -0
- package/dist/lib/filter-range.js +27 -0
- package/dist/lib/filter-range.js.map +1 -0
- package/dist/lib/interaction-modality.d.ts +40 -0
- package/dist/lib/interaction-modality.d.ts.map +1 -0
- package/dist/lib/interaction-modality.js +60 -0
- package/dist/lib/interaction-modality.js.map +1 -0
- package/dist/lib/mask.d.ts +94 -0
- package/dist/lib/mask.d.ts.map +1 -0
- package/dist/lib/mask.js +114 -0
- package/dist/lib/mask.js.map +1 -0
- package/dist/lib/menu.d.ts +85 -0
- package/dist/lib/menu.d.ts.map +1 -0
- package/dist/lib/menu.js +33 -0
- package/dist/lib/menu.js.map +1 -0
- package/dist/lib/overlay-layer.d.ts +101 -0
- package/dist/lib/overlay-layer.d.ts.map +1 -0
- package/dist/lib/overlay-layer.js +40 -0
- package/dist/lib/overlay-layer.js.map +1 -0
- package/dist/lib/period.d.ts +82 -0
- package/dist/lib/period.d.ts.map +1 -0
- package/dist/lib/period.js +58 -0
- package/dist/lib/period.js.map +1 -0
- package/dist/lib/progress-status.d.ts +38 -0
- package/dist/lib/progress-status.d.ts.map +1 -0
- package/dist/lib/progress-status.js +7 -0
- package/dist/lib/progress-status.js.map +1 -0
- package/dist/lib/published-height.d.ts +16 -0
- package/dist/lib/published-height.d.ts.map +1 -0
- package/dist/lib/published-height.js +72 -0
- package/dist/lib/published-height.js.map +1 -0
- package/dist/lib/reorder.d.ts +44 -0
- package/dist/lib/reorder.d.ts.map +1 -0
- package/dist/lib/reorder.js +22 -0
- package/dist/lib/reorder.js.map +1 -0
- package/dist/lib/responsive.d.ts +51 -0
- package/dist/lib/responsive.d.ts.map +1 -0
- package/dist/lib/responsive.js +12 -0
- package/dist/lib/responsive.js.map +1 -0
- package/dist/lib/state.d.ts +25 -0
- package/dist/lib/state.d.ts.map +1 -0
- package/dist/lib/state.js +21 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/table-state.d.ts +105 -0
- package/dist/lib/table-state.d.ts.map +1 -0
- package/dist/lib/table-state.js +121 -0
- package/dist/lib/table-state.js.map +1 -0
- package/dist/lib/tag.d.ts +25 -0
- package/dist/lib/tag.d.ts.map +1 -0
- package/dist/lib/tag.js +6 -0
- package/dist/lib/tag.js.map +1 -0
- package/dist/lib/themes.d.ts +54 -0
- package/dist/lib/themes.d.ts.map +1 -0
- package/dist/lib/themes.js +40 -0
- package/dist/lib/themes.js.map +1 -0
- package/dist/lib/time.d.ts +35 -0
- package/dist/lib/time.d.ts.map +1 -0
- package/dist/lib/time.js +68 -0
- package/dist/lib/time.js.map +1 -0
- package/dist/lib/toolbar-axis.d.ts +17 -0
- package/dist/lib/toolbar-axis.d.ts.map +1 -0
- package/dist/lib/toolbar-axis.js +10 -0
- package/dist/lib/toolbar-axis.js.map +1 -0
- package/dist/lib/touch.d.ts +16 -0
- package/dist/lib/touch.d.ts.map +1 -0
- package/dist/lib/touch.js +2 -0
- package/dist/lib/touch.js.map +1 -0
- package/dist/lib/variants.d.ts +204 -0
- package/dist/lib/variants.d.ts.map +1 -0
- package/dist/lib/variants.js +127 -0
- package/dist/lib/variants.js.map +1 -0
- package/dist/locales/en.d.ts +11 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +3 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/locales/ru.d.ts +5 -0
- package/dist/locales/ru.d.ts.map +1 -0
- package/dist/locales/ru.js +591 -0
- package/dist/locales/ru.js.map +1 -0
- package/docs/accessibility.en.md +79 -0
- package/docs/accessibility.md +73 -0
- package/docs/api/editor-extension.en.md +309 -0
- package/docs/api/editor-extension.md +308 -0
- package/docs/api/messages.md +750 -0
- package/docs/api/migration-notes.en.md +885 -0
- package/docs/api/migration-notes.md +890 -0
- package/docs/api/utilities.md +252 -0
- package/docs/component-selection.en.md +48 -0
- package/docs/component-selection.md +44 -0
- package/docs/components/actions/button.en.md +59 -0
- package/docs/components/actions/button.md +58 -0
- package/docs/components/actions/chip.en.md +47 -0
- package/docs/components/actions/chip.md +46 -0
- package/docs/components/actions/copy-and-truncate.en.md +98 -0
- package/docs/components/actions/copy-and-truncate.md +96 -0
- package/docs/components/actions/feedback.en.md +69 -0
- package/docs/components/actions/feedback.md +68 -0
- package/docs/components/actions/split-button.en.md +76 -0
- package/docs/components/actions/split-button.md +75 -0
- package/docs/components/actions/toggles.en.md +139 -0
- package/docs/components/actions/toggles.md +136 -0
- package/docs/components/actions/toolbar.en.md +197 -0
- package/docs/components/actions/toolbar.md +192 -0
- package/docs/components/app-shell/app-shell.en.md +172 -0
- package/docs/components/app-shell/app-shell.md +172 -0
- package/docs/components/app-shell/mobile-tab-bar.en.md +207 -0
- package/docs/components/app-shell/mobile-tab-bar.md +210 -0
- package/docs/components/app-shell/side-nav.en.md +153 -0
- package/docs/components/app-shell/side-nav.md +152 -0
- package/docs/components/app-shell/split-panel.en.md +148 -0
- package/docs/components/app-shell/split-panel.md +147 -0
- package/docs/components/app-shell/top-bar.en.md +103 -0
- package/docs/components/app-shell/top-bar.md +100 -0
- package/docs/components/content/avatar.en.md +123 -0
- package/docs/components/content/avatar.md +119 -0
- package/docs/components/content/card.en.md +123 -0
- package/docs/components/content/card.md +122 -0
- package/docs/components/content/carousel.en.md +236 -0
- package/docs/components/content/carousel.md +227 -0
- package/docs/components/content/code-view.en.md +177 -0
- package/docs/components/content/code-view.md +172 -0
- package/docs/components/content/data-display.en.md +205 -0
- package/docs/components/content/data-display.md +212 -0
- package/docs/components/content/editable-list.en.md +437 -0
- package/docs/components/content/editable-list.md +418 -0
- package/docs/components/content/empty-and-loading.en.md +149 -0
- package/docs/components/content/empty-and-loading.md +149 -0
- package/docs/components/content/grid.en.md +148 -0
- package/docs/components/content/grid.md +142 -0
- package/docs/components/content/image.en.md +115 -0
- package/docs/components/content/image.md +114 -0
- package/docs/components/content/keyboard.en.md +129 -0
- package/docs/components/content/keyboard.md +129 -0
- package/docs/components/content/layout-primitives.en.md +105 -0
- package/docs/components/content/layout-primitives.md +104 -0
- package/docs/components/content/media-card.en.md +137 -0
- package/docs/components/content/media-card.md +132 -0
- package/docs/components/content/notifications.en.md +216 -0
- package/docs/components/content/notifications.md +207 -0
- package/docs/components/content/rich-text-view.en.md +325 -0
- package/docs/components/content/rich-text-view.md +324 -0
- package/docs/components/content/sortable-list.en.md +212 -0
- package/docs/components/content/sortable-list.md +206 -0
- package/docs/components/content/tag-group.en.md +158 -0
- package/docs/components/content/tag-group.md +151 -0
- package/docs/components/content/timeline.en.md +77 -0
- package/docs/components/content/timeline.md +76 -0
- package/docs/components/content/typography.en.md +152 -0
- package/docs/components/content/typography.md +146 -0
- package/docs/components/content/virtual-grid.en.md +305 -0
- package/docs/components/content/virtual-grid.md +315 -0
- package/docs/components/date-time/calendar.en.md +47 -0
- package/docs/components/date-time/calendar.md +46 -0
- package/docs/components/date-time/date-picker.en.md +52 -0
- package/docs/components/date-time/date-picker.md +50 -0
- package/docs/components/date-time/date-range-picker.en.md +58 -0
- package/docs/components/date-time/date-range-picker.md +57 -0
- package/docs/components/date-time/date-time-picker.en.md +44 -0
- package/docs/components/date-time/date-time-picker.md +43 -0
- package/docs/components/date-time/range-calendar.en.md +47 -0
- package/docs/components/date-time/range-calendar.md +46 -0
- package/docs/components/date-time/time-picker.en.md +47 -0
- package/docs/components/date-time/time-picker.md +51 -0
- package/docs/components/index.en.md +155 -0
- package/docs/components/index.md +150 -0
- package/docs/components/inputs/choice-controls.en.md +118 -0
- package/docs/components/inputs/choice-controls.md +112 -0
- package/docs/components/inputs/color-picker.en.md +126 -0
- package/docs/components/inputs/color-picker.md +122 -0
- package/docs/components/inputs/combobox.en.md +62 -0
- package/docs/components/inputs/combobox.md +58 -0
- package/docs/components/inputs/file-dropzone.en.md +51 -0
- package/docs/components/inputs/file-dropzone.md +50 -0
- package/docs/components/inputs/form-layout.en.md +99 -0
- package/docs/components/inputs/form-layout.md +97 -0
- package/docs/components/inputs/form.en.md +87 -0
- package/docs/components/inputs/form.md +86 -0
- package/docs/components/inputs/inline-edit.en.md +93 -0
- package/docs/components/inputs/inline-edit.md +94 -0
- package/docs/components/inputs/masked-input.en.md +149 -0
- package/docs/components/inputs/masked-input.md +147 -0
- package/docs/components/inputs/number-input.en.md +81 -0
- package/docs/components/inputs/number-input.md +80 -0
- package/docs/components/inputs/password-input.en.md +101 -0
- package/docs/components/inputs/password-input.md +97 -0
- package/docs/components/inputs/phone-input.en.md +78 -0
- package/docs/components/inputs/phone-input.md +76 -0
- package/docs/components/inputs/pin-input.en.md +109 -0
- package/docs/components/inputs/pin-input.md +108 -0
- package/docs/components/inputs/rating.en.md +100 -0
- package/docs/components/inputs/rating.md +99 -0
- package/docs/components/inputs/rich-text-editor.en.md +1539 -0
- package/docs/components/inputs/rich-text-editor.md +1538 -0
- package/docs/components/inputs/search-control.en.md +60 -0
- package/docs/components/inputs/search-control.md +58 -0
- package/docs/components/inputs/select.en.md +68 -0
- package/docs/components/inputs/select.md +65 -0
- package/docs/components/inputs/slider.en.md +60 -0
- package/docs/components/inputs/slider.md +59 -0
- package/docs/components/inputs/swatch-group.en.md +181 -0
- package/docs/components/inputs/swatch-group.md +177 -0
- package/docs/components/inputs/tag-input.en.md +50 -0
- package/docs/components/inputs/tag-input.md +48 -0
- package/docs/components/inputs/text-control.en.md +76 -0
- package/docs/components/inputs/text-control.md +74 -0
- package/docs/components/inputs/transfer.en.md +118 -0
- package/docs/components/inputs/transfer.md +114 -0
- package/docs/components/inputs/tree-select.en.md +91 -0
- package/docs/components/inputs/tree-select.md +89 -0
- package/docs/components/inputs/upload-queue.en.md +160 -0
- package/docs/components/inputs/upload-queue.md +158 -0
- package/docs/components/navigation/accordion.en.md +76 -0
- package/docs/components/navigation/accordion.md +75 -0
- package/docs/components/navigation/breadcrumbs.en.md +73 -0
- package/docs/components/navigation/breadcrumbs.md +72 -0
- package/docs/components/navigation/contextual-save-bar.en.md +104 -0
- package/docs/components/navigation/contextual-save-bar.md +103 -0
- package/docs/components/navigation/page-header.en.md +71 -0
- package/docs/components/navigation/page-header.md +70 -0
- package/docs/components/navigation/pagination.en.md +69 -0
- package/docs/components/navigation/pagination.md +69 -0
- package/docs/components/navigation/section-header.en.md +67 -0
- package/docs/components/navigation/section-header.md +66 -0
- package/docs/components/navigation/selection-bar.en.md +83 -0
- package/docs/components/navigation/selection-bar.md +83 -0
- package/docs/components/navigation/stepper.en.md +78 -0
- package/docs/components/navigation/stepper.md +77 -0
- package/docs/components/navigation/tabs.en.md +71 -0
- package/docs/components/navigation/tabs.md +70 -0
- package/docs/components/navigation/tree-view.en.md +173 -0
- package/docs/components/navigation/tree-view.md +172 -0
- package/docs/components/overlays/command-palette.en.md +153 -0
- package/docs/components/overlays/command-palette.md +147 -0
- package/docs/components/overlays/confirm-dialog.en.md +134 -0
- package/docs/components/overlays/confirm-dialog.md +127 -0
- package/docs/components/overlays/context-menu.en.md +115 -0
- package/docs/components/overlays/context-menu.md +113 -0
- package/docs/components/overlays/dialog.en.md +120 -0
- package/docs/components/overlays/dialog.md +117 -0
- package/docs/components/overlays/drawer.en.md +87 -0
- package/docs/components/overlays/drawer.md +88 -0
- package/docs/components/overlays/hover-card.en.md +179 -0
- package/docs/components/overlays/hover-card.md +175 -0
- package/docs/components/overlays/menu.en.md +103 -0
- package/docs/components/overlays/menu.md +100 -0
- package/docs/components/overlays/popover.en.md +78 -0
- package/docs/components/overlays/popover.md +77 -0
- package/docs/components/overlays/shortcuts-help.en.md +190 -0
- package/docs/components/overlays/shortcuts-help.md +186 -0
- package/docs/components/overlays/tooltip.en.md +78 -0
- package/docs/components/overlays/tooltip.md +77 -0
- package/docs/components/table/data-table.en.md +137 -0
- package/docs/components/table/data-table.md +133 -0
- package/docs/components/table/filter-bar.en.md +161 -0
- package/docs/components/table/filter-bar.md +155 -0
- package/docs/components/table/saved-views.en.md +117 -0
- package/docs/components/table/saved-views.md +115 -0
- package/docs/components/table/toolbar.en.md +100 -0
- package/docs/components/table/toolbar.md +97 -0
- package/docs/conventions.md +358 -0
- package/docs/getting-started.en.md +382 -0
- package/docs/getting-started.md +381 -0
- package/docs/glossary.en.md +179 -0
- package/docs/glossary.md +174 -0
- package/docs/i18n.en.md +147 -0
- package/docs/i18n.md +143 -0
- package/docs/icons.en.md +252 -0
- package/docs/icons.md +251 -0
- package/docs/index.md +62 -0
- package/docs/palette.en.md +400 -0
- package/docs/palette.md +396 -0
- package/docs/theming.en.md +522 -0
- package/docs/theming.md +518 -0
- package/llms.txt +144 -0
- package/package.json +173 -4
- package/src/color.ts +21 -0
- package/src/components/actions/Alert.tsx +121 -0
- package/src/components/actions/Banner.tsx +120 -0
- package/src/components/actions/Button.tsx +241 -0
- package/src/components/actions/ButtonGroup.tsx +65 -0
- package/src/components/actions/Chip.tsx +113 -0
- package/src/components/actions/CopyButton.tsx +112 -0
- package/src/components/actions/Progress.tsx +103 -0
- package/src/components/actions/Segmented.tsx +162 -0
- package/src/components/actions/Spinner.tsx +80 -0
- package/src/components/actions/SplitButton.tsx +164 -0
- package/src/components/actions/StatusBadge.tsx +61 -0
- package/src/components/actions/Toaster.tsx +148 -0
- package/src/components/actions/Toggle.tsx +224 -0
- package/src/components/actions/ToggleGroup.tsx +147 -0
- package/src/components/actions/Toolbar.tsx +223 -0
- package/src/components/actions/Truncate.tsx +96 -0
- package/src/components/app-shell/AppShell.tsx +268 -0
- package/src/components/app-shell/MobileTabBar.tsx +305 -0
- package/src/components/app-shell/NavItem.tsx +149 -0
- package/src/components/app-shell/SideNav.tsx +217 -0
- package/src/components/app-shell/SplitPanel.tsx +381 -0
- package/src/components/app-shell/TopBar.tsx +115 -0
- package/src/components/content/AspectRatio.tsx +45 -0
- package/src/components/content/Avatar.tsx +132 -0
- package/src/components/content/AvatarGroup.tsx +92 -0
- package/src/components/content/Badge.tsx +78 -0
- package/src/components/content/Card.tsx +117 -0
- package/src/components/content/Carousel.tsx +427 -0
- package/src/components/content/CodeView.tsx +178 -0
- package/src/components/content/DescriptionList.tsx +64 -0
- package/src/components/content/EditableList.tsx +879 -0
- package/src/components/content/EmptyState.tsx +65 -0
- package/src/components/content/ErrorState.tsx +100 -0
- package/src/components/content/Grid.tsx +208 -0
- package/src/components/content/Heading.tsx +81 -0
- package/src/components/content/Image.tsx +235 -0
- package/src/components/content/Kbd.tsx +67 -0
- package/src/components/content/Keyboard.tsx +634 -0
- package/src/components/content/Link.tsx +199 -0
- package/src/components/content/MediaCard.tsx +167 -0
- package/src/components/content/NotificationCenter.tsx +168 -0
- package/src/components/content/NotificationList.tsx +329 -0
- package/src/components/content/ScrollArea.tsx +69 -0
- package/src/components/content/Separator.tsx +62 -0
- package/src/components/content/Skeleton.tsx +40 -0
- package/src/components/content/SortableList.tsx +249 -0
- package/src/components/content/Stat.tsx +75 -0
- package/src/components/content/Table.tsx +500 -0
- package/src/components/content/TagGroup.tsx +222 -0
- package/src/components/content/Text.tsx +127 -0
- package/src/components/content/Timeline.tsx +126 -0
- package/src/components/content/VirtualGrid.tsx +726 -0
- package/src/components/content/VisuallyHidden.tsx +25 -0
- package/src/components/content/keyboardEditing.ts +51 -0
- package/src/components/content/useDragToSelect.ts +255 -0
- package/src/components/date-time/Calendar.tsx +181 -0
- package/src/components/date-time/DatePicker.tsx +143 -0
- package/src/components/date-time/DateRangePicker.tsx +349 -0
- package/src/components/date-time/DateTimePicker.tsx +92 -0
- package/src/components/date-time/RangeCalendar.tsx +156 -0
- package/src/components/date-time/TimePicker.tsx +377 -0
- package/src/components/inputs/Checkbox.tsx +284 -0
- package/src/components/inputs/CheckboxCard.tsx +132 -0
- package/src/components/inputs/CheckboxGroup.tsx +140 -0
- package/src/components/inputs/ChoiceCardBody.tsx +53 -0
- package/src/components/inputs/ColorPicker.tsx +221 -0
- package/src/components/inputs/Combobox.tsx +156 -0
- package/src/components/inputs/FileDropzone.tsx +253 -0
- package/src/components/inputs/Form.tsx +77 -0
- package/src/components/inputs/FormField.tsx +235 -0
- package/src/components/inputs/FormLayout.tsx +80 -0
- package/src/components/inputs/InlineEdit.tsx +205 -0
- package/src/components/inputs/MaskedInput.tsx +288 -0
- package/src/components/inputs/NumberInput.tsx +138 -0
- package/src/components/inputs/PasswordInput.tsx +166 -0
- package/src/components/inputs/PhoneInput.tsx +55 -0
- package/src/components/inputs/PinInput.tsx +197 -0
- package/src/components/inputs/RadioGroup.tsx +322 -0
- package/src/components/inputs/Rating.tsx +224 -0
- package/src/components/inputs/SearchControl.tsx +278 -0
- package/src/components/inputs/Select.tsx +741 -0
- package/src/components/inputs/Slider.tsx +436 -0
- package/src/components/inputs/SwatchGroup.tsx +342 -0
- package/src/components/inputs/TagInput.tsx +221 -0
- package/src/components/inputs/TextControl.tsx +335 -0
- package/src/components/inputs/Transfer.tsx +368 -0
- package/src/components/inputs/TreeSelect.tsx +275 -0
- package/src/components/inputs/UploadQueue.tsx +240 -0
- package/src/components/navigation/Accordion.tsx +118 -0
- package/src/components/navigation/Breadcrumbs.tsx +112 -0
- package/src/components/navigation/ContextualSaveBar.tsx +174 -0
- package/src/components/navigation/PageHeader.tsx +41 -0
- package/src/components/navigation/Pagination.tsx +184 -0
- package/src/components/navigation/SectionHeader.tsx +54 -0
- package/src/components/navigation/SelectionBar.tsx +151 -0
- package/src/components/navigation/Stepper.tsx +153 -0
- package/src/components/navigation/Tabs.tsx +130 -0
- package/src/components/navigation/TreeView.tsx +246 -0
- package/src/components/overlays/CommandPalette.tsx +343 -0
- package/src/components/overlays/ConfirmDialog.tsx +272 -0
- package/src/components/overlays/ContextMenu.tsx +284 -0
- package/src/components/overlays/Dialog.tsx +283 -0
- package/src/components/overlays/Drawer.tsx +193 -0
- package/src/components/overlays/HelpTooltip.tsx +96 -0
- package/src/components/overlays/HoverCard.tsx +404 -0
- package/src/components/overlays/Menu.tsx +243 -0
- package/src/components/overlays/ModalHeader.tsx +125 -0
- package/src/components/overlays/ModalOverlay.tsx +108 -0
- package/src/components/overlays/Popover.tsx +120 -0
- package/src/components/overlays/ShortcutsHelp.tsx +147 -0
- package/src/components/overlays/Tooltip.tsx +89 -0
- package/src/components/table/DataTable.tsx +164 -0
- package/src/components/table/FilterBar.tsx +114 -0
- package/src/components/table/FilterBarAdd.tsx +268 -0
- package/src/components/table/SavedViews.tsx +122 -0
- package/src/components/table/TablePreferences.tsx +173 -0
- package/src/components/table/TableToolbar.tsx +74 -0
- package/src/editor/RichTextEditor.tsx +1747 -0
- package/src/editor/RichTextView.tsx +119 -0
- package/src/editor/core/atomText.ts +140 -0
- package/src/editor/core/blockCommands.ts +329 -0
- package/src/editor/core/blockTypes.ts +252 -0
- package/src/editor/core/containers.ts +100 -0
- package/src/editor/core/customMarkdown.ts +74 -0
- package/src/editor/core/customTool.ts +472 -0
- package/src/editor/core/customToolEngine.ts +442 -0
- package/src/editor/core/document.ts +27 -0
- package/src/editor/core/emptiness.ts +31 -0
- package/src/editor/core/extensions.ts +295 -0
- package/src/editor/core/fieldNotice.ts +75 -0
- package/src/editor/core/handle.ts +311 -0
- package/src/editor/core/insertItems.ts +434 -0
- package/src/editor/core/jsonDocument.ts +99 -0
- package/src/editor/core/markdown.ts +313 -0
- package/src/editor/core/maxLength.ts +99 -0
- package/src/editor/core/pasteTrim.ts +85 -0
- package/src/editor/core/schema.ts +986 -0
- package/src/editor/core/schemaConform.ts +122 -0
- package/src/editor/core/shortcuts.ts +435 -0
- package/src/editor/core/skeleton.ts +108 -0
- package/src/editor/core/slashCommand.ts +143 -0
- package/src/editor/core/sourceFormat.ts +194 -0
- package/src/editor/core/toolButtons.ts +133 -0
- package/src/editor/core/toolDescriptor.ts +359 -0
- package/src/editor/core/toolEntryShortcuts.ts +66 -0
- package/src/editor/core/toolLoader.ts +50 -0
- package/src/editor/core/tools.ts +150 -0
- package/src/editor/core/viewIsland.ts +35 -0
- package/src/editor/features/builtins.ts +145 -0
- package/src/editor/features/code/CodeBlockView.tsx +240 -0
- package/src/editor/features/code/CodeCopyButton.tsx +76 -0
- package/src/editor/features/code/CodeCopyIsland.tsx +71 -0
- package/src/editor/features/code/codeBlock.ts +188 -0
- package/src/editor/features/code/codeBlockTool.ts +107 -0
- package/src/editor/features/code/codeHeader.ts +97 -0
- package/src/editor/features/code/codeLanguages.ts +62 -0
- package/src/editor/features/code/highlight.ts +109 -0
- package/src/editor/features/code/highlightNodes.ts +65 -0
- package/src/editor/features/code/lowlight.ts +45 -0
- package/src/editor/features/color/CustomColorPopover.tsx +180 -0
- package/src/editor/features/color/contrast.ts +52 -0
- package/src/editor/features/color/customColor.ts +91 -0
- package/src/editor/features/color/styleExtensions.ts +316 -0
- package/src/editor/features/emoji/EmojiGrid.tsx +160 -0
- package/src/editor/features/emoji/emoji.ts +156 -0
- package/src/editor/features/emoji/emojiTool.tsx +67 -0
- package/src/editor/features/image/ImagePopover.tsx +254 -0
- package/src/editor/features/image/ImageToolbar.tsx +147 -0
- package/src/editor/features/image/image.ts +821 -0
- package/src/editor/features/image/imageNotice.ts +27 -0
- package/src/editor/features/layout/columnLayouts.ts +71 -0
- package/src/editor/features/layout/layout.ts +537 -0
- package/src/editor/features/link/LinkPopover.tsx +114 -0
- package/src/editor/features/link/link.ts +112 -0
- package/src/editor/features/link/linkTool.tsx +36 -0
- package/src/editor/features/search/SearchPanel.tsx +264 -0
- package/src/editor/features/search/revealMatch.ts +69 -0
- package/src/editor/features/search/searchMatch.ts +129 -0
- package/src/editor/features/search/searchPlugin.ts +254 -0
- package/src/editor/features/search/searchTool.ts +30 -0
- package/src/editor/features/table/TableInsertDialog.tsx +129 -0
- package/src/editor/features/table/TableMenuButton.tsx +503 -0
- package/src/editor/features/table/TableSizeGrid.tsx +265 -0
- package/src/editor/features/table/tableActions.ts +380 -0
- package/src/editor/features/table/tableCells.ts +63 -0
- package/src/editor/features/table/tableChunk.ts +11 -0
- package/src/editor/features/table/tableTool.ts +74 -0
- package/src/editor/features/table/tables.ts +220 -0
- package/src/editor/features/wrapper/wrapper.ts +350 -0
- package/src/editor/features/wrapper/wrapperTones.ts +51 -0
- package/src/editor/highlight-entry.ts +23 -0
- package/src/editor/highlight-grammars.d.ts +12 -0
- package/src/editor/hooks/useChangeThrottle.ts +62 -0
- package/src/editor/hooks/useCoarsePointer.ts +36 -0
- package/src/editor/hooks/useFieldsetDisabled.ts +53 -0
- package/src/editor/hooks/useRichTextEditorState.ts +51 -0
- package/src/editor/hooks/useSubmenuPlacement.ts +40 -0
- package/src/editor/index.ts +59 -0
- package/src/editor/schema-entry.ts +40 -0
- package/src/editor/styles/codeTheme.ts +66 -0
- package/src/editor/styles/glass.ts +66 -0
- package/src/editor/styles/palette.ts +191 -0
- package/src/editor/styles/prose.ts +429 -0
- package/src/editor/styles/surface.ts +24 -0
- package/src/editor/styles/tokens.ts +173 -0
- package/src/editor/ui/BlockHandle.tsx +601 -0
- package/src/editor/ui/BlockIsland.tsx +23 -0
- package/src/editor/ui/BlockIslandSlots.tsx +114 -0
- package/src/editor/ui/ColorMenus.tsx +613 -0
- package/src/editor/ui/EditorToolbar.tsx +630 -0
- package/src/editor/ui/FieldResizer.tsx +155 -0
- package/src/editor/ui/FieldStatus.tsx +85 -0
- package/src/editor/ui/FloatingToolbar.tsx +77 -0
- package/src/editor/ui/FormatMenus.tsx +706 -0
- package/src/editor/ui/InsertMenuButton.tsx +104 -0
- package/src/editor/ui/SelectionBubble.tsx +317 -0
- package/src/editor/ui/ShortcutsButton.tsx +128 -0
- package/src/editor/ui/SlashMenu.tsx +199 -0
- package/src/editor/ui/SuggestionList.tsx +217 -0
- package/src/editor/ui/TextCard.tsx +312 -0
- package/src/editor/ui/ToolButtonView.tsx +242 -0
- package/src/editor/ui/TriggerMenu.tsx +255 -0
- package/src/editor/ui/focusWithin.ts +26 -0
- package/src/editor/ui/lockableMenu.ts +37 -0
- package/src/editor/ui/rowMarks.ts +66 -0
- package/src/editor/ui/selectionMenu.ts +97 -0
- package/src/hooks/useFieldDescription.ts +126 -0
- package/src/hooks/useMediaQuery.ts +46 -0
- package/src/hooks/useOpenerFocus.ts +118 -0
- package/src/hooks/useOverlayLayer.ts +42 -0
- package/src/hooks/useScrollLock.ts +92 -0
- package/src/hooks/useStickyOffset.ts +228 -0
- package/src/hooks/useTableState.ts +76 -0
- package/src/i18n/UiProvider.tsx +152 -0
- package/src/i18n/context.ts +30 -0
- package/src/i18n/defaults.ts +657 -0
- package/src/i18n/messages.ts +1183 -0
- package/src/i18n/useMessages.ts +47 -0
- package/src/icons.ts +566 -0
- package/src/index.ts +502 -0
- package/src/lib/accessible-name.ts +35 -0
- package/src/lib/bottom-bar-slot.ts +27 -0
- package/src/lib/button.ts +158 -0
- package/src/lib/choice-card.ts +79 -0
- package/src/lib/clamp.ts +18 -0
- package/src/lib/cn.ts +48 -0
- package/src/lib/date.ts +87 -0
- package/src/lib/dev-warn.ts +82 -0
- package/src/lib/empty.ts +43 -0
- package/src/lib/field.ts +125 -0
- package/src/lib/filter-range.ts +48 -0
- package/src/lib/interaction-modality.ts +134 -0
- package/src/lib/mask.ts +226 -0
- package/src/lib/menu.ts +196 -0
- package/src/lib/overlay-layer.ts +163 -0
- package/src/lib/period.ts +167 -0
- package/src/lib/progress-status.ts +44 -0
- package/src/lib/published-height.ts +147 -0
- package/src/lib/reorder.ts +65 -0
- package/src/lib/responsive.ts +71 -0
- package/src/lib/state.ts +39 -0
- package/src/lib/table-state.ts +264 -0
- package/src/lib/tag.ts +31 -0
- package/src/lib/themes.ts +144 -0
- package/src/lib/time.ts +107 -0
- package/src/lib/toolbar-axis.tsx +31 -0
- package/src/lib/touch.ts +15 -0
- package/src/lib/variants.ts +342 -0
- package/src/locales/en.ts +10 -0
- package/src/locales/ru.ts +695 -0
- package/styles.css +1650 -0
|
@@ -0,0 +1,1538 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: RichTextEditor
|
|
3
|
+
exports:
|
|
4
|
+
[
|
|
5
|
+
RichTextEditor,
|
|
6
|
+
RichTextEditorProps,
|
|
7
|
+
RichTextEditorHandle,
|
|
8
|
+
RichTextDocument,
|
|
9
|
+
useRichTextEditorState,
|
|
10
|
+
Tool,
|
|
11
|
+
ToolName,
|
|
12
|
+
RichTextEditorPreset,
|
|
13
|
+
RICH_TEXT_PRESETS,
|
|
14
|
+
CustomTool,
|
|
15
|
+
CustomToolButton,
|
|
16
|
+
CustomToolLabel,
|
|
17
|
+
CustomToolPopover,
|
|
18
|
+
CustomInsertItem,
|
|
19
|
+
CustomBlockAction,
|
|
20
|
+
CustomTrigger,
|
|
21
|
+
CustomIslandProps,
|
|
22
|
+
TriggerItem,
|
|
23
|
+
TriggerContext,
|
|
24
|
+
CustomTextCard,
|
|
25
|
+
CustomTextCardRender,
|
|
26
|
+
TextCardRange,
|
|
27
|
+
TextCardRangeAt,
|
|
28
|
+
markRange,
|
|
29
|
+
CustomBlock,
|
|
30
|
+
CustomMark,
|
|
31
|
+
CustomAttribute,
|
|
32
|
+
PrefixedName,
|
|
33
|
+
FieldNotice,
|
|
34
|
+
FieldNoticeTone,
|
|
35
|
+
]
|
|
36
|
+
category: inputs
|
|
37
|
+
status: stable
|
|
38
|
+
source: src/editor/RichTextEditor.tsx
|
|
39
|
+
story: src/editor/RichTextEditor.stories.tsx
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
# RichTextEditor
|
|
43
|
+
|
|
44
|
+
## Кратко для AI
|
|
45
|
+
|
|
46
|
+
`RichTextEditor` — поле форматированного текста на Tiptap v3 (ProseMirror).
|
|
47
|
+
|
|
48
|
+
- **Вход и движок.** Живёт отдельным входом `@olddevs/ui/editor`, не в barrel, а движок потребитель ставит сам: в манифесте кита его нет.
|
|
49
|
+
- **Значение и режимы.** Значение — HTML-строка, ограниченная Схемой; пустой Документ — `""`. Работают режимы `value`/`onChange` и `defaultValue`; `name` — для нативной формы; подпись и ошибка приходят от `FormField`.
|
|
50
|
+
- **Высота.** По умолчанию поле растёт с Документом. `maxHeight` (пиксели или CSS-длина) ограничивает область Документа: он прокручивается внутри рамки, Панель и Строка статуса остаются на месте. `resizable` даёт ей ручку высоты (полоса с хватом и ползунок с клавиатуры).
|
|
51
|
+
- **Панель и Меню выделения.** Панель (`toolbar`) — липкая над полем, плавающая над выделением или компактное Меню выделения на инверсной поверхности (`"bubble"`); на сенсорном экране — всегда липкая. `Alt+F10` переводит в неё фокус.
|
|
52
|
+
- **Ручка Блока.** У Блока с кареткой слева стоит Ручка «⋮» с меню Действий над Блоком (добавить ниже, формат, вверх, вниз, дублировать, удалить; `Ctrl/⌘+/`).
|
|
53
|
+
- **Ссылка и картинка.** Ссылка правится в поповере по `Ctrl/⌘+K`. Картинка — `<figure>` с подписью и шириной 50/75/100 %, вставка по адресу; а с `onUploadImage(file) → Promise<url>` ещё и из буфера, перетаскиванием и выбором файла (base64 в Документ не пишется никогда).
|
|
54
|
+
- **Блок кода.** С шапкой (язык слева, «Копировать» справа), языком (Python, TypeScript, JavaScript, Go, HTML, JSON, YAML, Markdown или обычный текст) и подсветкой lowlight. В Документ пишется только `class="language-…"`, а для `RichTextView` Блоки подсвечивает серверный хелпер из `@olddevs/ui/editor/highlight`.
|
|
55
|
+
- **HTML-режим.** Кнопка «HTML» фиксированной Панели (Инструмент `source`) меняет Документ на поле с его исходным HTML; выход, `blur`, отправка формы и `getHTML()` применяют правку через Схему, как входящий `value`.
|
|
56
|
+
- **Набор возможностей.** `preset="full" | "basic"` или точный список `tools`; выключенный Инструмент убирает и кнопку, и разметку из Схемы.
|
|
57
|
+
- **Хэндл (`ref`).** Экран управляет полем через `ref`: `insertHTML`, `insertText`, `getText`, `getSelectedText`, `isEmpty`, `focus(position?)`, `blur`, `undo`, `redo`, `openSearch`; `getJSON()`/`setJSON()` выгружают и загружают Документ JSON-конвертом без потерь (раздел «JSON»), конверт принимает и `defaultValue`. Свою панель экран строит на хуке `useRichTextEditorState(ref, selector)`. До Готовности (на сервере, до монтажа, пока работают Загрузчики Инструментов) методы тихие: раздел «Хэндл (`ref`) и своя панель вне поля».
|
|
58
|
+
- **События.** О событиях поля экран узнаёт колбэками `onReady()`, `onFocus()` и `onBlur()` (раздел «События»).
|
|
59
|
+
- **Скелет и Загрузчик.** До Готовности поле показывает Скелет: рамку и Документ статичным HTML без ввода, без прыжка высоты. Документ Скелета — `value`, очищенный Схемой, и только в браузере; на сервере Скелет — рамка без Документа. Часть для движка своего Инструмента можно передать Загрузчиком (`engine: () => import(…)`); его сбой — ошибка поля с «Повторить».
|
|
60
|
+
- **Строка статуса и Сообщение поля.** Под полем внутри рамки — Строка статуса (`status`: React-узел экрана у начала, счётчик кита в конце, без `role`) и Сообщение поля: команда движка `showNotice({ tone: "status" | "error", text })` / `showNotice(null)`. `status` только озвучивается, `error` видна строкой `role="alert"` до следующей правки; тем же каналом идут сообщения о картинках.
|
|
61
|
+
- **Кнопки своего Инструмента.** Свой Инструмент (`CustomTool` в `tools`) может нести кнопки в поле `buttons`: подпись (строка или функция от `{ locale }`), иконка, `can`, `isActive`, `isVisible`, `selectionMenu`, `shortcut` и ровно одно из `run` и `popover`. Кит рисует кнопку одной группой перед «Отменить» и «Повторить», вешает сочетание и показывает его в подсказке, `aria-keyshortcuts` и Справочнике сочетаний.
|
|
62
|
+
- **Карточка у текста.** Ещё один вид UI-записи — Карточка у текста (`textCards`): диапазон `rangeAt(state, pos)` (для Метки — готовый `markRange("acme_comment")`) и тело `render({ editor, range, close })`. Кит открывает её кареткой в диапазоне или щелчком, держит рамку (слой, якорь под диапазоном, переворот у края, Escape), не забирает фокус из Документа и переводит фокус в неё по `Alt+F10`.
|
|
63
|
+
- **Пункты вставки и Действия над Блоком.** Ещё два вида UI-записей — Пункты вставки (`insertItems`: «/», «+», «Добавить ›» и «Формат ›» Ручки) и Действия над Блоком (`blockActions`: секция меню Ручки перед «Удалить»), тоже с ровно одним из `run` и `popover`. Поповер Пункта встаёт у каретки, Действия — у Блока.
|
|
64
|
+
- **Вставка и drop.** Свой Инструмент влияет на вставку и drop только через Часть для движка.
|
|
65
|
+
- **Имена и HTML-соглашение.** Имена всего своего (Инструмента, Блоков, Меток, атрибутов) начинаются с Префикса потребителя (`acme_card`). HTML своего Блока — `div` или `span` с `data-block`, Метки — `span` с `data-mark`, атрибуты — `data-acme_*` со значением по правилу (список, шаблон, адрес).
|
|
66
|
+
- **Пресеты.** Это массивы `RICH_TEXT_PRESETS.full` и `.basic`, чтобы дописать к ним своё.
|
|
67
|
+
- **Сервер и Просмотр.** Тот же массив `tools` принимают серверная очистка (`sanitizeRichText(html, DOMPurify, tools)`, `richTextSchema(tools)`) и `RichTextView` (Остров своего Блока — поле `island`).
|
|
68
|
+
- **Вес Атома.** Атом весит в `maxLength` длину своего Текста атома (`renderText`): разделитель — 0, мягкий перенос — 1, картинка — длину подписи.
|
|
69
|
+
|
|
70
|
+
Всё API расширения собрано на странице [API расширения Редактора](../../api/editor-extension.md) вместе с границей semver (раздел «Стабильность») и рецептами.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { RichTextEditor } from "@olddevs/ui/editor";
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Когда использовать
|
|
77
|
+
|
|
78
|
+
- Длинный текст с форматированием: статья, описание, регламент, предложение клиенту. Автор пишет заголовки, списки, цитаты и выделяет слова.
|
|
79
|
+
- Значение хранится и отдаётся как HTML, а показывается потом страницей.
|
|
80
|
+
- Компонент «на будущее» (ADR-0048): экрана-заказчика при выпуске не было, API взят по аналогам — Imperavi Redactor 5, Tiptap Simple Editor, minimal-tiptap, Notion. Ваш экран — первая проверка этого API. Решение о движке — ADR-0050.
|
|
81
|
+
|
|
82
|
+
## Когда не использовать
|
|
83
|
+
|
|
84
|
+
- Простой многострочный текст без разметки — `TextControl as="textarea"`. Редактор с движком весит 295–339 кБ gzip ([таблица размеров](../../getting-started.md#размер-импорта)); комментарий в две строки этого не стоит.
|
|
85
|
+
- Код или конфигурация — это не форматированный текст. Показ кода — `CodeView`; редактора кода в ките нет (ADR-0039).
|
|
86
|
+
- Показ сохранённого Документа без правки. `readOnly` держит движок живым; для страницы, которая только показывает Документ, есть [`RichTextView`](../content/rich-text-view.md) — без движка и годится для серверного компонента.
|
|
87
|
+
|
|
88
|
+
## Установка
|
|
89
|
+
|
|
90
|
+
Движка **нет в манифесте кита** — ни в `peerDependencies`, ни в `dependencies`: кто вход `@olddevs/ui/editor` не импортирует, ничего не ставит, и обновление кита Tiptap ему не притащит. Раньше движок был опциональными peer-зависимостями, но прежний реестр кита отбрасывал признак опциональности, и npm ставил движок каждому потребителю. Компромисс: о несовпадении версий npm не предупредит, диапазон ниже держите сами. Кто импортирует — ставит одной командой:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install @tiptap/core @tiptap/pm @tiptap/react @tiptap/starter-kit @tiptap/extensions \
|
|
94
|
+
@tiptap/extension-list @tiptap/extension-subscript @tiptap/extension-superscript \
|
|
95
|
+
@tiptap/extension-text-style @tiptap/extension-highlight @tiptap/extension-text-align @tiptap/extension-link \
|
|
96
|
+
@tiptap/extension-table @tiptap/extension-blockquote @tiptap/extension-horizontal-rule \
|
|
97
|
+
@tiptap/markdown marked @tiptap/suggestion \
|
|
98
|
+
@tiptap/extension-code-block-lowlight lowlight highlight.js
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Пакеты ставятся все, даже если ваш набор Инструментов без чек-листа или индексов: вход импортирует расширения статически, а выбор `tools` происходит уже при создании поля. Блок кода и таблица приходят отдельным чанком (раздел «Скелет»), но их пакеты сборщик всё равно разрешает при сборке — `@tiptap/extension-table`, `@tiptap/extension-code-block-lowlight`, `lowlight` и `highlight.js` нужны и экрану с `basic`. `@tiptap/extension-blockquote` и `@tiptap/extension-horizontal-rule` npm и yarn ставят вместе с `@tiptap/starter-kit`, но в строгом `node_modules` pnpm их нужно назвать явно — команда выше их уже содержит. Плавающая Панель берёт позиционирование из `@tiptap/react/menus`: нужный ей `@tiptap/extension-bubble-menu` (с `@floating-ui/dom`) — необязательная зависимость самого `@tiptap/react`, и менеджер пакетов ставит его вместе с ним. Если вы ставите пакеты с `--omit=optional`, добавьте его явно.
|
|
102
|
+
|
|
103
|
+
Входов у Редактора три. `@olddevs/ui/editor` — сам Редактор, Просмотр и очистка; ему нужны пакеты движка выше. `@olddevs/ui/editor/schema` — только Схема данными (`RICH_TEXT_SCHEMA`, `richTextSchema(tools)`), очистка `sanitizeRichText(html, DOMPurify)`, закрытый конфиг `richTextSanitizeConfig()` для сервера и хелпер Карточки у текста `markRange`. В графе этого входа нет ни React, ни Tiptap, ничего ставить не нужно (DOMPurify приносит потребитель). `@olddevs/ui/editor/highlight` — подсветка без Редактора: `highlightCodeBlocks(html)` для сохранённого Документа (раздел «Блок кода» ниже) и `highlightCodeNodes(code, language)` — те же токены узлами для [`CodeView`](../content/code-view.md#подсветка-кита); в графе этого входа `lowlight`, `highlight.js` и `react`. Очистка с DOMPurify — на странице [`RichTextView`](../content/rich-text-view.md#санитизация--на-сервере-потребителя).
|
|
104
|
+
|
|
105
|
+
В Next.js добавьте вход `@olddevs/ui/editor` в `optimizePackageImports` отдельной строкой рядом с `@olddevs/ui` ([getting-started.md](../../getting-started.md#пакет-в-серверных-компонентах)): без неё страница, импортирующая из входа один `RichTextView`, отгружает в браузер весь Редактор с движком.
|
|
106
|
+
|
|
107
|
+
Держите версии пакетов Tiptap одинаковыми: они ссылаются друг на друга точной версией. Диапазон — `^3.31.4`.
|
|
108
|
+
|
|
109
|
+
Отдельный `@source` для Tailwind не нужен: классы Редактора лежат в том же `dist`, что и остальной кит, и строка `@source "./node_modules/@olddevs/ui/dist"` из [getting-started.md](../../getting-started.md) их уже покрывает.
|
|
110
|
+
|
|
111
|
+
## Минимальный пример
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { FormField } from "@olddevs/ui";
|
|
115
|
+
import { RichTextEditor } from "@olddevs/ui/editor";
|
|
116
|
+
|
|
117
|
+
<FormField label="Описание" htmlFor="description" hint="Видно на странице обзора">
|
|
118
|
+
<RichTextEditor id="description" value={html} onChange={setHtml} maxLength={5000} />
|
|
119
|
+
</FormField>;
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Панель
|
|
123
|
+
|
|
124
|
+
Фиксированная Панель стоит над полем и **липнет при прокрутке** под шапкой приложения: отступ — CSS-переменная `--topbar-h`, которую публикует `TopBar` кита (ADR-0016, ADR-0019). Без шапки переменной нет, и Панель липнет к краю окна браузера. Липкость — внутри рамки поля: Панель уходит вместе с полем, когда оно прокручено целиком. По умолчанию своего скролла у Редактора нет — он растёт вместе с Документом; предел высоты и ручку высоты включают `maxHeight` и `resizable` (раздел «Высота поля»).
|
|
125
|
+
|
|
126
|
+
- `toolbar="fixed"` (по умолчанию) — Панель над полем, а над непустым выделением ещё и Меню выделения — та же плашка, что в `toolbar="bubble"` (выключается `selectionMenu={false}`; на сенсорном экране его нет); `toolbar="floating"` — Панель над непустым выделением (ниже); `toolbar="bubble"` — вместо Панели Меню выделения, компактная плашка над выделением (ниже); `toolbar="none"` — без Панели, форматирование остаётся за клавиатурой.
|
|
127
|
+
- В `readOnly` Панели нет. В `disabled` Панель видна, но все кнопки выключены и фокуса не берут.
|
|
128
|
+
- **Справочник сочетаний** — последняя кнопка Панели, с иконкой клавиатуры, у правого края; есть только у фиксированной Панели (над выделением и в `readOnly` её нет, при `toolbar="none"` Панели нет вовсе). Подробнее — «Справочник сочетаний».
|
|
129
|
+
- **Состав и порядок — один компактный ряд, порядок задаёт кит, а не `tools`**. Группы через разделитель:
|
|
130
|
+
|
|
131
|
+
| Группа | Кнопки и меню по порядку |
|
|
132
|
+
| ------------ | ---------------------------------------------------------------------------- |
|
|
133
|
+
| Блок целиком | «+» Меню вставки · HTML · «¶» тип Блока · Выравнивание ▾ |
|
|
134
|
+
| Метки | жирный · курсив · маркер · «…» (остальные Метки, блок кода, цитата, очистка) |
|
|
135
|
+
| Списки | Списки ▾ |
|
|
136
|
+
| Вставка | ссылка · картинка · Таблица ▾ · эмодзи |
|
|
137
|
+
| История | отмена · повтор |
|
|
138
|
+
|
|
139
|
+
У правого края — Поиск (Инструмент `search`) и Справочник сочетаний. Кнопка или меню без включённого Инструмента не рисуется, группа без кнопок пропадает вместе со своим разделителем: в `basic` ряд — жирный, курсив, «…» | Списки ▾ | ссылка | отмена, повтор | у правого края поиск и Справочник («¶» из одного «Текста» не рисуется). Тот же состав и порядок у плавающей Панели (`toolbar="floating"`), только без поиска и Справочника; у Меню выделения (`toolbar="bubble"`) свой короткий состав — без выравнивания, со своими «Списки ▾» и «Цвет» (ниже).
|
|
140
|
+
|
|
141
|
+
- Отдельных кнопок выравнивания, списков, цитаты, горизонтальной линии и блока кода на Панели нет: выравнивание и списки — меню, цитата — флажок «…», линия — пункт Меню вставки («+» и `/`), блок кода — пункт «…» (им пользуются нечасто). Когда ширины не хватает, Панель переносится на следующую строку **группами целиком**, а не уезжает за край: «Отменить · Повторить» не расходятся по строкам, а разделитель стоит в конце группы, и перенесённая строка начинается с кнопки. Полный ряд в колонке 640 px — две строки: поиск и Справочник сочетаний уходят на вторую, к правому краю.
|
|
142
|
+
|
|
143
|
+
### Меню «¶» — тип строки
|
|
144
|
+
|
|
145
|
+
Кнопка «¶» открывает меню типа строки под кареткой: **Текст** (`Ctrl/⌘+Alt+0`), **Заголовок 1** (только с `heading1`, `Ctrl/⌘+Alt+1`), **Заголовок 2–4** (`Ctrl/⌘+Alt+2…4`). Сочетание подписано у каждого пункта: `⌘`, `⌥`, `⇧` через плюс на Mac и iOS (`⌘+⌥+2`), `Ctrl`, `Alt`, `Shift` у остальных.
|
|
146
|
+
|
|
147
|
+
- **Списков и цитаты в «¶» нет** (решение владельца): у списков — меню «Списки ▾» (на Панели и в Меню выделения), у цитаты — флажок «Цитата» в «…». В «Формат ›» Ручки Блока и в Меню вставки («/», «Превратить в») они остаются: там Блок превращают во что угодно, а не выбирают тип строки.
|
|
148
|
+
- **Пункты — из включённых Инструментов** (`heading`, `heading1`) и пункты «¶» своих Инструментов; «Текст» есть всегда, пока есть хоть один другой пункт. Без заголовков кнопки «¶» нет — так в `basic`.
|
|
149
|
+
- **Решает строка под кареткой**: абзац в пункте списка или в цитате — «Текст», заголовок в цитате — его уровень. Отмечен этот тип, он же — в доступном имени кнопки («Тип блока: Заголовок 2») и **на иконке**: уровень заголовка (H1–H4), иконка пункта своего Инструмента, у текста — «¶» (прежде иконка была всегда «¶», ADR-0054). В блоке кода кнопка называет «Блок кода», иконка — «¶», а отмечен ни один пункт: блок кода — пункт «…».
|
|
150
|
+
- **Выбор меняет тип строки на месте** с сохранением текста: заголовок в цитате остаётся в цитате. Где Схема строке этот тип не даёт (заголовок первым в пункте списка), действует «Превратить в» Меню вставки: списки и цитаты вокруг строки снимаются, тип ставится. Правка одна, `Ctrl/⌘+Z` отменяет её целиком. Выбор текущего типа ничего не меняет. Фокус возвращается в Документ.
|
|
151
|
+
|
|
152
|
+
### Меню «…» — остальное форматирование
|
|
153
|
+
|
|
154
|
+
Кнопка «…» (подсказка и имя — «Ещё форматирование», `editor.moreFormatting`) стоит сразу за кнопкой «Маркер» (в Меню выделения — за «Цветом») и открывает меню остального форматирования: **Строчный код** (`Ctrl/⌘+E`), **Подчёркивание** (`Ctrl/⌘+U`), **Зачёркнутый** (`Ctrl/⌘+Shift+S`), **Верхний индекс** (`Ctrl/⌘+.`), **Нижний индекс** (`Ctrl/⌘+,`), **Блок кода** (`Ctrl/⌘+Alt+C`), **Цитата** (`Ctrl/⌘+Shift+B`), **Очистить стили** (`Ctrl/⌘+\`). Цветов в «…» нет — у них свои кнопки (ниже). Сочетания подписаны у пунктов — из той же таблицы, что подсказки Панели и Справочник. Сочетаний из привычных редакторов (`⌘H`, `⌘L`, `⌘⇧M`) нет: их перехватывают macOS и браузер, и подпись обещала бы то, что не срабатывает.
|
|
155
|
+
|
|
156
|
+
- **Пункты — из включённых Инструментов** (`code`, `underline`, `strike`, `superscript`, `subscript`, `codeBlock`, `blockquote`, `clearFormatting`). Без единого из них кнопки «…» нет; с одним — меню из одного пункта. В `basic` в «…» строчный код, зачёркнутый и цитата. Метка, у которой есть кнопка в ряду Панели или Меню выделения, в «…» этого меню не повторяется: в Меню выделения нет подчёркнутого — он там в ряду.
|
|
157
|
+
- **Метки — пункты с отметкой** (`menuitemcheckbox`, `aria-checked`): отмечены Метки выделения или каретки. Выбор ставит или снимает Метку одной правкой (`Ctrl/⌘+Z` отменяет её целиком), закрывает меню и возвращает фокус в Документ. Пункт, которому здесь нечего делать (Метка в блоке кода), недоступен.
|
|
158
|
+
- **Блок кода — отдельный раздел сразу за Метками** (прежде — кнопка Панели между «HTML» и «¶»). Это Блок, а не Метка, поэтому за разделителем, но такой же пункт-флажок с сочетанием: все переключатели «…» идут подряд, «Очистить стили» — после них.
|
|
159
|
+
- Пункт блока кода отмечен, когда каретка в блоке кода; выбор превращает Блоки выделения в блок кода или возвращает его в текст — одной правкой, как `Ctrl/⌘+Alt+C`. Недоступен там, где движок блок кода не ставит.
|
|
160
|
+
- Тот же пункт есть в «…» Меню выделения: кнопки блока кода там не было, а превратить выделенные строки в код удобно именно оттуда.
|
|
161
|
+
- **Цитата — флажок рядом с Блоком кода** (прежде — пункт «¶»): отмечена, когда каретка в цитате; выбор оборачивает Блоки выделения в цитату или снимает её — одной правкой, как `Ctrl/⌘+Shift+B`. Недоступна там, где движок цитату не ставит. Есть, только если включён `blockquote`.
|
|
162
|
+
- **На узком экране подменю — под пунктом** (меньше 640 px): сбоку на телефоне места нет, и подменю Ручки Блока «⋮» и меню «Таблица» (вместе с сеткой размера) встают под своим пунктом поверх родительского меню. На широком экране — сбоку, как раньше.
|
|
163
|
+
- Отдельных кнопок строчного кода, подчёркивания, зачёркнутого, индексов, блока кода и очистки на Панели нет: «…» их заменяет. Меню собрано в общем модуле меню форматирования (`MoreFormatMenu` в `FormatMenus.tsx`) — то же «…» стоит и в Меню выделения.
|
|
164
|
+
- Ряд частых Меток общий для Панели и Меню выделения (`rowMarks.ts`): на Панели — жирный и курсив, в Меню выделения — ещё и подчёркнутый.
|
|
165
|
+
|
|
166
|
+
### Кнопки «Маркер» и «Цвет»
|
|
167
|
+
|
|
168
|
+
Цвет выбирают глазами, по кружкам, одним щелчком — как в Notion-like шаблоне Tiptap. Прежде цвет текста и маркер были подменю «…», а кнопка «Маркер» одним щелчком ставила жёлтый.
|
|
169
|
+
|
|
170
|
+
**Где какая кнопка** (решение владельца — без дублей): на Панели (фиксированной и плавающей) — только «Маркер», в Меню выделения — только «Цвет», в котором есть и сетка маркера. Без Инструмента `color` в Меню выделения стоит «Маркер» — иначе маркер там не поставить. **Компромисс решения:** цвет текста выбирается только из Меню выделения. Кнопками его не поставить там, где Меню нет: при `selectionMenu={false}`, при `toolbar="floating"` и на сенсорном экране (там вместо Меню выделения фиксированная Панель). Остаются Хэндл и команды движка.
|
|
171
|
+
|
|
172
|
+
- **«Маркер»** (Инструмент `highlight`, подсказка — `editor.highlightToggle`): щелчок открывает ряд из девяти маркеров по оттенку — серый, коричневый, оранжевый, жёлтый, зелёный, синий, фиолетовый, розовый, красный — и «Убрать маркер» (перечёркнутый круг с подсказкой, `editor.highlightRemove`). Имя кнопки называет текущий маркер («Маркер: Розовый», «Маркер: Свой #ffe066», «Маркер: Без маркера»), и его слышно, не открывая поповер; пока на выделении маркер, кнопка выглядит нажатой. Щелчок по самой кнопке жёлтый больше не ставит: жёлтый ставится щелчком по кружку.
|
|
173
|
+
- **«Цвет»** (Инструмент `color`, `editor.color`): буква «A» текущим цветом текста и стрелка. Поповер — две сетки: **«Цвет текста»** (`editor.textColorSection`: шесть тонов, кружок с буквой «A» этим цветом, и последней ячейкой — «Убрать цвет», `editor.colorRemove`) и **«Цвет маркера»** (`editor.highlightSection`: те же девять маркеров и последней ячейкой — «Убрать маркер»). Сетка маркера есть, только если включён и `highlight`. Снятие — перечёркнутый круг с подсказкой в конце сетки, как в «Маркере», а не белый кружок первым: тот выглядел ещё одним цветом и стоял отмеченным, когда цвета нет. Без цвета в сетке ничего не отмечено, фокус при открытии — на первом кружке. У заголовка каждой сетки — **«Свой цвет…»**: открывает поповер `ColorPicker` (раздел «Свой цвет» ниже).
|
|
174
|
+
- **Роли — как у меню из WAI-ARIA Authoring Practices (APG), а не радиогруппа:** поповер — `menu`, сетки — `group` с заголовком, кружки и «Свой цвет…» — `menuitemradio` с `aria-checked` (текущий отмечен кольцом вокруг кружка), «Убрать цвет» и «Убрать маркер» — `menuitem`. В радиогруппе стрелка сразу выбирала бы и закрывала поповер, здесь стрелки только ходят: `←` `→` — соседний кружок, `↑` `↓` — тот же столбец в ряду выше или ниже (по пять в ряду), `Home` и `End` — первый и последний; Enter, пробел и щелчок применяют.
|
|
175
|
+
- **Фокус.** При открытии — на отмеченном цвете (или на первом). Выбор применяет цвет одной правкой (`Ctrl/⌘+Z` отменяет её целиком), закрывает поповер и возвращает фокус в Документ на прежнее выделение. Escape закрывает только поповер и возвращает фокус на кнопку — и в `Dialog`, и в `Drawer` окно или ящик остаётся открытым (ADR-0041).
|
|
176
|
+
- **В Меню выделения** поповер держит сама кнопка: пока он открыт, у неё `aria-expanded="true"`, и Меню выделения не прячется. В HTML-режиме и на строчном коде кнопки недоступны (`aria-disabled`) и поповер не открывают. В `basic` обеих кнопок нет: ни `color`, ни `highlight` в него не входят.
|
|
177
|
+
|
|
178
|
+
### Меню «Выравнивание» и «Списки»
|
|
179
|
+
|
|
180
|
+
Оба — меню того же общего модуля, что «¶» и «…» (`AlignMenu` и `ListMenu` в `FormatMenus.tsx`): пункты-радио (`menuitemradio`) с отметкой текущего значения и подписями сочетаний из общей таблицы. Иконка кнопки — иконка текущего значения, имя — значение словами; выбор возвращает фокус в Документ и отменяется одним `Ctrl/⌘+Z`.
|
|
181
|
+
|
|
182
|
+
- **Выравнивание ▾** (Инструмент `align`): **По левому краю** (`Ctrl/⌘+Shift+L`), **По центру** (`Ctrl/⌘+Shift+E`), **По правому краю** (`Ctrl/⌘+Shift+R`). Имя кнопки — «Выравнивание: по центру»; у Блока без `data-align` отмечено «По левому краю», у выделения со смешанным выравниванием — тоже. Выбор отмеченного пункта ничего не меняет и пустой правки в историю не кладёт; вернуть умолчание можно выбором «По левому краю». **По ширине нет** — решение ниже, в «Цвет, маркер и выравнивание» (WCAG 1.4.8).
|
|
183
|
+
- **Списки ▾** (любой из `bulletList`, `orderedList`, `taskList`): **Маркированный список** (`Ctrl/⌘+Shift+8`), **Нумерованный список** (`Ctrl/⌘+Shift+7`), **Чек-лист** (`Ctrl/⌘+Shift+9`) — те же виды, что в «Формат ›» Ручки Блока, только из включённых Инструментов. Отмечен вид ближайшего списка вокруг каретки (и в цитате внутри пункта), имя кнопки — «Список: нумерованный», вне списка — «Списки» с общей иконкой списка. Выбор другого вида идёт тем же путём, что «Превратить в» Меню вставки: Блок выходит из списка и цитаты и оборачивается заново одной правкой. **Повторный выбор отмеченного снимает список**, как прежняя кнопка-переключатель. Меню с одним включённым видом остаётся меню.
|
|
184
|
+
- Без Инструмента `align` нет меню выравнивания, без единого вида списка — меню «Списки». В `basic` — «Списки» с двумя пунктами, выравнивания нет.
|
|
185
|
+
|
|
186
|
+
### Плавающая Панель
|
|
187
|
+
|
|
188
|
+
`toolbar="floating"` показывает ту же Панель — те же кнопки, тот же порядок — над выделенным текстом. Пустое выделение (просто каретка) Панель прячет, и тогда её нет ни на экране, ни в обходе Tab, ни у скринридера. Показ идёт с короткой задержкой после того, как выделение перестало меняться: пока мышь тянет выделение, Панель не мигает. Позицию и переворот у края экрана считает `BubbleMenu` Tiptap (floating-ui).
|
|
189
|
+
|
|
190
|
+
- **Сенсорный экран — фиксированная Панель.** Если основной указатель — палец (`pointer: coarse`: телефон, планшет), Редактор рисует фиксированную Панель вместо плавающей: над выделением на iOS и Android стоит системное меню «Копировать / Вставить», и плавающая Панель спорила бы с ним за одно место. Медиазапрос слушается на лету — планшет с подключённой мышью получит плавающую.
|
|
191
|
+
- **Панель лежит в DOM Редактора**, а не в портале: она едет вместе с текстом в любом контейнере прокрутки (окно, ящик, карточка). Поэтому предок с `overflow: hidden` срежет её у своего края — не ставьте Редактор в такой контейнер вплотную к верху, или берите `toolbar="fixed"`.
|
|
192
|
+
- Слой — ступень `OVERLAY_LAYER.popover`, как у поповеров. В стеке Escape Панель не участвует: закрывать в ней нечего, и Escape остаётся окну или ящику вокруг Редактора.
|
|
193
|
+
|
|
194
|
+
### Меню выделения: `selectionMenu` и `toolbar="bubble"`
|
|
195
|
+
|
|
196
|
+
Меню выделения по умолчанию появляется и поверх фиксированной Панели (`toolbar="fixed"`, проп `selectionMenu`, по умолчанию `true`): Панель сверху — для всего, Меню выделения — чтобы не уводить мышь от текста. `Alt+F10` в этом случае переводит фокус в Панель. `toolbar="bubble"` ставит **Меню выделения** вместо Панели — компактную плашку над выделенным текстом, как в Notion и Medium. Она на **инверсной поверхности**: тёмная в светлой теме, светлая в `dark` и `dim`, — и поэтому не сливается ни с текстом, ни с полем под ним.
|
|
197
|
+
|
|
198
|
+
- **Состав задан китом и не настраивается:** «¶» (тип строки, то же меню, что на Панели) · Списки ▾ (то же меню, что на Панели) · жирный · курсив · подчёркнутый · Цвет ▾ (обе сетки — текста и маркера; без `color` — «Маркер») · «…» (остальные Метки и блок кода — то же меню, что на Панели, но без подчёркнутого: он здесь в ряду) · ссылка (тот же поповер, что у `Ctrl/⌘+K`) · эмодзи (та же сетка). Кнопка выключенного Инструмента не рисуется: в `basic` нет эмодзи, при `tools={["italic", "link"]}` остаются две кнопки. Отмены, повтора, Справочника сочетаний и редких Меток в Меню выделения нет — оно относится к выделенному тексту.
|
|
199
|
+
- **Жирный, курсив и подчёркнутый — переключатели** с `aria-pressed`; подсказка называет сочетание из той же таблицы, что у Панели, кнопка объявляет его `aria-keyshortcuts`.
|
|
200
|
+
- **Показ и позиция — как у плавающей Панели:** Меню выделения появляется, когда выделение устоялось, и прячется на пустом выделении, на выделенной картинке (у неё своя панель) и когда фокус ушёл из Редактора. У края окна браузера или контейнера прокрутки оно переворачивается под выделение и сдвигается внутрь. В `readOnly` Меню выделения нет.
|
|
201
|
+
- **Сенсорный экран — фиксированная Панель**, по той же причине, что у `floating`: системное меню выделения iOS и Android стоит на том же месте.
|
|
202
|
+
- **Меню и поповеры, открытые из Меню выделения, — на обычной поверхности поповера**, а не на инверсной: «¶», «…», поле ссылки и сетка эмодзи выглядят так же, как с Панели. Escape в открытом меню закрывает только меню и возвращает фокус на его кнопку в Меню выделения; внутри `Dialog` и `Drawer` окно или ящик остаётся открытым.
|
|
203
|
+
- **Меню выделения лежит в DOM Редактора**, как плавающая Панель: едет вместе с текстом в контейнере прокрутки, а предок с `overflow: hidden` срежет его у своего края. Слой — `OVERLAY_LAYER.popover`.
|
|
204
|
+
- Имя Меню выделения — `editor.selectionToolbar` («Форматирование выделения» / «Selection formatting»): скринридер отличает его от Панели.
|
|
205
|
+
- **Цвета — токены инверсной поверхности** `--surface-inverse`, `--surface-inverse-hover`, `--surface-inverse-pressed`, `--text-on-inverse` и контур фокуса `--accent-on-inverse` ([palette.md](../../palette.md#инверсная-поверхность)). На Листе `surface="paper"` Меню выделения остаётся в теме приложения, как Панель.
|
|
206
|
+
|
|
207
|
+
### Стекло всплывающих поверхностей
|
|
208
|
+
|
|
209
|
+
Всё, что Редактор показывает поверх Документа, лежит на **Стекле** — полупрозрачной подложке своей поверхности с размытием фона: плавающая Панель и панель картинки, Меню выделения, списки меню («¶», «…», выравнивание, списки, таблица, Меню вставки, Ручка Блока, язык блока кода), поповеры (ссылка, картинка, свой цвет, эмодзи, Справочник сочетаний, поповеры кнопок, Пунктов вставки и Действий над Блоком своего Инструмента) и Карточка у текста. Подложка — 82 % токена поверхности в `color-mix`, размытие — 14 px, как у тостов (ADR-0010); подсказки остаются со своими 85 % и 10 px.
|
|
210
|
+
|
|
211
|
+
- **Цвет — токен своей поверхности:** Панель и поповеры — `--bg-1`, списки меню — `--surface-elevated`, Меню выделения — `--surface-inverse`. Тема меняет токен, Стекло меняется вместе с ним.
|
|
212
|
+
- **Сплошная подложка при `prefers-reduced-transparency: reduce` и `prefers-contrast: more`:** под полупрозрачной подложкой контраст текста зависит от того, что лежит под меню, а пользователь, попросивший систему убрать прозрачность или поднять контраст, получает прежнюю сплошную поверхность без размытия.
|
|
213
|
+
- **Сплошная подложка на iOS:** в Safari на iOS 27 стеклянная подложка меню не рисовалась вовсе, и пункты ложились на текст Документа. Поэтому на iOS (условие `@supports (-webkit-touch-callout: none)`, которое понимает только WebKit на iOS) Стекло заменяется сплошной поверхностью без размытия; десктопный Safari, Chrome и Firefox Стекло сохраняют.
|
|
214
|
+
- **Только Редактор.** `Menu` и `Popover` кита вне Редактора остаются сплошными: Стекло Редактор передаёт им классом, публичный API не менялся.
|
|
215
|
+
|
|
216
|
+
### Клавиатура: `Alt+F10`
|
|
217
|
+
|
|
218
|
+
`Alt+F10` (на Mac — `⌥F10`) из Документа переводит фокус на первую кнопку Панели — фиксированной, плавающей или Меню выделения; плавающую Панель и Меню выделения клавиша показывает сразу, не дожидаясь задержки. Сочетание то же, что у CKEditor и TinyMCE: у APG своего нет, а одно сочетание на все редакторы запомнить проще. Tab для этого не годится — в Документе он сдвигает пункт списка.
|
|
219
|
+
|
|
220
|
+
- Внутри Панели — стрелки, Home и End, как всегда. Сработавшая кнопка возвращает фокус в Документ на то же выделение; если ничего не нажато, фокус обратно переводит `Shift+Tab`: плавающая Панель стоит в DOM сразу за Документом.
|
|
221
|
+
- Escape Панель не перехватывает: в окне или ящике он закроет их (см. «Доступность и форма»).
|
|
222
|
+
- На пустом выделении у плавающей Панели и Меню выделения и при `toolbar="none"` клавиша ничего не делает и уходит дальше.
|
|
223
|
+
- Панель объявляет сочетание `aria-keyshortcuts="Alt+F10"` — скринридер называет его на самой Панели.
|
|
224
|
+
- Из поля HTML-режима `Alt+F10` переводит фокус на кнопку «HTML»: остальные кнопки Панели там `aria-disabled`, и она — первая, которая сработает (Enter на ней возвращает визуальный режим, стрелки идут по Панели дальше). Раздел «HTML-режим».
|
|
225
|
+
- **Открытая Карточка у текста** приоритетнее Панели: `Alt+F10` переводит фокус в неё, Escape возвращает каретку; пока Карточка спрятана или закрыта, клавиша переводит фокус в Панель (раздел «Карточка у текста»).
|
|
226
|
+
- С кареткой в блоке кода `Alt+F10` переводит фокус сначала на кнопку языка этого Блока, затем на «Копировать», затем — в Панель (раздел «Блок кода»). Так же на выделенной картинке клавиша переводит фокус в её панель.
|
|
227
|
+
|
|
228
|
+
## Справочник сочетаний
|
|
229
|
+
|
|
230
|
+
Кнопка с иконкой клавиатуры («Сочетания клавиш» / «Keyboard shortcuts») — последняя на фиксированной Панели, у правого края. Она открывает `Popover` кита со списком «команда — сочетание» **только для чтения**: строки не нажимаются, это не палитра команд.
|
|
231
|
+
|
|
232
|
+
- **На телефоне кнопки нет.** На сенсорном экране без наведения (`@media (hover: none) and (pointer: coarse)` — телефон, планшет без трекпада) сочетаний нет, поэтому кнопка Справочника скрыта, а пункты меню («¶», «…», таблица, Меню вставки, Ручка Блока) не показывают подписи сочетаний. Планшет с трекпадом и ноутбук с сенсорным экраном подсказки сохраняют.
|
|
233
|
+
- **Разметка — таблица** с заголовками столбцов «Команда» и «Сочетание»; сочетание — `Kbd` кита (`<kbd>`). Скринридер читает строку как пару «команда — сочетание».
|
|
234
|
+
- **Свои кнопки.** Сочетание кнопки своего Инструмента (поле `shortcut`) попадает в Справочник отдельной группой без заголовка после групп кита, в порядке `tools`; подпись берётся из локали `UiProvider`. Клавиши Части для движка своего Инструмента сюда не попадают — раздел «Кнопка своего Инструмента».
|
|
235
|
+
- **Порядок групп:** выделение (первое `⌘A` — текст Блока, второе — весь Документ), отмена и повтор, команды Блоков (дублировать, вверх, вниз, меню Ручки), очистить стили, Метки (жирный, курсив, зачёркнутый, подчёркнутый, строчный код, индексы), тип Блока (текст, заголовки, цитата, блок кода), списки и сдвиг пункта (`⌘]` и `⌘`), выравнивание, ссылка, поиск (`Ctrl/⌘+F`, `Ctrl/⌘+Alt+F`), переход в Панель (`Alt+F10`).
|
|
236
|
+
- **Состав по `tools`.** Строка есть только у команды включённого Инструмента: в `basic` нет заголовков, подчёркивания, индексов, выравнивания, чек-листа и блока кода, а сдвиг пункта есть при любом из трёх списков. Заголовок 1 — только при `heading` и `heading1` вместе. Всегда есть команды без своего Инструмента: выделение, дублировать и переставить Блок, меню Ручки Блока, `Alt+F10`.
|
|
237
|
+
- **Свои Инструменты.** Сочетания их Пунктов вставки и Действий над Блоком (поле `shortcut`) стоят группой без заголовка после групп кита, по Инструменту на группу, в порядке `tools`; подпись строки — подпись UI-записи в текущей локали.
|
|
238
|
+
- **Ширина — по содержимому:** поповер равен самой длинной строке, без пустого поля между командой и сочетанием; край экрана ограничивает его, длинный список прокручивается.
|
|
239
|
+
- **Подписи.** Клавиши через плюс на всех платформах: `⌘+⇧+Z` на Apple и `Ctrl+Shift+Z` у остальных. Слитная запись macOS (`⌘⌥⇧↑`) в длинном списке читается как одно слово. Признак Apple — тот же, что у ProseMirror, то есть тот, по которому движок решает, какая клавиша `mod`. Знаки `⌘ ⌥ ⇧ ⌃` `Kbd` рисует пропорциональным шрифтом: в моноширинном стеке `⇧` нет, и запасной глиф выходит тонким.
|
|
240
|
+
- **Одна таблица команд — источник правды.** Подсказки и `aria-keyshortcuts` кнопок Панели, подписи в Меню вставки и Справочник читают сочетания из одной таблицы (`src/editor/core/shortcuts.ts`: id команды, Инструмент, клавиши, подпись из каталога). Привязки вешают расширения, а таблица их подписывает; чтобы подпись не обещала того, чего нет, `tests/rich-text-editor-shortcuts.test.tsx` нажимает каждое сочетание таблицы в живом Редакторе и проверяет результат.
|
|
241
|
+
- **Закрытие.** Escape и щелчок снаружи закрывают поповер и возвращают фокус на кнопку (если щелчок сам поставил фокус на поле или кнопку, его не отбирают). Внутри `Dialog` и `Drawer` Escape закрывает только Справочник, а не окно или ящик ([ADR-0041 (ADR-0041)).
|
|
242
|
+
- **Не показывается:** в `readOnly`, при `toolbar="none"`, над выделением в `toolbar="floating"` и в Меню выделения (`toolbar="bubble"`); на сенсорном экране они заменены фиксированной Панелью, но кнопку Справочника там прячет правило из начала раздела: на телефоне и планшете без трекпада её нет.
|
|
243
|
+
|
|
244
|
+
Строки каталога: `editor.shortcuts` (имя кнопки и поповера), `shortcutsCommand` и `shortcutsKeys` (заголовки столбцов), а также подписи команд без своей кнопки — `selectBlock`, `selectDocument`, `duplicateBlock`, `moveBlockUp`, `moveBlockDown`, `blockActions`, `indentListItem`, `outdentListItem`, `focusToolbar`.
|
|
245
|
+
|
|
246
|
+
## Ручка Блока «⋮»
|
|
247
|
+
|
|
248
|
+
У Блока, **в котором стоит каретка**, слева от его первой строки появляется кнопка «⋮» — Ручка Блока («Действия с блоком» / «Block actions»). Наведение мыши Ручку не показывает (решение владельца): она не мелькает у каждого абзаца под указателем. Видна, пока фокус в Документе, на самой Ручке или в её меню; в `readOnly` её нет, у выключенного поля — тоже. Под Ручку у Документа в правке есть левое поле (у Листа `surface="paper"` — шире на столько же), и текст она не перекрывает.
|
|
249
|
+
|
|
250
|
+
Щелчок или `Ctrl/⌘+/` из Документа открывают меню — `Menu` кита:
|
|
251
|
+
|
|
252
|
+
| Пункт | Действие |
|
|
253
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
254
|
+
| **Добавить ›** | Подменю с Пунктами вставки из включённых Инструментов (абзац, заголовки, списки, цитата, блок кода, разделитель, таблица, картинка). Выбор ставит **новый Блок под текущим** и каретку в него; без `insertMenu` пункта нет |
|
|
255
|
+
| **Формат ›** | Подменю с видами Блока (Текст, Заголовок 1–4, Цитата, три вида списков): превращает Блок, текущий тип отмечен. Таблицу, картинку и линию превратить нельзя — пункт виден, но недоступен (`aria-disabled`) |
|
|
256
|
+
| **Вверх**, **Вниз** | Переставить Блок с соседом; у первого и последнего Блока соответствующий пункт недоступен |
|
|
257
|
+
| **Дублировать** | Копия под Блоком, каретка — в копию |
|
|
258
|
+
| **Удалить** | Красный пункт (тон `danger` пункта `Menu`). Удаляет Блок, каретка — в Блок выше (у первого — ниже); единственный Блок уступает место пустому абзацу |
|
|
259
|
+
|
|
260
|
+
- **Блок — целиком.** Ручка стоит у Блока Документа: абзац, заголовок, список целиком, чек-лист, цитата, блок кода, таблица, картинка, линия. Каретка во втором пункте списка — Ручка у первой строки списка, и действия меню берут весь список; «Формат › Заголовок 2» превращает в заголовки все его пункты. Выделение через несколько Блоков Ручки не имеет.
|
|
261
|
+
- **Подписи сочетаний — только когда сочетание делает то же.** У абзаца или заголовка верхнего уровня пункты подписаны: `⌥+⇧+↑/↓`, `⌘+⇧+D`, сочетания «¶» в «Формат ›». В пункте списка `⌥⇧↑` двигает пункт, а не список, поэтому подписей нет; в таблице `⌥⇧↑/↓` отданы строкам — у «Вверх» и «Вниз» подписи нет, а `⌘⇧D` копирует таблицу целиком — у «Дублировать» есть. У «Добавить ›» подписей нет: сочетания меняют текущий Блок, а пункт ставит новый.
|
|
262
|
+
- **Одна правка на отмену**, фокус после действия — в Документе. Escape закрывает подменю, затем меню, и возвращает фокус в Документ; внутри `Dialog` и `Drawer` окно или ящик при этом не закрывается (ADR-0041).
|
|
263
|
+
- **Клавиатура.** Ручка — вне обхода Tab (в Документе Tab сдвигает пункт списка); фокус на неё переводит `Ctrl/⌘+/` — как «действия с Блоком» в Notion, у браузеров и систем это сочетание в поле свободно. Строка есть в Справочнике сочетаний; на Ручке — `aria-keyshortcuts`.
|
|
264
|
+
- **Действия Инструментов.** Свои Действия над Блоком (`blockActions` описания Инструмента, как «Снять обёртку» у Обёртки) стоят отдельной секцией перед «Удалить»: только у Блоков, чьи имена названы в `blocks`, и по предикату видимости; `run` получает позицию Блока. Свои Пункты вставки — в «Добавить ›» и «Формат ›» (раздел «Свой Инструмент»).
|
|
265
|
+
- **Перетаскивания за Ручку нет** (решение владельца).
|
|
266
|
+
- **Раскладка** (раздел «Раскладка»). Колонка — контейнер Блоков, как Документ: у Блока в колонке своя Ручка у левого края колонки, и его действия не выходят из колонки. В «Добавить ›» Раскладка — подменю шести вариантов; у Блока в колонке есть пункт «Раскладка ›» (вариант её Раскладки), а у Раскладки, выделенной целиком, «Формат ›» — эти шесть вариантов с отметкой текущего.
|
|
267
|
+
- **Обёртка** (раздел «Обёртка»). Обёртка — тоже контейнер Блоков: у Блока в ней своя Ручка у левого края Обёртки, и его действия из неё не выходят. Пункты **«Тон ›»** и **«Снять обёртку»** есть у Блока прямо в Обёртке и у самой Обёртки, выделенной целиком (Backspace на Блоке под ней); у неё же «Вверх», «Вниз», «Дублировать» и «Удалить» берут Обёртку вместе с содержимым, а «Формат ›» недоступен — типа Блока у неё нет.
|
|
268
|
+
- **Место.** Ручка лежит в DOM Редактора и едет вместе с текстом в любом контейнере прокрутки, в `Dialog` и `Drawer`; липкая Панель и поповеры её перекрывают. Позиция пересчитывается на каждой правке и при смене размера.
|
|
269
|
+
|
|
270
|
+
Строки каталога: `editor.blockActions` (имя Ручки, меню и строки Справочника), `blockAdd`, `blockFormat`, `blockMoveUp`, `blockMoveDown`, `blockDuplicate`, `blockDelete`. Иконки — `MoreVerticalIcon`, `AddIcon`, `ParagraphIcon`, `ArrowUpIcon`, `ArrowDownIcon`, `DuplicateIcon`, `DeleteIcon` из `@olddevs/ui/icons`.
|
|
271
|
+
|
|
272
|
+
## Клавиатура: команды над Блоками
|
|
273
|
+
|
|
274
|
+
Команды над Блоками — как в Notion и Google Docs. Своей разметки у них нет, поэтому они есть при любом наборе `tools` (кроме очистки стилей — она идёт с Инструментом `clearFormatting`). Каждая команда — **одна правка**: `Ctrl/⌘+Z` отменяет её целиком. В `readOnly` и `disabled` правящие команды Документ не меняют и отдают сочетание браузеру; `⌘A` работает и там — выделение Документ не меняет.
|
|
275
|
+
|
|
276
|
+
| Сочетание (Mac / остальные) | Действие |
|
|
277
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
278
|
+
| `⌘⇧D` / `Ctrl+Shift+D` | Дублировать Блок: копия встаёт сразу под ним, каретка — в копию на то же место |
|
|
279
|
+
| `⌥⇧↑` / `Alt+Shift+↑` | Переставить Блок вверх |
|
|
280
|
+
| `⌥⇧↓` / `Alt+Shift+↓` | Переставить Блок вниз |
|
|
281
|
+
| `⌘/` / `Ctrl+/` | Открыть меню Ручки Блока (раздел «Ручка Блока «⋮»») |
|
|
282
|
+
| `⌘A` / `Ctrl+A` | Первое нажатие — текст текущего Блока, второе — весь Документ. Если текст Блока уже выделен или Блок пуст, первое нажатие сразу выделяет Документ |
|
|
283
|
+
| `⌘]` и `⌘[` / `Ctrl+]` и `[` | Сдвинуть пункт списка или чек-листа вправо и влево — то же, что `Tab` и `Shift+Tab` |
|
|
284
|
+
| `⌘\` / `Ctrl+\` | Очистить стили выделения — то же, что пункт «Очистить стили» меню «…» (`clearFormatting`) |
|
|
285
|
+
|
|
286
|
+
- **Что такое Блок.** Верхнеуровневый Блок Документа — абзац, заголовок, цитата, блок кода, картинка, таблица целиком, разделитель, список целиком. В списке Блок — пункт: копия встаёт в тот же список, а перестановка идёт среди соседних пунктов и из списка не выводит. Выделение через несколько Блоков дублируется и переставляется целиком.
|
|
287
|
+
- **На краю** Документа или списка перестановка ничего не делает: соседа нет. Сочетание при этом поглощается — иначе на Mac оно растягивало бы выделение.
|
|
288
|
+
- **В таблице** `⌥⇧↑/↓` не действует: выделение внутри одной таблицы не переставляет ни её, ни строки, а `Alt+↓` по-прежнему добавляет строку (раздел «Таблицы»). Таблица переставляется, когда выделение начинается или кончается вне её. Дублирование из ячейки копирует таблицу целиком.
|
|
289
|
+
- **`⌘]` и `⌘` вне списка** ничего не делают и отдают сочетание браузеру (в Chrome на Mac это «Вперёд» и «Назад»): отступа абзаца в Схеме нет. В пункте списка сочетание поглощается, даже если сдвигать некуда.
|
|
290
|
+
- **Почему не сочетания привычных редакторов.** `⌘⇧↑/↓` на Mac — «выделить до начала или конца документа», `⌘⇧A` в Chrome — поиск по вкладкам: забирать их у системы нельзя. Перестановка — `⌥⇧↑/↓`, как в VS Code.
|
|
291
|
+
- Пункт «Очистить стили» меню «…» подписан `⌘\` (`Ctrl+\` у остальных).
|
|
292
|
+
|
|
293
|
+
## Ссылки
|
|
294
|
+
|
|
295
|
+
Инструмент `link`: кнопка «Ссылка» в Панели и `Ctrl/⌘+K` открывают поповер с полем адреса у выделенного текста.
|
|
296
|
+
|
|
297
|
+
- **Enter** (или «Применить») делает выделение ссылкой и возвращает фокус в Документ. Без выделения адрес вставляется текстом ссылки. Каретка в существующей ссылке — поле показывает её адрес, Enter меняет его на весь текст ссылки, а кнопка **«Убрать ссылку»** снимает ссылку, оставляя текст. Пустое поле и Enter — то же снятие.
|
|
298
|
+
- **Escape** отменяет и возвращает фокус в Документ на прежнее выделение. Внутри `Dialog` и `Drawer` Escape закрывает только поповер: `Popover`, окна и ящики кита — один стек слоёв Radix ([ADR-0041 (ADR-0041)). Щелчок мимо закрывает поповер, а фокус остаётся там, куда щёлкнули.
|
|
299
|
+
- Адрес пишется как в строке браузера: `example.com` становится `https://example.com`, `team@example.com` — `mailto:team@example.com`.
|
|
300
|
+
- **Разрешены только абсолютные адреса с протоколом `http`, `https` или `mailto`.** Поле не примет `javascript:`, `data:`, `ftp:` и относительный адрес (`/docs`, `#part`) — под полем появится ошибка, ссылка не ставится. Относительных нет намеренно: Документ уезжает со страницы, где его написали: в `RichTextView` на другом маршруте, в письмо, в Markdown. Относительный адрес там значит другое или ничего, а якорям в Документе не на что указывать. Ослабить правило позже можно без ломки сохранённого, ужесточить — уже нет.
|
|
301
|
+
- **Автоссылка при наборе:** адрес в тексте (`example.com`, `https://…`, адрес почты) становится ссылкой, когда слово закончено пробелом. Вставка адреса поверх выделения делает выделение ссылкой. Обе дороги проходят ту же проверку протокола.
|
|
302
|
+
- **В Редакторе ссылка по щелчку не открывается** — щелчок ставит каретку, как в любом тексте. Ссылки открывают `RichTextView` и `readOnly`-поле — как обычные `<a>`.
|
|
303
|
+
- На выходе каждая ссылка — `<a href="…" rel="noopener noreferrer">`. `target`, `class`, `title` и `rel` из входящего HTML отбрасываются: новая вкладка — решение страницы, которая показывает Документ, а не данных.
|
|
304
|
+
|
|
305
|
+
## Инструменты и Пресеты
|
|
306
|
+
|
|
307
|
+
`preset="full"` (по умолчанию) — всё, что есть в выпуске. `preset="basic"` — для комментариев и коротких описаний. `tools` — точный список, сильнее Пресета; в нём вперемешку с именами кита стоят описания своих Инструментов (раздел «Свой Инструмент»). Тип элемента `tools` — `Tool` (имя кита или `CustomTool`), тип имени кита — `ToolName`; оба экспортируются из `@olddevs/ui/editor`.
|
|
308
|
+
|
|
309
|
+
Пресеты экспортируются и массивами имён — `RICH_TEXT_PRESETS.full` и `RICH_TEXT_PRESETS.basic`, чтобы собирать из них свой набор: `tools={[...RICH_TEXT_PRESETS.full, mergeTagTool]}`. Массивы заморожены. Состав Пресета — «то, что кит считает полным набором», а не замороженный список: новый встроенный Инструмент приходит в Пресет minor-версией. Нужен стабильный состав — передайте точный список имён.
|
|
310
|
+
|
|
311
|
+
**Выключенный Инструмент убирает и кнопку, и расширение Схемы.** Его Блок или Метку не получить ни сочетанием клавиш, ни Markdown-разметкой при наборе, ни вставкой, а из `value` они приходят без разметки Блока, остаётся текст.
|
|
312
|
+
|
|
313
|
+
| `Tool` | Кнопка | Разметка | Сочетание | Markdown при наборе | `basic` |
|
|
314
|
+
| ----------------- | -------------------------------------- | ------------------------------------ | ------------------------------------------ | ---------------------------- | ------- |
|
|
315
|
+
| `heading` | «¶»: Заголовок 2–4 | `h2`, `h3`, `h4` | `Ctrl/⌘+Alt+2…4` | `## `, `### `, `#### ` | — |
|
|
316
|
+
| `heading1` | «¶»: добавляет Заголовок 1 | `h1` | `Ctrl/⌘+Alt+1` | `# ` | — |
|
|
317
|
+
| `history` | Отменить, Повторить | — | `Ctrl/⌘+Z`, `+Shift+Z` | — | да |
|
|
318
|
+
| `bold` | Жирный | `strong` | `Ctrl/⌘+B` | `**текст**` | да |
|
|
319
|
+
| `italic` | Курсив | `em` | `Ctrl/⌘+I` | `*текст*` | да |
|
|
320
|
+
| `underline` | «…»: Подчёркивание | `u` | `Ctrl/⌘+U` | — | — |
|
|
321
|
+
| `strike` | Зачёркнутый | `s` | `Ctrl/⌘+Shift+S` | `~~текст~~` | да |
|
|
322
|
+
| `code` | «…»: Строчный код | `code` | `Ctrl/⌘+E` | `` `текст` `` | да |
|
|
323
|
+
| `superscript` | «…»: Верхний индекс | `sup` | `Ctrl/⌘+.` | — | — |
|
|
324
|
+
| `subscript` | «…»: Нижний индекс | `sub` | `Ctrl/⌘+,` | — | — |
|
|
325
|
+
| `clearFormatting` | «…»: Очистить стили | — (снимает Метки) | `Ctrl/⌘+\` | — | — |
|
|
326
|
+
| `link` | Ссылка (поповер адреса) | `a[href]` | `Ctrl/⌘+K` | `[текст](адрес)` при вставке | да |
|
|
327
|
+
| `color` | Цвет ▾ (Меню выделения): «Цвет текста» | `span[data-color]`, `span[style]` | — | — | — |
|
|
328
|
+
| `highlight` | Маркер ▾ (Панель), «Цвет ▾» | `mark[data-color]`, `mark[style]` | — | — | — |
|
|
329
|
+
| `align` | Выравнивание ▾ (меню) | `p`, `h2`–`h4` с `data-align` | `Ctrl/⌘+Shift+L`, `+E`, `+R` | — | — |
|
|
330
|
+
| `bulletList` | Списки ▾: Маркированный | `ul`, `li` | `Ctrl/⌘+Shift+8` | `- ` или `* ` | да |
|
|
331
|
+
| `orderedList` | Списки ▾: Нумерованный | `ol`, `li` | `Ctrl/⌘+Shift+7` | `1. ` | да |
|
|
332
|
+
| `taskList` | Списки ▾: Чек-лист | `ul[data-type=taskList]` и пункты | `Ctrl/⌘+Shift+9` | `[ ] ` или `[x] ` | — |
|
|
333
|
+
| `blockquote` | «…»: Цитата | `blockquote` | `Ctrl/⌘+Shift+B` | `> ` | да |
|
|
334
|
+
| `codeBlock` | «…»: Блок кода | `pre`, `code class="language-*"` | `Ctrl/⌘+Alt+C` | ` ``` ` | — |
|
|
335
|
+
| `horizontalRule` | «+»: Разделитель | `hr` | — | `---` | — |
|
|
336
|
+
| `table` | Таблица ▾ (меню) | `table`, `tbody`, `tr`, `th`, `td` | `Tab`, `Shift+Tab`, `Alt+↓` в таблице | — | — |
|
|
337
|
+
| `image` | Картинка (поповер адреса) | `figure`, `img`, `figcaption` | — | `` при вставке | — |
|
|
338
|
+
| `emoji` | Эмодзи (поповер с сеткой) | — (обычный текст) | — | — | — |
|
|
339
|
+
| `source` | HTML (переключатель режима) | — (правит HTML Документа) | — | — | — |
|
|
340
|
+
| `search` | Поиск (панель поиска и замены) | — (ищет по Документу) | `Ctrl/⌘+F`, `Ctrl/⌘+Alt+F` | — | да |
|
|
341
|
+
| `layout` | «+»: Раскладка | `div` с `data-block` и `data-layout` | `Tab`, `Shift+Tab`, стрелки у края колонки | — | — |
|
|
342
|
+
| `wrapper` | «+»: Обёртка | `div[data-block=wrapper]` | `Enter` в пустом последнем абзаце — выход | — | — |
|
|
343
|
+
| `insertMenu` | «+» Меню вставки | — (Блоки других Инструментов) | — | `/` | — |
|
|
344
|
+
|
|
345
|
+
Абзац есть всегда, мягкий перенос строки — `Shift+Enter`. `insertMenu` и `emoji`, как и `clearFormatting`, своей разметки не имеют: `insertMenu` даёт доступ к Блокам других Инструментов, а выключают его там, где `/` — обычный символ (пути, «и/или»); в `basic` его нет по той же причине. `emoji` вставляет обычный текст — Схема, санитайзер и Markdown от него не меняются (раздел «Эмодзи»); в `basic` его нет: в комментарии системная палитра ОС удобнее, а кнопка занимала бы место у короткого ряда. `source` — HTML-режим (раздел «HTML-режим»): своей разметки у него тоже нет, правка HTML проходит ту же Схему, что `value`, и обойти набор Инструментов им нельзя; в `basic` его нет — в комментарии кнопка «HTML» лишняя, если экран её не просил.
|
|
346
|
+
|
|
347
|
+
**Заголовок первого уровня — `heading1`** (надстройка над `heading`): в `full` он есть, в `basic` нет. С ним уровни Блока — 1–4: `Ctrl/⌘+Alt+1` и `# ` с пробелом дают H1, `<h1>` из `value`, `defaultValue` и вставки остаётся H1, меню «¶» на Панели предлагает «Заголовок 1». Без него — прежнее поведение: `# ` при наборе остаётся текстом, `Ctrl/⌘+Alt+1` ничего не делает, `<h1>` во входящем HTML становится абзацем. Выключайте `heading1` на экране, где заголовок страницы уже стоит над Редактором: второй H1 ломает структуру страницы. Без Инструмента `heading` H1 нет, даже если `heading1` передан.
|
|
348
|
+
|
|
349
|
+
Раньше «H1 в Документе нет» было решением кита (заголовок страницы стоит вне Редактора); теперь это решение экрана — ADR-0053 пересматривает прежнее решение; ссылка на ADR-0053 стоит в ADR-0050.
|
|
350
|
+
|
|
351
|
+
Имя приходит в тип `Tool` вместе с самим Инструментом, не раньше: расширение входного типа потребителя не ломает, а имя без реализации компилировалось бы и ничего не давало. Так пришли `link` — сразу и в Пресет `basic` — и `color`, `highlight`, `align`, `table`, `image`, `emoji`, `layout`, `wrapper`, `source`; зарезервированных имён сейчас нет. Неизвестные имена в `tools` (из JS без типов) отбрасываются молча.
|
|
352
|
+
|
|
353
|
+
`preset` и `tools` читаются **при создании движка**: Схему живого ProseMirror не поменять. Чтобы сменить набор у смонтированного поля, перемонтируйте его ключом (`key`) — история правок при этом теряется.
|
|
354
|
+
|
|
355
|
+
## Свой Инструмент
|
|
356
|
+
|
|
357
|
+
Свой Инструмент — описание одной возможности Редактора: его Блоки и Метки, правила Схемы данными и Часть для движка (расширения Tiptap). Он передаётся в `tools` вперемешку с именами кита и подчиняется тем же правилам: **не переданный Инструмент убирает свои Блоки и Метки из Схемы** — из `value`, вставки и команд они не проходят, Блок разворачивается в своё содержимое, Атом пропадает. Решение — ADR-0058, разделы 1 и 2.
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
import { Node } from "@tiptap/core";
|
|
361
|
+
import { RICH_TEXT_PRESETS, RichTextEditor, type CustomTool } from "@olddevs/ui/editor";
|
|
362
|
+
|
|
363
|
+
// Часть для движка: только поведение. Группу, разбор HTML и атрибуты ставит кит.
|
|
364
|
+
const MergeTag = Node.create({
|
|
365
|
+
name: "acme_mergeTag",
|
|
366
|
+
atom: true,
|
|
367
|
+
addAttributes: () => ({ acme_key: { default: null } }),
|
|
368
|
+
renderText: ({ node }) => `{{${node.attrs.acme_key ?? ""}}}`,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
export const mergeTagTool: CustomTool = {
|
|
372
|
+
name: "acme_mergeTag",
|
|
373
|
+
engine: [MergeTag],
|
|
374
|
+
blocks: {
|
|
375
|
+
acme_mergeTag: {
|
|
376
|
+
kind: "inline",
|
|
377
|
+
attributes: { acme_key: { kind: "pattern", pattern: "[a-z_]+" } },
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
<RichTextEditor
|
|
383
|
+
tools={[...RICH_TEXT_PRESETS.full, mergeTagTool]}
|
|
384
|
+
value={html}
|
|
385
|
+
onChange={setHtml}
|
|
386
|
+
/>;
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Описание (`CustomTool`).** Обязательны имя, Часть для движка и правила Схемы для каждого Node и Mark, которые она определяет. Остальное — по умолчанию: обычный Блок без Признаков, без своих кнопок и пунктов меню.
|
|
390
|
+
|
|
391
|
+
| Поле | Что это |
|
|
392
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
393
|
+
| `name` | Имя Инструмента с Префиксом потребителя, уникальное в `tools` |
|
|
394
|
+
| `engine` | Часть для движка — массив расширений Tiptap (Node, Mark, Extension, плагины, React-вид узла; Node и Mark — на верхнем уровне) или Загрузчик такого массива (ниже) |
|
|
395
|
+
| `blocks` | Правила Схемы Блоков — по имени Node (`CustomBlock`) |
|
|
396
|
+
| `marks` | Правила Схемы Меток — по имени Mark (`CustomMark`) |
|
|
397
|
+
| `buttons` | Кнопки Панели — раздел «Кнопка своего Инструмента» (`CustomToolButton`) |
|
|
398
|
+
| `insertItems` | Пункты Меню вставки (`CustomInsertItem`) — раздел «Пункты вставки и Действия над Блоком» |
|
|
399
|
+
| `blockActions` | Действия над Блоком в меню Ручки Блока (`CustomBlockAction`) — тот же раздел |
|
|
400
|
+
| `textCards` | Карточки у текста (`CustomTextCard`) — раздел «Карточка у текста» |
|
|
401
|
+
| `trigger` | Свой символ-триггер (`CustomTrigger`) — раздел «Свой символ-триггер» |
|
|
402
|
+
|
|
403
|
+
**Префикс потребителя.** Имена Инструмента, его Блоков, Меток и атрибутов начинаются с префикса через подчёркивание: `acme_mergeTag`, атрибут `acme_size`. В именах кита подчёркивания не бывает, поэтому своё и встроенное не пересекаются ни в наборе, ни в сохранённом Документе. Имена атрибутов — строчными: в HTML регистр имени атрибута теряется. Встроенный Инструмент не подменяется: выключите его в `tools` и поставьте свой под своим именем.
|
|
404
|
+
|
|
405
|
+
**HTML-соглашение.** Его пишет и читает кит, Часть для движка задаёт только поведение:
|
|
406
|
+
|
|
407
|
+
- блочный Блок — `<div data-block="acme_card">…</div>`, строчный Атом — `<span data-block="acme_mergeTag"></span>`;
|
|
408
|
+
- своя Метка — `<span data-mark="acme_comment">…</span>`;
|
|
409
|
+
- свой атрибут `acme_size` — `data-acme_size`; других атрибутов у своего Блока и Метки нет.
|
|
410
|
+
|
|
411
|
+
Кит дописывает Node и Mark Части для движка: ставит группу (`block`, `inline` или верхний уровень) и строчность по виду Блока; разбирает HTML только по этому соглашению (теги кита `p`, `table`, `img` свой Блок занять не может); для атрибутов из правил разбирает и выводит `data-*` с проверкой значения. Свой `renderHTML` можно оставить (например, чтобы положить текст в Атом), но он обязан давать тот же тег и `data-block`. Свои атрибуты Node объявлять не обязательно: кит добавит их из правил.
|
|
412
|
+
|
|
413
|
+
**Правила Схемы — данные, без функций.** Их проверяет код кита, и ослабить проверку Частью для движка нельзя: значение вне правил не попадает в HTML Документа ни из `value`, ни из вставки, ни из команды. При разборе атрибут получает значение по умолчанию, при выводе не пишется.
|
|
414
|
+
|
|
415
|
+
| Поле `CustomBlock` | Значение |
|
|
416
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
417
|
+
| `kind` | `"block"` — блочный (`div`) или `"inline"` — строчный (`span`). Обязательно |
|
|
418
|
+
| `place` | `"anywhere"` (по умолчанию) — везде, где стоит Блок; `"top"` — только верхний уровень Документа, как Раскладка |
|
|
419
|
+
| `attributes` | Свои атрибуты: имя → вид значения (`CustomAttribute`) |
|
|
420
|
+
| `container` | Признак «контейнер»: Блоки внутри — со своей Ручкой Блока, как у Обёртки |
|
|
421
|
+
| `turnInto` | Признак «можно превратить в»: «Формат ›» Ручки Блока доступен и для этого Блока |
|
|
422
|
+
| `plainAtom` | Признак «атом без меню форматирования»: над выделенным Блоком нет Меню выделения и плавающей Панели |
|
|
423
|
+
| `island` | Остров Блока в `RichTextView` — клиентский компонент из модуля с `"use client"` (`CustomIslandProps`); Редактор поле не читает |
|
|
424
|
+
|
|
425
|
+
У `CustomMark` — только `attributes`. Вид значения атрибута (`CustomAttribute`):
|
|
426
|
+
|
|
427
|
+
- `{ kind: "list", values: ["s", "m", "l"] }` — одно из значений списка;
|
|
428
|
+
- `{ kind: "pattern", pattern: "[0-9]{3}" }` — регулярное выражение строкой на всё значение (`^…$` кит ставит сам);
|
|
429
|
+
- `{ kind: "url" }` — адрес по политике протоколов кита, как у ссылки: абсолютный `http`, `https` или `mailto`.
|
|
430
|
+
|
|
431
|
+
HTML внутри атрибута не бывает: значение с `<` не проходит ни по какому правилу, а содержимое нередактируемого Блока хранится разметкой Документа внутри Блока.
|
|
432
|
+
|
|
433
|
+
**Признаки Блока** по умолчанию выключены: обычный Блок — не контейнер, без «Превратить в», с Меню выделения. Ручка Блока у него есть, как у любого Блока: «Добавить ›», «Формат ›» (недоступен), «Вверх», «Вниз», «Дублировать», «Удалить». Кит переносит Признаки в spec узла сам; их словарь ведёт кит, а не Tiptap.
|
|
434
|
+
|
|
435
|
+
**Непрошедшая проверка при сборке набора — ошибка поля.** Набор проверяется один раз, при создании движка; с Загрузчиками — когда они отработали, по тому, что они отдали. Ошибка, если:
|
|
436
|
+
|
|
437
|
+
- имя Инструмента, Блока, Метки или атрибута не имеет Префикса потребителя;
|
|
438
|
+
- у двух Инструментов совпало имя либо имя Блока или Метки;
|
|
439
|
+
- Часть для движка расходится с правилами: Node или Mark без правила, правило без Node или Mark на верхнем уровне `engine`, атрибут узла без правила, `renderHTML` мимо соглашения, Схема, которую движок не собрал;
|
|
440
|
+
- у кнопки нет `id` или иконки, два `id` в одном Инструменте совпали, или не задано ровно одно из `run` и `popover` (у Пункта вставки и Действия над Блоком — тоже).
|
|
441
|
+
|
|
442
|
+
При ошибке движок не создаётся: под рамкой поля — строка `role="alert"` «Редактор не запустился. Документ не изменён.» (`editor.toolsFailed`), скрытое поле формы и `getHTML()` отдают входящее значение, `onChange` молчит, `ref.current.editor` — `null`. Причина — в `console.error` в режиме разработки, с именем Инструмента. Так потеря данных видна до первого сохранения, а не на сервере. **Неизвестная строка в `tools` по-прежнему отбрасывается молча**: ошибка относится только к описаниям своих Инструментов.
|
|
443
|
+
|
|
444
|
+
Кнопки, пункты Меню вставки, Действия над Блоком и Карточки у текста описаны в разделах ниже; сообщить автору об ошибке Инструмент может командой `showNotice` (раздел «Сообщение поля»). Всё API расширения и граница его стабильности (что подчиняется semver, что считается ломающим) описаны на странице [API расширения Редактора](../../api/editor-extension.md).
|
|
445
|
+
|
|
446
|
+
### Загрузчик Части для движка
|
|
447
|
+
|
|
448
|
+
Вместо массива `engine` своего Инструмента можно передать функцию с динамическим импортом — она отдаёт промис того же массива (ADR-0058, пп. 1.3 и 11.3–11.6). Так модуль с описанием можно импортировать и на сервере (правила Схемы — данные), а Tiptap и React Части для движка уходят в отдельный чанк:
|
|
449
|
+
|
|
450
|
+
```tsx
|
|
451
|
+
// acme-merge-tag.ts — без Tiptap в графе: его тянет только чанк Части для движка.
|
|
452
|
+
export const mergeTagTool: CustomTool = {
|
|
453
|
+
name: "acme_mergeTag",
|
|
454
|
+
engine: () => import("./acme-merge-tag-engine.js").then((module) => module.engine),
|
|
455
|
+
blocks: {
|
|
456
|
+
acme_mergeTag: {
|
|
457
|
+
kind: "inline",
|
|
458
|
+
attributes: { acme_key: { kind: "pattern", pattern: "[a-z_]+" } },
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
- **Готовность ждёт Загрузчики.** Движок создаётся, когда отработали Загрузчики всех Инструментов набора: Схема фиксируется при создании, и Блоки из `value` без своей Части для движка потерялись бы при разборе. До того методы Хэндла тихие, `ref.current.editor` — `null`, `onReady` не вызван (раздел «События»).
|
|
465
|
+
- **Загрузка стартует с первого рендера в браузере**, ещё до монтажа, а не в эффекте; на сервере Загрузчик не вызывается. Промис кэшируется на самой функции Загрузчика: второй Редактор на странице с тем же Инструментом ничего не ждёт и Загрузчик не вызывает. Поэтому Загрузчик — одна функция на модуль, а не новая стрелка на каждый рендер.
|
|
466
|
+
- **Сбой загрузки** (сеть, устаревший чанк после выкладки) — ошибка поля «Редактор не запустился. Документ не изменён.» и кнопка «Повторить» (`editor.retry`). Она заново вызывает только упавшие Загрузчики; удачные остаются в кэше. Документ всё это время виден Скелетом. Редактор не стартует без Инструмента: его Блоки пропали бы из Документа. `onReady` при сбое не вызывается, причина — в `console.error` в режиме разработки.
|
|
467
|
+
- **Предзагрузки кит не публикует.** Чтобы чанк пришёл вместе со страницей, а не после её скрипта, добавьте на него `modulepreload` сами. Адрес чанка с хешем — из манифеста сборщика (в Vite — `build.manifest`):
|
|
468
|
+
|
|
469
|
+
```html
|
|
470
|
+
<link rel="modulepreload" href="/assets/acme-merge-tag-engine-3f9a1c.js" />
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
В React 19 тот же тег ставит `preloadModule(url)` из `react-dom`. Без предзагрузки чанк начнёт грузиться с первого рендера Редактора — это время закрывает Скелет.
|
|
474
|
+
|
|
475
|
+
### Скелет
|
|
476
|
+
|
|
477
|
+
Скелет (ADR-0058, п. 11.4) есть у каждого Редактора, с Загрузчиками и без: до Готовности и при сбое Загрузчика поле рисует рамку, место фиксированной Панели и Документ статичным HTML — тем же классом и ритмом, что Документ движка, без ввода и без роли `textbox`. Движок встаёт на его место без прыжка высоты.
|
|
478
|
+
|
|
479
|
+
- **Документ Скелета — `value`, очищенный Схемой**, а не строка как есть: кит разбирает её в инертном документе браузера (скрипты и обработчики там не исполняются) по Схеме встроенных Инструментов набора и своих, чья Часть для движка уже есть, и выгружает обратно в HTML. В страницу не попадает и до Готовности всё, чего Схема не знает: `<script>`, `on*`, `javascript:`, неизвестные теги и атрибуты (раздел «Схема и безопасность»). Блок своего Инструмента, чей Загрузчик ещё не отработал, виден текстом: его разметку Схема снимает, а движок с пришедшей Частью разбирает `value` заново.
|
|
480
|
+
- **Блок кода и таблица в `full` тоже ленивые** (ADR-0058, п. 11.1): их нет в `basic`, а их Часть для движка тяжелее 10 кБ gzip, поэтому она приходит отдельным чанком тем же Загрузчиком, что у своего Инструмента. Вместе с ней приходят меню «Таблица», окно «Вставить таблицу» и шапка блока кода. `basic` чанки не грузит и готов сразу.
|
|
481
|
+
- До прихода чанков Редактор с ними — Скелет, `onReady` ждёт оба. Их Блоки в Скелете видны своей формой (блок кода с шапкой, таблица ячейками), и поле не прыгает с приходом чанков. Чтобы чанки пришли вместе со страницей, поставьте на них `modulepreload`, как на чанк своего Инструмента. Размер «старта» и «полного» — в таблице «Размер импорта» в [getting-started](../../getting-started.md#размер-импорта).
|
|
482
|
+
- **На сервере и до гидратации Скелет — только рамка и место Панели**, без Документа: разбор Схемой идёт в DOM браузера. Документ появляется после монтирования, поэтому на SSR-странице высота поля меняется один раз, как до Скелета. В клиентском рендере Документ Скелета есть с первого кадра.
|
|
483
|
+
|
|
484
|
+
Вид Скелета не входит в контракт.
|
|
485
|
+
|
|
486
|
+
### Текст атома
|
|
487
|
+
|
|
488
|
+
Одно правило для встроенных и своих Атомов (ADR-0058, раздел 4): Текст атома — то, что даёт `renderText` Части для движка; Атом без содержимого и без `renderText` — пустая строка, Атом с содержимым внутри (картинка с подписью) — текст своих детей. Из него кит строит всё, что выдаёт текст:
|
|
489
|
+
|
|
490
|
+
- **Вес в `maxLength`** равен длине Текста атома, поэтому счётчик совпадает с `getText()` без разделителей Блоков. Для встроенных: разделитель весит 0, мягкий перенос — 1, картинка — длину подписи; Merge Tag из примера выше — длину `{{ключа}}`. Отдельного поля веса нет: оно разошлось бы с текстом. Серверного счётчика у кита нет — вес зависит от Части для движка.
|
|
491
|
+
- **`getText()`, `getSelectedText()`, текст при копировании и `getMarkdown()`** отдают один и тот же Текст атома; Атом попадает в выделенный текст только целиком. Атом без `renderMarkdown` выгружается в Markdown своим Текстом атома; пропадает только Атом без текста; свой `renderMarkdown` у Части для движка сильнее.
|
|
492
|
+
- **Пустота — правило кита, без поля описания.** Любой Атом, свой или встроенный, — содержимое для `required`, `isEmpty()` и `getHTML()`: вставка видео без подписи проходит проверку обязательности. Пустой не-атомарный Блок (абзац без текста) содержимым не считается.
|
|
493
|
+
- **Вставка из буфера за пределом `maxLength`** срезает хвост по символам, а не по позициям. Атом не режется: не поместившийся уходит целиком вместе со всем, что вставлено после него; если предел всё ещё превышен, вставка отвергается целиком.
|
|
494
|
+
|
|
495
|
+
### Вне Редактора: сервер, Markdown и Просмотр
|
|
496
|
+
|
|
497
|
+
Тот же массив `tools` понимают серверная очистка и Просмотр (ADR-0058, п. 2.3 и раздел 3).
|
|
498
|
+
|
|
499
|
+
- **Сервер** передаёт очистке тот же массив `tools`: `sanitizeRichText(html, DOMPurify, tools)` пропускает свои Блоки, Атомы и Метки по их правилам и срезает всё прочее, `richTextSchema(tools)` отдаёт эти правила данными. Имена Инструментов кита белый список не сужают, без третьего аргумента очистка прежняя. Оба имени есть и в `@olddevs/ui/editor`, и в лёгком `@olddevs/ui/editor/schema`; в графе лёгкого входа по-прежнему нет ни Tiptap, ни React, а описание своего Инструмента — данные, которые сервер читает без Части для движка. Подробно — [RichTextView, «Свои Инструменты: третий аргумент»](../content/rich-text-view.md#свои-инструменты-третий-аргумент).
|
|
500
|
+
- **Markdown.** Свои правила — в Части для движка, как у любого расширения Tiptap: `renderMarkdown`, а для разбора — `markdownTokenizer` и `parseMarkdown`; с ними Блок проходит круг `getMarkdown()` → `setMarkdown()`. Без них `getMarkdown()` не теряет текст: Блок выгружается своим содержимым (контейнер — своими Блоками), Атом — Текстом атома из `renderText` (экранированным, как любой текст), Метка — своим текстом; пропадает только Атом без текста. Обратно такой Блок из Markdown не создаётся — его текст встаёт абзацами. В режиме разработки — одно предупреждение `console.warn` на Инструмент, когда это умолчание впервые сработало.
|
|
501
|
+
- **`RichTextView`** показывает свой Блок очищенным HTML с вертикальным ритмом абзаца — и больше ничего; вид задаёт ваш CSS: селекторы по `data-block` и токены кита ([RichTextView, «Свой Блок»](../content/rich-text-view.md#свой-блок)). Оживить Блок в показе можно Островом — полем `island` у Блока; `RichTextView` получает тот же `tools` ([RichTextView, «Остров своего Блока»](../content/rich-text-view.md#остров-своего-блока)).
|
|
502
|
+
|
|
503
|
+
## Кнопка своего Инструмента
|
|
504
|
+
|
|
505
|
+
Свой Инструмент описывает кнопку данными в поле `buttons`, а рисует её кит: роли, одну Tab-остановку Панели и стрелки по ней, `aria-pressed`, подсказку с `Kbd` и поповер с рамкой, слоем и возвратом фокуса. Произвольный React допускается только в содержимом поповера. Решение — ADR-0058, разделы 5 и 6.
|
|
506
|
+
|
|
507
|
+
```tsx
|
|
508
|
+
import { RichTextEditor, type CustomTool } from "@olddevs/ui/editor";
|
|
509
|
+
import { SparkleIcon } from "@olddevs/ui/icons";
|
|
510
|
+
|
|
511
|
+
export const stampTool: CustomTool = {
|
|
512
|
+
name: "acme_stamp",
|
|
513
|
+
engine: [], // кнопке своей Схемы и Части для движка не нужно
|
|
514
|
+
buttons: [
|
|
515
|
+
{
|
|
516
|
+
id: "stamp",
|
|
517
|
+
label: ({ locale }) => (locale.startsWith("ru") ? "Штамп" : "Stamp"),
|
|
518
|
+
icon: SparkleIcon,
|
|
519
|
+
can: (editor) => editor.state.selection.$to.parent.inlineContent,
|
|
520
|
+
run: (editor) => void editor.chain().focus().insertContent("★").run(),
|
|
521
|
+
shortcut: ["mod", "shift", "Y"],
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
};
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
| Поле | Что это |
|
|
528
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
529
|
+
| `id` | Ключ кнопки, уникальный в своём Инструменте. С `id` кита и других Инструментов не сталкивается: кит добавляет к нему имя Инструмента |
|
|
530
|
+
| `label` | Обязательно. Подпись — строка или функция от `{ locale }`. Имя кнопки для скринридера, текст подсказки и имя поповера |
|
|
531
|
+
| `icon` | Компонент с контрактом `IconProps` из словаря кита или свой. Обязателен: имён из словаря и запасного иконки нет |
|
|
532
|
+
| `can` | Обязательно. Доступна ли кнопка сейчас. Недоступная — `aria-disabled`, остаётся в обходе стрелками, щелчок и сочетание ничего не делают |
|
|
533
|
+
| `isActive` | Есть — кнопка-переключатель с `aria-pressed`; нет — разовое действие без `aria-pressed` |
|
|
534
|
+
| `isVisible` | Предикат видимости по контексту: кнопка скрыта, пока он возвращает `false`. Нет — видна всегда |
|
|
535
|
+
| `selectionMenu` | `true` — кнопка стоит ещё и в Меню выделения (`toolbar="bubble"`) |
|
|
536
|
+
| `shortcut` | Сочетание: `mod`, `alt`, `shift` и клавиша — `["mod", "shift", "Y"]` |
|
|
537
|
+
| `run` | `(editor) => void` — щелчок и сочетание |
|
|
538
|
+
| `popover` | `({ editor, close }) => ReactNode` — поповер вместо действия. Ровно одно из `run` и `popover` |
|
|
539
|
+
|
|
540
|
+
- **Место.** Свои кнопки стоят на Панели одной группой перед «Отменить» и «Повторить», после групп кита, в порядке `tools` и порядке массива `buttons`. Фиксированная и плавающая Панель показывают один набор. Порядок кнопок кита задаёт кит; спрятать встроенную можно только выключив Инструмент, а короткую Панель получают, сужая `tools`. Выключенный Инструмент уносит и кнопки, и сочетание.
|
|
541
|
+
- **Состояние.** `can`, `isActive` и `isVisible` читаются на каждой транзакции, как у кнопок кита, поэтому берут состояние Документа: `editor.isActive("acme_pin")`. В нередактируемом поле (`readOnly`, `disabled`) кит выключает кнопки сам.
|
|
542
|
+
- **Поповер.** Рамку держит кит: слой `Popover`, якорь у кнопки, стек Escape (внутри `Dialog` Escape закрывает поповер, а не окно), имя рамки — подпись кнопки. Содержимое монтируется заново на каждое открытие. `close` закрывает поповер тем же путём, что Escape. Закрытие возвращает фокус и выделение в Документ, а не на кнопку; щелчок мимо фокус не трогает. Вызванный сочетанием поповер встаёт у каретки, а не у кнопки: кнопки на экране может и не быть (`toolbar="none"`).
|
|
543
|
+
- **Сочетание — единственный источник правды.** Кит сам привязывает клавишу к `run` (или открывает поповер), пишет её в подсказку с `Kbd`, в `aria-keyshortcuts` кнопки и в Справочник сочетаний: свои строки стоят там отдельной группой без заголовка после групп кита, в порядке `tools`. Недоступная кнопка клавишу не берёт, и она идёт дальше, в Документ. Клавиши, которые вешает Часть для движка (`addKeyboardShortcuts`), в Справочник не попадают: сочетание кнопки задавайте полем `shortcut`, а не расширением.
|
|
544
|
+
- **Подпись.** Функция получает `{ locale }` (локаль `UiProvider`), и подпись перерисовывается при её смене; в одноязычном приложении хватает строки. Переопределение подписей — опциями фабрики Инструмента (например, поле `labels`; рецепт — [API расширения, «Подписи через опции фабрики»](../../api/editor-extension.md#подписи-через-опции-фабрики)), каталог `UiProvider` под Инструменты потребителя не расширяется. Если подпись пустая, в разработке в консоль идёт `console.error` с именем Инструмента и кнопки, а вместо подписи в имя кнопки, подсказку и имя поповера подставляется имя Инструмента.
|
|
545
|
+
- **Кнопок-меню** (с радиопунктами, с подменю) у своих Инструментов нет: для выбора из списка хватает поповера.
|
|
546
|
+
|
|
547
|
+
## Пункты вставки и Действия над Блоком
|
|
548
|
+
|
|
549
|
+
Свой Инструмент добавляет пункт в Меню вставки и Действие в меню Ручки Блока **описанием, без правки меню**: кит рисует, потребитель описывает (ADR-0058, разделы 5 и 6). Выключенный Инструмент убирает свои пункты и Действия отовсюду.
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
import { ColumnLayoutIcon } from "@olddevs/ui/icons";
|
|
553
|
+
|
|
554
|
+
export const calloutTool: CustomTool = {
|
|
555
|
+
name: "acme_callout",
|
|
556
|
+
engine: [Callout],
|
|
557
|
+
blocks: { acme_callout: { kind: "block", container: true } },
|
|
558
|
+
insertItems: [
|
|
559
|
+
{
|
|
560
|
+
section: "insert",
|
|
561
|
+
// Строка — для одного языка; функция получает { locale } из UiProvider.
|
|
562
|
+
label: ({ locale }) => (locale.startsWith("ru") ? LABELS.ru : LABELS.en),
|
|
563
|
+
icon: ColumnLayoutIcon,
|
|
564
|
+
aliases: ["note", "tip"],
|
|
565
|
+
shortcut: ["mod", "alt", "N"],
|
|
566
|
+
run: (chain) =>
|
|
567
|
+
chain.insertContent({ type: "acme_callout", content: [{ type: "paragraph" }] }),
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
blockActions: [
|
|
571
|
+
{
|
|
572
|
+
label: "Unwrap callout",
|
|
573
|
+
icon: ColumnLayoutIcon,
|
|
574
|
+
blocks: ["acme_callout"],
|
|
575
|
+
isVisible: (editor, pos) => editor.state.doc.nodeAt(pos)?.type.name === "acme_callout",
|
|
576
|
+
run: (editor, pos) => unwrapCallout(editor, pos),
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
};
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
**Пункт вставки (`CustomInsertItem`).**
|
|
583
|
+
|
|
584
|
+
| Поле | Что это |
|
|
585
|
+
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
586
|
+
| `section` | `"insert"` («Добавить» — новый Блок после текущего) или `"turnInto"` («Превратить в» — сменить тип текущего Блока, сохранив текст). Обязательно |
|
|
587
|
+
| `label` | Подпись: строка или функция, которая получает `{ locale }` (локаль `UiProvider`), как у кнопки. Обязательно |
|
|
588
|
+
| `icon` | Компонент с контрактом `IconProps` из `@olddevs/ui/icons` или свой. Обязательно: запасной иконки нет |
|
|
589
|
+
| `aliases` | Массив строк на любом языке — слова для поиска сверх подписи |
|
|
590
|
+
| `shortcut` | Сочетание в нотации Панели: `["mod", "alt", "N"]`. Единственный источник клавиши: кит вешает её, пишет в строку пункта и в Справочник сочетаний |
|
|
591
|
+
| `run` | `(chain, editor) => chain` — команда над цепочкой: вернуть цепочку с командой Блока, `.run()` вызывает кит |
|
|
592
|
+
| `popover` | `({ editor, close }) => ReactNode` — поповер у каретки вместо команды. Ровно одно из `run` и `popover` |
|
|
593
|
+
|
|
594
|
+
- **Где виден пункт.** Он сам попадает в «/», «+» Панели, «Добавить ›» и «Формат ›» Ручки Блока: раздел `turnInto` — и в «Формат ›», и в «¶»; в «Добавить ›» стоят пункты обоих разделов (новый пустой Блок ниже, как у встроенных). Свои пункты встают **в конец своего раздела**, в порядке `tools`; порядок внутри Инструмента — порядок массива. Пункта без включённого `insertMenu` нет — как у встроенных.
|
|
595
|
+
- **Одна правка, одна отмена.** Раздел решает, что делается с Документом, а `run` — какой Блок: в `insert` кит готовит пустой абзац (текущий пустой, иначе новый за Блоком), в `turnInto` — снимает с Блока окружение (список, цитату). Подготовка и `run` идут одной транзакцией.
|
|
596
|
+
- **Поиск в «/»** идёт по подписи в текущей локали, по подписи, вызванной с `en-US`, и по `aliases` — как у встроенных. Подпись-функция вызывается при отрисовке: смена локали `UiProvider` перерисовывает пункт.
|
|
597
|
+
- **Напоминание** в строке — `Kbd` сочетания; управление меню с клавиатуры — как у встроенных (стрелки, `Enter`, `Escape`, первая буква). Сочетание срабатывает у каретки так же, как выбор пункта в «/».
|
|
598
|
+
- **Пустая подпись** — в разработке `console.error` с именем Инструмента и записи, а в меню вместо неё стоит имя Инструмента.
|
|
599
|
+
- **Поповер** (`popover`) — когда Блоку нужен выбор до вставки (символ, шаблон, адрес). Поповер тот же, что у кнопки: `Popover` кита в стеке Escape (внутри `Dialog` `Escape` закрывает поповер, а не окно), его доступное имя — подпись пункта, закрытие возвращает фокус и выделение в Документ. Выбор в «/», «+», «Добавить ›» или «Формат ›» закрывает меню и открывает поповер **у каретки**. Из «/» поповер встаёт там, где стоял «/», а набранная «/команда» убирается, как у обычного пункта. Сочетание пункта открывает тот же поповер у каретки. Документ кит не готовит — ни пустого абзаца, ни снятого списка или цитаты: Блок ставит содержимое через `editor`, одной транзакцией — одна отмена.
|
|
600
|
+
|
|
601
|
+
**Действие над Блоком (`CustomBlockAction`).**
|
|
602
|
+
|
|
603
|
+
| Поле | Что это |
|
|
604
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
605
|
+
| `label` | Подпись — строка или функция, которая получает `{ locale }`. Обязательно |
|
|
606
|
+
| `icon` | Компонент с контрактом `IconProps`. Обязательно |
|
|
607
|
+
| `blocks` | Имена Блоков, для которых Действие показывается: имя Блока, у которого открыта Ручка, или контейнера, в котором этот Блок стоит прямо. Обязательно, непустой массив |
|
|
608
|
+
| `run` | `(editor, pos) => void` — `pos` — позиция Блока, у которого открыта Ручка, в Документе. Правка — одна транзакция: отмена одна |
|
|
609
|
+
| `popover` | `({ editor, close, pos }) => ReactNode` — поповер у Блока вместо действия; `pos` — позиция этого Блока. Ровно одно из `run` и `popover` |
|
|
610
|
+
| `isVisible` | `(editor, pos) => boolean` — предикат видимости по контексту; нет — Действие видно у всех своих `blocks` |
|
|
611
|
+
| `shortcut` | Сочетание в нотации Панели: срабатывает над Блоком под кареткой и только там, где Действие видно в меню Ручки; на другом Блоке клавиша идёт в Документ |
|
|
612
|
+
|
|
613
|
+
Свои Действия стоят в меню Ручки Блока **своей секцией перед «Удалить»**, в порядке `tools`. Действие с `popover` открывает поповер **у Блока** — поповер, `Escape` и возврат фокуса те же, что у кнопки, его доступное имя — подпись Действия: выбор в меню Ручки — у её Блока, сочетание — у Блока под кареткой. `pos` едет за правками Документа, пока поповер открыт. Сочетание — в строке пункта (как у встроенных, и только когда сочетание делает то же, что пункт) и в Справочнике сочетаний: строки Инструмента — отдельная группа без заголовка после групп кита.
|
|
614
|
+
|
|
615
|
+
**Проверка набора `tools`** добавляет ошибки поля для записей: `insertItems`/`blockActions` не массив или запись не объект; нет подписи или иконки; не ровно одно из `run` и `popover`; раздел не `insert`/`turnInto`; `aliases` не массив строк; `blocks` пуст; `isVisible` не функция; сочетание короче двух клавиш (модификатор и клавиша).
|
|
616
|
+
|
|
617
|
+
## Свой символ-триггер
|
|
618
|
+
|
|
619
|
+
Свой Инструмент открывает тот же список, что «/», **своим символом**: `@` для упоминаний, `#` для тегов. Решение — ADR-0064. Список рисует кит — тот же компонент, что у Меню вставки: поповер у каретки, поиск по набранному, стрелки, `Enter`, `Escape`, строка «Ничего не найдено»; фокус остаётся в Документе. Потребитель описывает символ, пункты по запросу и то, что происходит при выборе пункта.
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
import { RichTextEditor, type CustomTool } from "@olddevs/ui/editor";
|
|
623
|
+
import { MentionIcon } from "@olddevs/ui/icons";
|
|
624
|
+
|
|
625
|
+
const mentionTool: CustomTool = {
|
|
626
|
+
name: "acme_mention",
|
|
627
|
+
engine: [Mention], // Атом `acme_mention` с атрибутом `acme_id`
|
|
628
|
+
blocks: {
|
|
629
|
+
acme_mention: {
|
|
630
|
+
kind: "inline",
|
|
631
|
+
attributes: { acme_id: { kind: "pattern", pattern: "[a-z]+" } },
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
trigger: {
|
|
635
|
+
char: "@",
|
|
636
|
+
// Пункты по запросу — сразу или промисом; фильтрует сам потребитель.
|
|
637
|
+
items: (query) =>
|
|
638
|
+
PEOPLE.filter((person) => person.name.toLowerCase().includes(query.toLowerCase())).map(
|
|
639
|
+
(person) => ({ id: person.id, label: person.name, icon: MentionIcon })
|
|
640
|
+
),
|
|
641
|
+
// Выбор заменяет набранное `@запрос` своим узлом.
|
|
642
|
+
onSelect: (item, { editor, range }) =>
|
|
643
|
+
void editor
|
|
644
|
+
.chain()
|
|
645
|
+
.focus()
|
|
646
|
+
.insertContentAt(range, [
|
|
647
|
+
{ type: "acme_mention", attrs: { acme_id: item.id } },
|
|
648
|
+
{ type: "text", text: " " },
|
|
649
|
+
])
|
|
650
|
+
.run(),
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Триггер (`CustomTrigger`).**
|
|
656
|
+
|
|
657
|
+
| Поле | Что это |
|
|
658
|
+
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
659
|
+
| `char` | Символ: один печатный знак, не `/` и не пробел (`@`, `#`, `:`). Уникален в `tools`. Обязательно |
|
|
660
|
+
| `items` | `(query) => TriggerItem[] \| Promise<TriggerItem[]>` — пункты по запросу. Вызывается на каждое изменение запроса, в том числе с `""` сразу после символа. Обязательно |
|
|
661
|
+
| `onSelect` | `(item, { editor, range, query }) => void` — выбор пункта: обработчик заменяет `range` своим узлом. Обязательно |
|
|
662
|
+
| `label` | Имя списка для скринридера: строка или функция, которая получает `{ locale }`. Нет — «Подсказки» из каталога сообщений (`editor.triggerList`) |
|
|
663
|
+
|
|
664
|
+
**Пункт (`TriggerItem`)**: `id` (уникален в одном ответе `items`) и `label` — текст строки; `icon` — иконка с контрактом `IconProps` (нет — строка без иконки); `data` — любые данные потребителя, кит их не читает и отдаёт в `onSelect`. Подпись приходит готовой на языке интерфейса: фильтрует и переводит её потребитель.
|
|
665
|
+
|
|
666
|
+
**Контекст выбора (`TriggerContext`)**: `editor` — сырой Tiptap для вставки своего узла; `range` — `{ from, to }`, набранное **вместе с символом** (`@gr`); `query` — запрос без символа (`gr`). **Кит набранное не удаляет:** это делает обработчик, заменяя `range` узлом одной транзакцией — тогда отмена одна, `Ctrl/⌘+Z` возвращает `@запрос`. После вызова `onSelect` кит закрывает список.
|
|
667
|
+
|
|
668
|
+
- **Когда открывается.** Те же правила, что у «/»: символ стоит в начале Блока или после пробела (неразрывный — тоже пробел), запрос идёт до пробела. Внутри слова (`a@b.com`) список молчит; в блоке кода и в строчном коде он не открывается. Пробел заканчивает запрос: список закрывается, текст остаётся.
|
|
669
|
+
- **Клавиатура.** Как у «/»: `↓`/`↑` по кругу, `Enter` выбирает, `Escape` закрывает и оставляет набранное (на том же символе повторно не откроется), `Enter` при пустом списке — обычный перевод строки. `Enter`, которым подтверждают ввод в IME (метод ввода), обрабатывает IME, а не список.
|
|
670
|
+
- **Асинхронные пункты.** Пока промис не завершён, показан прежний список (или ничего), строка «Ничего не найдено» не мигает. Ответ устаревшего запроса отбрасывается. Отклонённый промис — пустой список и `console.error` в разработке.
|
|
671
|
+
- **Независимость от «/».** Триггер работает и без `insertMenu` в `tools`; одновременно открыт только один список: запрос начинается ровно с одного символа-триггера. Открывается только в правке: в `readOnly`, `disabled` и в HTML-режиме списка нет.
|
|
672
|
+
- **Доступность.** Поле остаётся `role="textbox"` и на время списка получает `aria-controls` и `aria-activedescendant`, как у «/»; список — `role="listbox"` с именем из `label`.
|
|
673
|
+
- **Ошибки в `tools`** (поле показывает ошибку, причина — `console.error` в разработке): два Инструмента с одним `char`; `char` — не один печатный знак, либо пробел, либо `/`; `items` или `onSelect` не функции; `label` не строка и не функция. Свой символ-триггер не пересекается со встроенными: `/` занят Меню вставки.
|
|
674
|
+
- **Упоминание — узел потребителя.** Кит не знает, что вставляется: Атом упоминания, ссылка, тег — решает `onSelect`. Атом описывается как любой свой Блок (Часть для движка, правила Схемы, `renderText` — Текст атома), поэтому упоминание попадает в `getText()`, Markdown и `maxLength`.
|
|
675
|
+
|
|
676
|
+
## Карточка у текста
|
|
677
|
+
|
|
678
|
+
Четвёртый вид UI-записи (после кнопки, Пункта вставки и Действия над Блоком): небольшая карточка под диапазоном текста, в котором стоит каретка (например, карточка комментария под Меткой комментария). Кит держит Карточку, Инструмент описывает диапазон и тело (ADR-0058, раздел 7, п. 6–11).
|
|
679
|
+
|
|
680
|
+
```tsx
|
|
681
|
+
import { Mark } from "@tiptap/core";
|
|
682
|
+
import { markRange, type CustomTool } from "@olddevs/ui/editor";
|
|
683
|
+
|
|
684
|
+
const Comment = Mark.create({ name: "acme_comment" });
|
|
685
|
+
|
|
686
|
+
export const commentTool: CustomTool = {
|
|
687
|
+
name: "acme_comment",
|
|
688
|
+
engine: [Comment],
|
|
689
|
+
marks: { acme_comment: {} },
|
|
690
|
+
textCards: [
|
|
691
|
+
{
|
|
692
|
+
// Строка или функция, которая получает { locale }: имя Карточки и часть фразы для скринридера.
|
|
693
|
+
label: ({ locale }) => (locale.startsWith("ru") ? LABELS.ru : "Comment"),
|
|
694
|
+
// Диапазон под кареткой; для Метки — готовый помощник кита.
|
|
695
|
+
rangeAt: markRange("acme_comment"),
|
|
696
|
+
render: ({ editor, range, close }) => (
|
|
697
|
+
<div>
|
|
698
|
+
<p>{editor.state.doc.textBetween(range.from, range.to)}</p>
|
|
699
|
+
<button type="button" onClick={close}>
|
|
700
|
+
Resolve
|
|
701
|
+
</button>
|
|
702
|
+
</div>
|
|
703
|
+
),
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
};
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Запись (`CustomTextCard`).**
|
|
710
|
+
|
|
711
|
+
| Поле | Что это |
|
|
712
|
+
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
713
|
+
| `label` | Подпись: строка или функция, которая получает `{ locale }`. Доступное имя Карточки (`role="dialog"`) и часть фразы, которой кит сообщает скринридеру о появлении Карточки. Иконки у записи нет. Обязательно |
|
|
714
|
+
| `rangeAt` | `(state, pos) => { from, to } \| null` — диапазон, к которому привязана Карточка, если позиция `pos` (каретки или щелчка) лежит в нём; нет — `null`. Обязательно |
|
|
715
|
+
| `render` | `({ editor, range, close }) => ReactNode` — тело Карточки. `range` — диапазон из `rangeAt` на текущем состоянии (он едет за правкой), `close` прячет Карточку тем же путём, что `Escape`. Обязательно |
|
|
716
|
+
|
|
717
|
+
**`markRange(markName)`** — готовый `rangeAt` для Метки: непрерывный отрезок текста с этой Меткой и теми же значениями её атрибутов вокруг `pos`. **Края входят**: каретка сразу за словом или перед ним — ещё в диапазоне (иначе щелчок по первой букве не открывал бы Карточку); между двумя отрезками берётся тот, что кончается в позиции `pos`: так набираемый дальше текст продолжает этот отрезок. Для Блока или любого другого диапазона пишите свой `rangeAt` — он читает только `state` и вызывается на каждой транзакции, поэтому должен быть быстрым и без побочных эффектов. Кит отбрасывает диапазон, который выходит за Документ, имеет `from > to` или состоит не из чисел; исключение в `rangeAt` не роняет поле, а пишет `console.error` в разработке. `markRange` есть и в лёгком входе `@olddevs/ui/editor/schema`: описание своего Инструмента с Карточкой — это данные: их читает и серверная очистка, поэтому модулю описания не нужны ни Tiptap, ни React.
|
|
718
|
+
|
|
719
|
+
- **Когда открыта.** Каретка (свёрнутое выделение) внутри диапазона или щелчок по нему. **Наведением Карточка не открывается.** Развёрнутое выделение Карточку закрывает: там работает Меню выделения. Диапазон пересчитывается на каждой транзакции: Карточка едет за ним при правке и закрывается, когда он исчез. Поле вне фокуса (кроме `readOnly`) Карточку не показывает, `disabled` и HTML-режим — тоже.
|
|
720
|
+
- **Одна Карточка.** Открыта не больше одной: выигрывает **самый узкий диапазон** под кареткой, при равной ширине — запись, стоящая раньше, то есть Инструмент раньше в `tools` (внутри Инструмента — раньше в массиве).
|
|
721
|
+
- **Карточку держит кит.** Слой и стек Escape — как у `Popover` кита (ADR-0041: `Escape` закрывает Карточку, а не `Dialog` или `Drawer` вокруг поля); Карточка крепится под диапазоном, **переворачивается над диапазоном у нижнего края окна**, сдвигается от боковых краёв и выглядит как поповер кита. Заголовка и кнопок, заданных данными, нет: тело — ваш React-узел; монтируется заново на каждое открытие.
|
|
722
|
+
- **Немодальна.** Фокус остаётся в Документе, можно продолжать вводить текст, Карточка едет за диапазоном. **`Alt+F10` ведёт в открытую Карточку** (на первый фокусируемый элемент тела, иначе на саму Карточку) — как при выделенной картинке ведёт в панель картинки. **`Escape` возвращает каретку на место** и прячет Карточку.
|
|
723
|
+
- **Спрятана.** `Escape` и `close` прячут Карточку, **пока каретка не покинет диапазон**: правка и движение каретки внутри него её не возвращают. Щелчок по диапазону открывает её снова. Пока она спрятана, `Alt+F10` ведёт в Панель, как без Карточки.
|
|
724
|
+
- **Скринридер.** О появлении Карточки кит сообщает фразой каталога сообщений `editor.textCardShown` («Comment card opened. Press Alt+F10 to move into it.»); в русской локали фраза другая: «Открыта карточка «{подпись}». {сочетание} — перейти в неё.» В неё подставляются подпись записи и сочетание. Живой регион `role="status"` стоит в каждом Редакторе с Карточками и пуст, пока открытой нет.
|
|
725
|
+
- **`readOnly`.** Карточки работают — читать комментарии можно и без правки; правки из `render` режет сам движок. Каретки у такого поля нет, поэтому Карточку открывает **щелчок** (место щелчка едет за правкой), закрывают щелчок мимо поля, `Escape` и выделение мышью. С клавиатуры в `readOnly` Карточку не открыть: Документ не принимает каретку, а `Alt+F10` из него недостижим. При `disabled` Карточек нет.
|
|
726
|
+
- **Выключенный Инструмент** убирает и свои Карточки. В `RichTextView` Карточек нет: он показывает очищенный HTML без движка; читать текст с Карточками — Редактор с `readOnly`.
|
|
727
|
+
- **Проверка набора `tools`** добавляет ошибки поля: `textCards` не массив или запись не объект, нет подписи, `rangeAt` или `render`.
|
|
728
|
+
|
|
729
|
+
## Блок кода
|
|
730
|
+
|
|
731
|
+
Инструмент `codeBlock`: пункт-флажок «Блок кода» меню «…» и пункт Меню вставки, `Ctrl/⌘+Alt+C` и ограда ` ``` ` в начале строки. У Блока есть язык и подсветка синтаксиса — решение ADR-0051: lowlight с грамматиками highlight.js, только во входе `@olddevs/ui/editor` (`CodeView` кита по-прежнему без подсветки, ADR-0039).
|
|
732
|
+
|
|
733
|
+
**Языки.** Обычный текст (без языка), Python, TypeScript, JavaScript, Go, HTML, JSON, YAML и Markdown — данные в одном модуле `src/editor/features/code/codeLanguages.ts`, имена в Документе — `RICH_TEXT_SCHEMA.codeLanguages`. В HTML язык — класс на `<code>`: `<pre><code class="language-python">…</code></pre>`, у Блока без языка класса нет. Подсветка в Документ **не пишется**: токены `span class="hljs-…"` — декорации Редактора, в `getHTML()` их нет.
|
|
734
|
+
|
|
735
|
+
- **Псевдонимы** приводятся к имени при загрузке `value`, вставке HTML, разборе Markdown и в ограде: `py` → `python`, `ts` → `typescript`, `js` → `javascript`, `golang` → `go`, `htm` → `html`, `yml` → `yaml`, `md` → `markdown`; регистр не важен. У JSON псевдонимов нет: ` ```jsonc ` и ` ```json5 ` несут комментарии и хвостовые запятые, которых грамматика JSON не знает, и открывают обычный текст. ` ```py ` и пробел открывают Блок Python.
|
|
736
|
+
- **Неизвестный язык** (` ```rust `, `class="language-rust"`) становится обычным текстом: Блок остаётся, класс — нет.
|
|
737
|
+
- **Обычный текст не подсвечивается**, даже если похож на код: язык не угадывается ни в Редакторе, ни на сервере, поэтому один и тот же Блок выглядит одинаково в правке и при показе.
|
|
738
|
+
- **Markdown**: ограда с языком ↔ Блок с языком; `getMarkdown()` пишет имя (` ```python `), а не псевдоним, с которым Блок пришёл.
|
|
739
|
+
|
|
740
|
+
**Шапка Блока.** Над кодом — тонкая полоса (около 28 px) с нижней линией из токена разделителя: слева язык, справа «Копировать». Обе части всегда на виду — и на десктопе, и на сенсорном экране, без наведения. Код начинается сразу под полосой с обычным отступом Блока, кнопки его не перекрывают: длинная первая строка прокручивается целиком. Полоса — служебный элемент интерфейса Блока, не содержимое: у неё `contenteditable="false"` и `select-none`, каретка в неё не встаёт, в выделение и в копию Документа она не попадает, а в `getHTML()` её нет. Та же полоса — у Блоков в [`RichTextView`](../content/rich-text-view.md#кнопка-копировать-и-шапка).
|
|
741
|
+
|
|
742
|
+
**Выбор языка.** Слева в шапке — кнопка с текущим языком и шевроном («Python», «Обычный текст»). Она открывает `Menu` кита со списком языков — радиогруппа, текущий отмечен; выбор меняет язык **одним шагом отмены** (`Ctrl/⌘+Z` возвращает прежний, код не трогает) и возвращает фокус в код на прежнее место. Имя кнопки — «Language: Python» / «Язык: Python» (`messages.editor.codeLanguage`), меню называется так же. Меню — оверлей кита на `react-aria-components` и участвует в стеке слоёв (ADR-0041): внутри `Dialog` `Escape` закрывает меню, а не окно. В `readOnly` и `disabled` вместо кнопки — подпись с языком: менять нечего, а знать, на каком языке код, по-прежнему полезно. У Блока без языка подписи в этих режимах нет — строка «Обычный текст» живёт в каталоге сообщений и нужна кнопке меню; так же ведёт себя `RichTextView`, у которого каталога сообщений нет.
|
|
743
|
+
|
|
744
|
+
**Копирование.** Справа в шапке — `CopyButton` кита (иконка, без подписи) с подсказкой «Copy code» / «Копировать код» (`messages.editor.codeCopy`), которая на две секунды после нажатия сменяется на «Copied» / «Скопировано». Имя кнопки и объявление результата в живой области — штатные для `CopyButton`: «Copy» → «Copied» / «Копировать» → «Скопировано» (`messages.copyButton`), отдельного имени для блока кода не заведено. В буфер уходит **текст Блока строкой** (`node.textContent`): как набран, без токенов подсветки (они — декорации) и без разметки. Это правило ADR-0039: копируется строка, а не DOM. Кнопка есть **в любом режиме** — и при `readOnly`, и при `disabled`: править нельзя, скопировать можно. Отказ буфера (небезопасный контекст, запрет разрешений) кнопка показывает «Could not copy» / «Не удалось скопировать» и иконку «скопировано» не ставит.
|
|
745
|
+
|
|
746
|
+
**Клавиатура.** Обе кнопки шапки — **вне обхода Tab** (`tabindex="-1"`): иначе каждый блок кода добавлял бы две остановки между полем и следующим элементом формы. Попасть в шапку можно тем же `Alt+F10`: с кареткой в блоке кода он ведёт в кнопку языка этого Блока (как на выделенной картинке — в панель картинки), а не в Панель. Путь по шапке:
|
|
747
|
+
|
|
748
|
+
- `Alt+F10` с кнопки языка — на «Копировать», ещё раз — в Панель; так сочетание обходит язык → копирование → Панель;
|
|
749
|
+
- стрелка вправо с языка — на «Копировать», стрелка влево — обратно;
|
|
750
|
+
- на кнопке языка `Enter`, `Space` или стрелка вниз открывают меню; стрелки, `Home`, `End` и первая буква — выбор; `Enter` — применить, `Escape` — закрыть меню;
|
|
751
|
+
- на «Копировать» `Enter` или `Space` копируют;
|
|
752
|
+
- `Escape` на любой из двух кнопок — фокус обратно в код, каретка там же, где была.
|
|
753
|
+
|
|
754
|
+
Кнопка языка объявляет `aria-keyshortcuts="Alt+F10"`. **В `readOnly` и `disabled` каретки нет, и `Alt+F10` из Блока недостижим, поэтому там «Копировать» стоит в обычном порядке Tab** (в Редакторе, где можно править, её `tabindex` — `-1`, в остальных режимах — штатный). Tab в блоке кода по-прежнему уводит фокус из поля: отступы в коде пишутся пробелами.
|
|
755
|
+
|
|
756
|
+
**Тема подсветки** — готовые темы highlight.js: GitHub Light в теме `light`, GitHub Dark в `dark` и `dim`. Устроена из двух половин. Цвета — переменные `--rte-hl-*` и `--rte-code-block-*` в `styles.css` на элементах с `data-theme` (и на `:root` без атрибута), поэтому блок кода берёт тему **ближайшего предка с `data-theme`**: Документ во вложенном `data-theme="light"` внутри тёмного приложения получает GitHub Light, тёмная область на светлой странице — GitHub Dark. Правила `.hljs-*` — классы Tailwind на контейнере Документа (`src/editor/styles/codeTheme.ts`, часть `prose.ts`), они читают только переменные, поэтому ни `style`, ни отдельный CSS не нужны: правила приходят с `@import "@olddevs/ui/styles.css"` и строкой `@source` из [getting-started.md](../../getting-started.md). Переменные объявлены с нулевой специфичностью (`:where`), так что потребитель перекрывает любую своим правилом. Блок кода берёт фон и основной цвет темы подсветки (`#ffffff` / `#0d1117`), а не подложку кита: цвета GitHub держат контраст 4.5:1 только на своём фоне. Одно отступление от GitHub Light — `built_in` и `symbol` темнее (`#953800` вместо `#e36209`: у того 3.49:1 на белом). Это вторая система цвета в ките, и живёт она только в блоке кода Редактора; строчный `code` остаётся на токенах.
|
|
757
|
+
|
|
758
|
+
**Показ.** `RichTextView` подсветку не делает сам — иначе lowlight ехал бы в каждый показ Документа. Сохранённый HTML подсвечивается на сервере хелпером `highlightCodeBlocks(html)` из `@olddevs/ui/editor/highlight` **после очистки** — порядок на странице [RichTextView](../content/rich-text-view.md#подсветка-блоков-кода).
|
|
759
|
+
|
|
760
|
+
## Картинки
|
|
761
|
+
|
|
762
|
+
Инструмент `image` (в `full`, не в `basic`): кнопка «Картинка» на Панели, пункт «Картинка» в Меню вставки (`/img`) и Markdown `` при вставке. В Документ картинка пишется Блоком:
|
|
763
|
+
|
|
764
|
+
```html
|
|
765
|
+
<figure>
|
|
766
|
+
<img src="https://cdn.example.com/a.png" alt="Озеро" data-width="50" />
|
|
767
|
+
<figcaption>Подпись</figcaption>
|
|
768
|
+
</figure>
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
- **Поповер картинки** — поле адреса и поле альтернативного текста. Адрес пишется как в строке браузера: `cdn.example.com/a.png` становится `https://cdn.example.com/a.png`. **Принимаются только абсолютные `http` и `https`**: `data:`, `blob:`, `mailto:`, `javascript:` и относительный адрес поле не примет — под ним появится ошибка. `Enter` вставляет картинку на место пустого абзаца под кареткой (или рядом с текущим Блоком), и каретка уходит в подпись. `Escape` отменяет. Внутри `Dialog` `Escape` закрывает только поповер (ADR-0041).
|
|
772
|
+
- **Альтернативный текст** необязателен: пустой `alt=""` означает «декоративная картинка», скринридер её пропустит. Атрибут `alt` пишется всегда — без него скринридер читал бы имя файла из адреса.
|
|
773
|
+
- **Подпись** — `figcaption` под картинкой, с Метками и ссылками. Пустая подпись в HTML не пишется вовсе; в поле на её месте плейсхолдер «Подпись (необязательно)». `Enter` на выделенной картинке ставит каретку в подпись, `Enter` в подписи — новый абзац под картинкой, `Backspace` в начале подписи выделяет картинку. Заголовок, список и блок кода из подписи не получатся: внутри `figure` Схема держит только подпись.
|
|
774
|
+
- **Ширина** — один из трёх вариантов: 50, 75 или 100 % ширины Документа (`data-width`), без атрибута — свой размер картинки, но не шире Документа. Картинка стоит по центру. Мышью — ручки по бокам выделенной картинки: ширина меняется плавно, а при отпускании прилипает к ближайшему из трёх значений (одна правка — одна отмена). С клавиатуры — кнопки-переключатели «50 %», «75 %», «100 %» в поповере выделенной картинки и на её панели; нажатый вариант повторным нажатием снимается. `style` у картинки не бывает.
|
|
775
|
+
- **Выделенная картинка** (выделяется щелчком или стрелками) показывает над собой свою панель: варианты ширины, «Изменить картинку» (поповер с адресом и альтернативным текстом) и «Убрать картинку». `Alt+F10` на выделенной картинке ведёт в панель картинки, а не в Панель форматирования; плавающая Панель над картинкой не появляется. Кнопка «Картинка» Панели в этот момент нажата (`aria-pressed`) и открывает поповер этой картинки, а не вставку новой.
|
|
776
|
+
|
|
777
|
+
### Загрузка файлов: `onUploadImage`
|
|
778
|
+
|
|
779
|
+
```tsx
|
|
780
|
+
<RichTextEditor
|
|
781
|
+
value={html}
|
|
782
|
+
onChange={setHtml}
|
|
783
|
+
onUploadImage={async (file) => {
|
|
784
|
+
const body = new FormData();
|
|
785
|
+
body.set("file", file);
|
|
786
|
+
const response = await fetch("/api/uploads", { method: "POST", body });
|
|
787
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
788
|
+
return (await response.json()).url; // абсолютный https-адрес
|
|
789
|
+
}}
|
|
790
|
+
/>
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
- **С обработчиком загрузки (`onUploadImage`)** картинку можно вставить из буфера (снимок экрана, «Копировать картинку» в браузере), перетащить файлом в поле или выбрать кнопкой «Загрузить с устройства» в поповере. Несколько файлов загружаются по одному и встают в порядке файлов.
|
|
794
|
+
- **Пока промис не завершён**, на месте будущей картинки плейсхолдер «Загружается файл photo.png…» (с превью, где браузер умеет `blob:`), а в Документе картинки **нет**: ни в `getHTML()`, ни в `onChange`, ни в форме. Ход загрузки объявляет живой регион Сообщения поля (`role="status"`, см. «Строка статуса и Сообщение поля»).
|
|
795
|
+
- **Успех** — картинка встаёт на место плейсхолдера одной правкой: `Ctrl/⌘+Z` убирает именно её. **Отказ** (промис отклонён или адрес вне `http`/`https`, например `data:` или относительный) — плейсхолдер исчезает, Документ не меняется, в истории правок ничего нет, под полем строка `role="alert"`: «Не удалось загрузить файл photo.png. Документ не изменился.» Следующая правка её снимает. Это Сообщение поля: сообщения о файлах идут тем же каналом, что `showNotice`, и новое заменяет прежнее.
|
|
796
|
+
- **Без обработчика загрузки** файлы из буфера и перетаскивания не принимаются: под полем пояснение: вставить картинку по адресу. «Копировать картинку» из браузера при этом всё равно сработает — вместе с файлом в буфере лежит HTML с адресом картинки, и она вставится по адресу, если он разрешён политикой протоколов (`http`/`https`).
|
|
797
|
+
- **Что не загружается.** Файл не-картинка (`notes.pdf`) отклоняется с сообщением. Вставка из Word и со страниц, где рядом с файлом в буфере лежит HTML с текстом, идёт обычным разбором HTML: Word кладёт в буфер ещё и снимок выделения картинкой, и загружать его было бы ошибкой.
|
|
798
|
+
- Обработчик загрузки читается в момент вставки — его можно менять на лету. Отправка формы во время загрузки уйдёт без ещё не загруженной картинки.
|
|
799
|
+
- **Сигнала «картинка удалена из Документа» нет** (ADR-0058, раздел 13): правка, отмена и вырезание со вставкой дали бы ложные срабатывания, а Документ меняют и в обход Редактора. Загруженные файлы, на которые Документ больше не ссылается, находит сервер — [рецепт сравнения `src`](../../api/editor-extension.md#исчезнувшие-картинки-сравнение-src-на-сервере).
|
|
800
|
+
|
|
801
|
+
## Цвет, маркер и выравнивание
|
|
802
|
+
|
|
803
|
+
`color`, `highlight` и `align` входят в `preset="full"`, но не в `basic`. В Документ палитра пишется **именами, а не значениями CSS**: `<span data-color="danger">`, `<mark data-color="danger">`, `<p data-align="center">`. Атрибута `style` нет ни при загрузке, ни при вставке, ни в `getHTML()` — кроме одного узкого исключения, цвета автора (раздел «Свой цвет» ниже). Выравнивание из чужого `style` при этом не теряется: оно переводится в `data-align` (ниже). Конкретный цвет палитры подставляет тема (`dark`, `light`, `dim`), поэтому сохранённый текст читается в любой из них — и в Редакторе, и в [`RichTextView`](../content/rich-text-view.md): классы общие (`prose.ts`).
|
|
804
|
+
|
|
805
|
+
**Палитр две, и общих имён у них нет.** Цвет текста — шесть тонов статуса кита, маркер — девять цветов: три ярких (жёлтый, зелёный, розовый) и шесть приглушённых цветов Notion. Оба пишутся в `data-color`, но на разных тегах: `span[data-color]` берёт имя из палитры текста, `mark[data-color]` — из палитры маркера; имя маркера на `span` отбрасывается (`<span data-color="yellow">` вместе с тегом), а шесть прежних имён цвета текста на `mark` читаются как ближайший цвет маркера (ниже).
|
|
806
|
+
|
|
807
|
+
**Цвет текста** (`span`, сетка «Цвет текста» поповера «Цвет»). `strong`-ступень тона:
|
|
808
|
+
|
|
809
|
+
| Имя `data-color` | Кружок (en / ru) | Цвет текста |
|
|
810
|
+
| ---------------- | ------------------ | ---------------- |
|
|
811
|
+
| `accent` | Accent / Акцентный | `accent-strong` |
|
|
812
|
+
| `success` | Green / Зелёный | `success-strong` |
|
|
813
|
+
| `warning` | Amber / Жёлтый | `warning-strong` |
|
|
814
|
+
| `danger` | Red / Красный | `danger-strong` |
|
|
815
|
+
| `lime` | Lime / Лаймовый | `draft-strong` |
|
|
816
|
+
| `muted` | Gray / Серый | `fg-2` |
|
|
817
|
+
|
|
818
|
+
**Маркер** (`mark`, кнопка «Маркер» и сетка «Цвет маркера» поповера «Цвет»). Яркий, как у настоящего текстовыделителя: три ярких цвета **одинаковы во всех темах** (`dark`, `light`, `dim`) и на Листе `surface="paper"`, а **текст на трёх ярких маркерах всегда тёмный** — токен `--marker-text` (`#1a1a1a`). Значения — токены `--marker-<имя>` компонентного слоя ([theming.md](../../theming.md)), по теме их подставляет `styles.css`.
|
|
819
|
+
|
|
820
|
+
**Все девять — в сетке** (решение владельца), по оттенку, как в Notion: серый, коричневый, оранжевый, жёлтый, зелёный, синий, фиолетовый, розовый, красный. Прежде меню предлагало только три ярких, а шесть приглушённых цветов Notion продолжали работать: Схема, очистка и `RichTextView` их принимали и показывали, но выбрать их в меню было нельзя. Различаются они цветом текста на маркере, а не поддержкой: все девять принимаются одинаково:
|
|
821
|
+
|
|
822
|
+
Яркие:
|
|
823
|
+
|
|
824
|
+
| Имя `data-color` | Кружок (en / ru) | Токен | Все темы | Контраст тёмного текста (`--marker-text`) |
|
|
825
|
+
| ---------------- | ---------------- | ----------------- | --------- | ----------------------------------------- |
|
|
826
|
+
| `yellow` | Yellow / Жёлтый | `--marker-yellow` | `#ffea80` | 14.4 |
|
|
827
|
+
| `green` | Green / Зелёный | `--marker-green` | `#aaff80` | 14.4 |
|
|
828
|
+
| `pink` | Pink / Розовый | `--marker-pink` | `#ff80bf` | 7.5 |
|
|
829
|
+
|
|
830
|
+
Приглушённые цвета Notion (значения свои у каждой темы; текст на них — основной текст темы, а не тёмный):
|
|
831
|
+
|
|
832
|
+
| Имя `data-color` | Название (en / ru) | Токен | `light` | `dark` и `dim` | Контраст основного текста, худшая из тем |
|
|
833
|
+
| ---------------- | ------------------- | ----------------- | --------- | -------------- | ---------------------------------------- |
|
|
834
|
+
| `blue` | Blue / Синий | `--marker-blue` | `#d3e5ef` | `#28456c` | 8.93 |
|
|
835
|
+
| `gray` | Gray / Серый | `--marker-gray` | `#e3e2e0` | `#5a5a5a` | 6.33 |
|
|
836
|
+
| `brown` | Brown / Коричневый | `--marker-brown` | `#eee0da` | `#603b2c` | 8.92 |
|
|
837
|
+
| `orange` | Orange / Оранжевый | `--marker-orange` | `#fadec9` | `#854c1d` | 6.32 |
|
|
838
|
+
| `purple` | Purple / Фиолетовый | `--marker-purple` | `#e8deee` | `#492f64` | 10.28 |
|
|
839
|
+
| `red` | Red / Красный | `--marker-red` | `#ffe2dd` | `#6e3630` | 8.63 |
|
|
840
|
+
|
|
841
|
+
Имена `yellow`, `green` и `pink` те же, что были: **сохранённые Документы с ними сами становятся ярче** — значение принадлежит теме, а не Документу. Имя кнопки «Маркер» называет текущий цвет («Маркер: Синий»).
|
|
842
|
+
|
|
843
|
+
Кружок красят тем же токеном; имя цвета у кружка — для скринридера (`aria-label`), глазами цвет узнают по кружку.
|
|
844
|
+
|
|
845
|
+
- **Контраст проверен во всех темах** (`npm run contrast`, пары заведены из `src/editor/styles/palette.ts`): цвет текста держит 4.5:1 на всех подложках приложения (худшие значения — `accent` и `danger` около 4.6); тёмный текст `--marker-text` на каждом из трёх ярких маркеров — не менее 4.5:1 (7.5:1 на розовом, 14.4:1 на жёлтом и зелёном) в `dark`, `light`, `dim` и у бренда; основной текст `fg-0` на шести приглушённых — тоже (худший — `orange`, 6.32). Цветной текст поверх маркера гейт не меряет: сочетание выбирает автор.
|
|
846
|
+
- **Маркер следует за ближайшей темой.** Токены `--marker-*` объявлены на `[data-theme]` любого элемента, а не только на корне, и классы `prose.ts` читают саму переменную (`bg-(--marker-yellow)`), без моста `@theme` и без правил под тему на потомках. Область Документа во вложенном контейнере с `data-theme="light"` внутри тёмного приложения получает светлые значения приглушённых цветов; приложение вокруг неё — свои. Три ярких цвета и тёмный текст от темы не зависят. Остальные токены кита (`--fg-0`, поверхности) объявлены на `:root[data-theme]` и вложенную тему не понимают — Лист `surface="paper"` обходит это ролями `--rte-*` (раздел «Поверхность»).
|
|
847
|
+
- **Старые имена маркера переводятся.** До палитры Notion маркер делил имена с цветом текста. Сохранённые тогда `<mark data-color="accent">` и ещё пять читаются при загрузке (`value`, `defaultValue`) и вставке как ближайший новый цвет, и `getHTML()` отдаёт уже новое имя; `RichTextView` красит старое имя тем же цветом. Соответствие определяется по оттенку подложки, а не по смыслу тона:
|
|
848
|
+
|
|
849
|
+
| Было (`mark`) | Стало | Почему |
|
|
850
|
+
| ------------- | -------- | ----------------------------------------------------------------------------------------------------- |
|
|
851
|
+
| `accent` | `blue` | акцент кита по умолчанию синий |
|
|
852
|
+
| `success` | `green` | одноимённый цвет |
|
|
853
|
+
| `warning` | `yellow` | янтарь `warning-soft` ближе к жёлтому, чем к оранжевому |
|
|
854
|
+
| `danger` | `red` | одноимённый цвет |
|
|
855
|
+
| `lime` | `green` | подложка лайма читается зелёной; жёлтый занят `warning`, и «черновик» не должен стать предупреждением |
|
|
856
|
+
| `muted` | `gray` | одноимённый цвет |
|
|
857
|
+
|
|
858
|
+
Сохранённый Документ меняет вид: оттенки теперь Notion-подобные, а не `soft`-ступени тонов. Сами имена в базе потребителя переписывать не обязательно — Редактор и `RichTextView` читают оба написания; на сервере их переводит `sanitizeRichText` ([`RichTextView`](../content/rich-text-view.md#санитизация--на-сервере-потребителя)).
|
|
859
|
+
|
|
860
|
+
- **`accent` текста — цвет акцента приложения**, а не фиксированный синий: после смены акцента или бренда он меняется вместе с остальным интерфейсом. Маркер `blue` от акцента и бренда не зависит. Остальные имена текста от акцента не зависят.
|
|
861
|
+
- **Имена вне своей палитры отбрасываются** вместе с тегом, текст остаётся: `data-color="hotpink"`, `data-color="#ff0000"`, имя маркера на `span`, имя в другом регистре (`Yellow`), маркер без `data-color` или `style` (голый `<mark>` браузер красит жёлтым, и в тёмной теме он светится поверх Документа). Цвет из `style="color: …"` во вставке (Word, Google Docs) **не переводится** в ближайший цвет палитры, а `style` проходит только в строгой форме цвета автора — иначе пропадает: перевод угадывал бы за автора.
|
|
862
|
+
- **Код без цвета.** Строчный код не совмещается с цветом и маркером: Метка `code` исключает остальные.
|
|
863
|
+
- **Выравнивание** — абзац и заголовки H1–H4 (H1 — с `heading1`; в том числе абзац внутри списка или цитаты): `data-align="center"` или `"right"`. «По левому краю» — умолчание, атрибут не пишется, а `data-align="left"` из чужого HTML читается как его отсутствие. **Выравнивание из чужого HTML** (`value`, `defaultValue` и вставка из Word, Excel, Google Docs) читается и в виде `style="text-align: center|right"` (регистр и пробелы любые) и устаревшего атрибута `align="center|right"` на `p` и `h1`–`h4`: оно становится `data-align`, а сами `style` и `align` в Документ не попадают. `left`, `start` и `justify` дают «нет атрибута». Без Инструмента `align` такое выравнивание отбрасывается вместе со `style`. «По ширине» (`justify`) в v1 нет намеренно: без переносов по слогам текст рвётся пробелами-«реками», а WCAG (1.4.8) советует его не использовать. Выравнивание физическое (лево и право), а не логическое: для Документов с письмом справа налево набор придётся расширять.
|
|
864
|
+
- **Выключенный Инструмент** убирает атрибут из Схемы: `data-color`, `style` цвета автора и `data-align` из вставки и из `value` отбрасываются, текст остаётся.
|
|
865
|
+
- **Markdown** не выражает цвет, маркер и выравнивание: `getMarkdown()` оставляет текст, а `==текст==` разметкой маркера не становится (иначе он появлялся бы без цвета).
|
|
866
|
+
|
|
867
|
+
### Свой цвет
|
|
868
|
+
|
|
869
|
+
Кроме цветов палитры автор выбирает любой другой: «Свой цвет…» (`editor.colorCustom`) у заголовка каждой сетки поповера «Цвет» открывает поповер кита с [`ColorPicker`](./color-picker.md): кнопка с образцом и значением раскрывает область насыщенности, полосу тона и поле HEX; ниже — предупреждение о контрасте, «Применить» и «Отмена». Поповер встаёт у выделения, как поповер ссылки. Если на выделении уже цвет автора, поповер открывается на нём, а «Свой цвет…» в сетке отмечен; имя кнопки «Маркер» называет значение маркера автора: «Маркер: Свой #ffe066». После «Применить», «Отмена» или `Escape` фокус — в Документе.
|
|
870
|
+
|
|
871
|
+
**Как цвет хранится.** Строго `style` и ничего другого, на `span` для текста и на `mark` для маркера:
|
|
872
|
+
|
|
873
|
+
```html
|
|
874
|
+
<span style="color:#00aa55">текст</span> <mark style="background-color:#ffe066">маркер</mark>
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
Буквы в нижнем регистре, ровно шесть цифр, без пробелов и `;`; никаких других свойств в `style` и никогда вместе с `data-color` на одном элементе — цвет у Метки один, палитры или автора, и новый выбор заменяет прежний. Это **единственное исключение** из правила «в Документе нет `style`»: у цвета автора нет имени, а носитель значения в HTML — `style`. Исключение принято владельцем и описано в Схеме (`RICH_TEXT_SCHEMA.customColor`), а `sanitizeRichText` ([`RichTextView`](../content/rich-text-view.md#санитизация--на-сервере-потребителя)) проверяет его хуком.
|
|
878
|
+
|
|
879
|
+
**Что принимается из чужого HTML** (`value`, вставка): только это объявление, одно на элемент, на `span` — `color`, на `mark` — `background-color`; регистр букв и пробелы вокруг приводятся к записи Редактора (`#00AA55` → `#00aa55`). Всё остальное — `#abc`, `rgb(0, 170, 85)`, `red`, `var(--x)`, `!important`, второе объявление рядом (`color:#00aa55;font-size:40px` из Google Docs), `style` на другом теге — отбрасывается вместе с тегом, текст остаётся. Преобразование `rgb()` в `#rrggbb` технически просто, но расширило бы разбор форматами, которые должен знать и санитайзер потребителя, а угадывание за автора кит отвергает и для палитры. Цвет, скопированный из самого Редактора и вставленный обратно, проходит без потерь.
|
|
880
|
+
|
|
881
|
+
**Как `style` пишется в DOM.** Редактор ставит атрибут через `setAttribute`, а не через `style.cssText`, как делает ProseMirror для массива-описания. Разница существенная: браузер пересобирает `cssText` в свою запись (`color: rgb(0, 170, 85);`), и Документ при следующей загрузке не прошёл бы строгий разбор.
|
|
882
|
+
|
|
883
|
+
**Контраст — предупреждение.** Поповер меряет цвет по текущей теме: фон и основной текст Редактора — `--rte-background` и `--rte-foreground` на Документе. Для текста — сам цвет против фона, для маркера — **тёмный текст на маркере** (`--marker-text`), а не основной текст темы: на ярком маркере и на цвете автора текст тёмный. Порог — 4.5:1 (WCAG 1.4.3). Ниже порога в живой области поповера (`role="status"`) появляется строка с числом («Низкий контраст: 2.1:1 с фоном редактора (нужно 4,5:1)»); цвет всё равно применяется по «Применить». Если тему прочитать не удалось (нет стилей), предупреждения нет: «не измерено» не значит «плохо».
|
|
884
|
+
|
|
885
|
+
**Цвет автора не следует за темой.** Палитра — токены, и один сохранённый текст читается в `dark`, `light` и `dim`. `#ffff00` одинаков во всех темах: жёлтый текст, читаемый на тёмном фоне, на светлом сливается. Редактор предупреждает при выборе и сообщает об этом в поповере (`editor.customColorThemeNote`), но цвет не пересчитывает и не подменяет. Для Документов, которые показываются в разных темах, берите палитру.
|
|
886
|
+
|
|
887
|
+
**Размер.** `ColorPicker` тянет цветовую подсистему `react-aria-components` — 61 кБ gzip (ADR-0038), столько же, сколько весь кит, поэтому поповер грузится лениво (`React.lazy`) при первом «Свой цвет…». Кто его не открывает, этих килобайт не платит; в `@olddevs/ui/editor` они не включены, но сборщик потребителя вынесет их в отдельный чанк.
|
|
888
|
+
|
|
889
|
+
## Таблицы
|
|
890
|
+
|
|
891
|
+
Инструмент `table` входит в `preset="full"`, не в `basic`. Нужен пакет `@tiptap/extension-table` (в команде установки выше).
|
|
892
|
+
|
|
893
|
+
**Одна кнопка «Таблица ▾» на Панели — и вставка, и действия**, как у TinyMCE. Иконка таблицы с шевроном, `aria-haspopup="menu"`; кнопка есть всегда, и вне таблицы пункты, которым там нечего делать, недоступны (`aria-disabled`), но остаются в обходе стрелками — руководство ARIA Authoring Practices (APG) советует так, чтобы стрелка не перепрыгивала пункт молча. Доступен ли пункт, решает пробный прогон его команды движка, а не `editor.can()`: тот отвечает «да» на удаление единственной строки, которая потом молча не удалится.
|
|
894
|
+
|
|
895
|
+
| Пункт | Вне таблицы | Что делает |
|
|
896
|
+
| ----------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
897
|
+
| Таблица › | доступен | Подменю с сеткой размера (до 10×10): выбор вставляет таблицу N×M со строкой-заголовком, меню закрывается, каретка — в первой ячейке новой таблицы. Выбор в сетке стоит на **последнем выбранном размере этого Редактора** (сначала 3×3); сама сетка показывает не меньше 5×5 ячеек |
|
|
898
|
+
| Вставить таблицу… | доступен | Окно (`Dialog` кита) с полями «Столбцы» и «Строки» — до 20×50, начальные значения — последний размер; `Enter` в любом поле вставляет, «Отмена» и `Escape` закрывают без таблицы. Размер из окна запоминается так же, как из сетки |
|
|
899
|
+
| Ячейка › | недоступен | «Объединить ячейки» — доступно при выделении нескольких ячеек (мышью поперёк ячеек или `Shift` со стрелками); «Разделить ячейку» — на слитой ячейке |
|
|
900
|
+
| Строка › | недоступен | «Вставить строку выше / ниже», «Удалить строку» (у таблицы из одной строки недоступно — есть «Удалить таблицу»), «Строка-заголовок» — переключатель (`menuitemcheckbox`): первая строка из `th` ↔ `td` |
|
|
901
|
+
| Столбец › | недоступен | «Вставить столбец слева / справа», «Удалить столбец»; затем группа радио (`menuitemradio`) «Столбец по левому краю / по центру / по правому краю» — выравнивание столбца под кареткой. Отмечено общее выравнивание столбца; у смешанного не отмечено ничего |
|
|
902
|
+
| Сортировка › | недоступен | «По возрастанию», «По убыванию» — по столбцу под кареткой (правила ниже). В таблице со слитыми ячейками оба пункта недоступны, и подпись раздела подменю объясняет, почему |
|
|
903
|
+
| Удалить таблицу | недоступен | Убирает таблицу целиком; пункт красный (`danger`) и отделён разделителем |
|
|
904
|
+
|
|
905
|
+
Вставка из меню и из окна встаёт новым Блоком после текущего, а в пустом абзаце — на его место; изнутри таблицы — **после** неё: вложенных таблиц Редактор не строит. Пункт «Таблица» Меню вставки (`/table`, `/grid`, «+») по-прежнему сразу вставляет 3×3, без выбора размера и без памяти о нём.
|
|
906
|
+
|
|
907
|
+
**Клавиатура.** Чтобы открыть меню, нажмите `Alt+F10` (фокус перейдёт в Панель), стрелками дойдите до «Таблица ▾» и нажмите `Enter` или `↓`.
|
|
908
|
+
|
|
909
|
+
| Клавиша | Где | Действие |
|
|
910
|
+
| ----------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
911
|
+
| `↓` / `↑` | меню, подменю | Следующий / предыдущий пункт, недоступные — тоже |
|
|
912
|
+
| `→` | пункт с `›` | Открыть подменю; фокус — на первый пункт, а у «Таблица ›» — в сетку, когда подменю встало |
|
|
913
|
+
| `←` | подменю | Закрыть подменю, фокус — обратно на его пункт. В сетке `←` двигает выбор, а не закрывает подменю: горизонтальные стрелки принадлежат сетке |
|
|
914
|
+
| `←` `→` `↑` `↓`, `Home` `End` | сетка | Выбор размера, сетка растёт до 10×10 (клавиши сетки — в абзаце «Сетка выбора размера» ниже) |
|
|
915
|
+
| `Enter` | пункт, сетка | Выполнить; действие возвращает фокус в Документ, каретка остаётся в той ячейке, откуда пришли |
|
|
916
|
+
| `Escape` | любой уровень | Закрыть **только верхний** уровень: сетку или подменю → меню → кнопка. В `Dialog` окно закрывает лишь следующий `Escape` (ADR-0041) |
|
|
917
|
+
|
|
918
|
+
Фокус в сетку ставится, когда поповер подменю сам сообщил, что встал: место посчитано и анимация входа доиграна (render props `placement` и `isEntering` из react-aria). Сетка, взявшая фокус сразу при монтировании, ломала меню внутри окна, а прежний таймер на 50 мс был гонкой с устройством; сигнал поповера от скорости устройства не зависит. Подменю, открытое мышью, фокус у пункта оставляет — сетку двигает наведение.
|
|
919
|
+
|
|
920
|
+
**Слияние ячеек.** `colspan` и `rowspan` пишутся в HTML и переживают сохранение и загрузку; таблица из Word, Excel и Google Docs вставляется со своими слияниями. Значение — целое от 2 до предела Схемы (`RICH_TEXT_SCHEMA.tableSpans`: 20 по горизонтали, 50 по вертикали — столько же, сколько у самой большой вставляемой таблицы); ноль, дробное число, `2px`, отрицательное и число сверх предела читаются как 1 (ячейка остаётся, слияние распадается): `colspan="10000"` из чужого HTML иначе стал бы десятью тысячами пустых ячеек. Слияние, которое вышло бы за предел, «Объединить ячейки» не делает. Строка, целиком накрытая слиянием сверху, остаётся пустой `<tr></tr>`.
|
|
921
|
+
|
|
922
|
+
**Выравнивание столбца** — `data-align="center|right"` на **каждой** ячейке столбца (`th` и `td`), теми же значениями, что выравнивание Блока: `left` — умолчание, атрибута нет. Оформление — общие классы `prose.ts`, поэтому в `RichTextView` столбец выровнен так же. Абзац в ячейке со своим выравниванием (Инструмент `align`) сильнее столбца. Выравнивание чужой таблицы — `style="text-align: center|right"` или `align="center|right"` на `th` и `td` из Word, Excel, Google Docs или `value` — переводится в `data-align` ячейки (при включённом Инструменте `table`, независимо от `align`); сами `style` и `align` не пишутся. Из Markdown выравнивание приходит разделителем `:---:`.
|
|
923
|
+
|
|
924
|
+
**Сортировка** переставляет строки по столбцу под кареткой:
|
|
925
|
+
|
|
926
|
+
- строка-заголовок остаётся первой; таблица без неё сортируется целиком;
|
|
927
|
+
- столбец, где **все** непустые ячейки — числа, сравнивается как числа: разряды пробелом (в том числе неразрывным), апострофом, запятой или точкой, дробь запятой или точкой, минус, `%`. Где знак один и встречается раз, решает локаль интерфейса: `1,234` у `en` — тысяча двести тридцать четыре, у `ru` — одна целая с дробью; `1.2.3` — не число;
|
|
928
|
+
- столбец, где все непустые — даты ISO (`2026-10-06`, с временем через `T` или пробел) или `ДД.ММ.ГГГГ`, сравнивается как даты. `10/06/2026` не читается: порядок дня и месяца у локалей разный;
|
|
929
|
+
- иначе — текст через `Intl.Collator` локали интерфейса с `numeric`: «ё» на своём месте, «пункт 2» раньше «пункта 10»;
|
|
930
|
+
- пустые ячейки — в конце при любом направлении; порядок устойчивый (равные строки не перемешиваются); вся перестановка — **одна правка**, `Ctrl/⌘+Z` отменяет её целиком; каретка уезжает вместе со своей строкой;
|
|
931
|
+
- со слитыми ячейками сортировка недоступна: строку со слиянием нельзя переставить, не разрезав его.
|
|
932
|
+
|
|
933
|
+
**Прочие клавиши в Документе.**
|
|
934
|
+
|
|
935
|
+
- **`Tab` и `Shift+Tab`** — к следующей и предыдущей ячейке по порядку чтения (содержимое ячейки выделяется). **`Tab` в последней ячейке добавляет строку** и переходит в неё, как в Word, Google Docs и Notion; кит это поведение сохраняет. Цена: `Tab` не выводит из таблицы, как из остального Документа, — выйти можно стрелками (`↓`/`→` из последней ячейки ставят каретку после таблицы, в том числе когда она последний Блок Документа) или `Alt+F10` в Панель.
|
|
936
|
+
- **`Alt+↓`** (на macOS — `⌥↓`) — строка под текущей, каретка встаёт в тот же столбец новой строки; одна правка, `Ctrl/⌘+Z` убирает строку целиком. То же, что «Строка › Вставить строку ниже», и пункт меню показывает это сочетание. Работает только в таблице: вне неё сочетание остаётся браузеру. **На macOS внутри ячейки `⌥↓` не переводит каретку в конец абзаца**: это системное действие сочетание забирает. Для столбцов и для `Alt+↑` сочетаний нет — решение владельца.
|
|
937
|
+
- Выделение мышью поперёк нескольких ячеек подсвечивает их (`.selectedCell`, подложка выделения `--selection`); `Delete`/`Backspace` при выделении **всех** ячеек удаляет таблицу, при выделении части — очищает ячейки.
|
|
938
|
+
|
|
939
|
+
**Сетка выбора размера** (`src/editor/features/table/TableSizeGrid.tsx`, наружу не экспортируется) живёт в подменю «Таблица ›». Сетка открывается на 5×5 ячеек и растёт на столбец или строку, когда выбор доходит до её края, — до 10×10; уходит выбор назад — сетка сжимается. Верхняя строка закрашена и подчёркнута как заголовок (вставка всегда делает первую строку из `th`). Под сеткой — размер словами («3 столбца, 4 строки», склонение по локали). Числовых полей в меню нет — они в окне «Вставить таблицу…»: поле ввода внутри `Menu` react-aria закрывало бы меню, а `Tab` в меню не ходит. Сама сетка умеет поля (`fields`), это используют только её истории.
|
|
940
|
+
|
|
941
|
+
| Клавиша | Действие |
|
|
942
|
+
| --------------- | -------------------------------------------------------------------------- |
|
|
943
|
+
| `←` `→` `↑` `↓` | Сдвинуть выбор на ячейку; у края сетка растёт, у 10×10 останавливается |
|
|
944
|
+
| `Home` / `End` | Первый столбец / последний видимый столбец строки; с `Ctrl` — в углы сетки |
|
|
945
|
+
| `Enter` | Вставить таблицу выбранного размера |
|
|
946
|
+
| `Escape` | Закрыть сетку, фокус — на пункт «Таблица ›» |
|
|
947
|
+
|
|
948
|
+
ARIA сетки — шаблон `grid` из APG: `role="grid"` → `row` → `gridcell`. Фокус стоит на самой сетке, на текущую ячейку указывает `aria-activedescendant` (roving tabindex отвергнут: до ста остановок Tab вместо одной); `aria-selected` — на ячейках выбранного прямоугольника. Имя ячейки и живая область (`role="status"`, вежливая) содержат один и тот же текст — выбранный размер словами; наведение мыши двигает тот же выбор, что стрелки, но это не единственный путь.
|
|
949
|
+
|
|
950
|
+
**Что на выходе.** `<table><tbody><tr><th>…</th></tr><tr><td>…</td></tr></tbody></table>`, ячейка — `<th><p>…</p></th>`. У ячейки бывают только `colspan`, `rowspan` и `data-align`; ни `style`, ни `colwidth`, ни `class`, ни `scope`; `thead`, `colgroup` и `caption` не пишутся (строка-заголовок — обычная первая строка из `th`). Штатное расширение Tiptap пишет `style="min-width: …"` на таблице, `colgroup` с шириной столбцов, `colwidth` и `style="text-align"` на ячейках — кит это отключает (`src/editor/features/table/tables.ts`), а при загрузке и вставке такие атрибуты отбрасываются; выравнивание из них переходит в `data-align`.
|
|
951
|
+
|
|
952
|
+
- **Ширины столбцов не настраиваются.** Ручек изменения размера в v1 нет (параметр Tiptap `resizable: false`): ширину считает браузер по содержимому.
|
|
953
|
+
- **Строки с разным числом ячеек** (чужой HTML с ошибкой или со слиянием сверх предела) выравниваются пустыми ячейками при старте Редактора — без `onChange`: это не Правка автора.
|
|
954
|
+
- **Ячейка держит любые Блоки Схемы** (абзацы, списки, цитату, блок кода), а не только абзацы: если бы ячейка принимала один абзац, вставка списка разрезала бы таблицу надвое.
|
|
955
|
+
- **Выключенный `table`** убирает кнопку, пункт меню и расширение: таблица из вставки и из `value` приходит текстом ячеек, абзацами.
|
|
956
|
+
- **Прокрутка на узком экране.** Таблица шире блока прокручивается по горизонтали внутри себя (`display: block; overflow-x: auto` на `<table>`, `prose.ts`), страницу не раздвигает; минимальная ширина столбца — 6 rem. Контейнера вокруг таблицы нет: `RichTextView` кладёт сохранённый HTML как есть, и прокрутка должна работать на голой `<table>`. Подробности и цена — на странице [RichTextView](../content/rich-text-view.md#таблицы).
|
|
957
|
+
|
|
958
|
+
Живые примеры — истории `TableMenu`, `TableMerged`, `TableSortAndAlign`, `TableRussian`, `TableNarrow`, `TableInsertSlash` и сетка отдельно — `TableSizeGrid`, `TableSizeGridFields`, `TableSizeGridRussian`.
|
|
959
|
+
|
|
960
|
+
## Меню вставки
|
|
961
|
+
|
|
962
|
+
Список Блоков, который открывается **по `/` в тексте** и **по кнопке «+»**, которая стоит первой на Панели (Инструмент `insertMenu`, есть в `full`). Пункты двух разделов:
|
|
963
|
+
|
|
964
|
+
- **«Добавить»** — новый Блок после текущего: блок кода, разделитель, таблица, картинка, Раскладка (две колонки; остальные варианты — у Ручки Блока), Обёртка. Картинка открывает поповер адреса и встаёт на место подготовленного пустого абзаца. Если каретка стоит в пустом абзаце, Блок встаёт на его место; иначе — новым Блоком сразу за текущим верхним Блоком, не внутри цитаты или списка; при выделенной картинке — после неё. Текст текущего Блока не трогается. Тем же путём ставит Блок «Добавить ›» Ручки Блока — только всегда новым Блоком ниже, даже из пустого абзаца.
|
|
965
|
+
- **«Превратить в»** — сменить тип текущего Блока, сохранив текст: абзац, H2–H4 (и H1 с `heading1`), маркированный, нумерованный и чек-лист, цитата, блок кода. Блок сначала выходит из списка или цитаты, затем становится выбранным: «Превратить пункт списка в заголовок» даёт заголовок вне списка.
|
|
966
|
+
|
|
967
|
+
В пункте — иконка, подпись и напоминание: Markdown-шорткат (`##`, `-`, `>`, `---`), а где его нет — сочетание клавиш. Пункты есть только у включённых Инструментов: выключенный `codeBlock` пропадает из обоих разделов, раздел без пунктов не показывается. Меню без пунктов, кроме абзаца, не появляется вовсе.
|
|
968
|
+
|
|
969
|
+
**`/` открывает меню** в начале Блока или после пробела. Внутри слова (`и/или`), в блоке кода и в строчном коде `/` — обычный символ. Меню стоит у каретки; набранное после «/» фильтрует пункты — по подписи на языке интерфейса, по английской подписи и по коротким именам (`h2`, `ul`, `hr`, `todo`), без учёта регистра и `ё`. Ничего не нашлось — строка «Ничего не найдено». Пробел заканчивает команду: меню закрывается, набранное остаётся текстом.
|
|
970
|
+
|
|
971
|
+
| Клавиша | Список по `/` (фокус в поле) |
|
|
972
|
+
| --------- | ------------------------------------------------------------------------------ |
|
|
973
|
+
| Буквы | Фильтр: запрос после `/` |
|
|
974
|
+
| `↓` / `↑` | Следующий / предыдущий пункт, по кругу |
|
|
975
|
+
| `Enter` | Применить пункт; `/запрос` удаляется той же правкой, отмена — одним `Ctrl/⌘+Z` |
|
|
976
|
+
| `Escape` | Закрыть, оставив набранное; повторно на том же `/` меню не откроется |
|
|
977
|
+
| Пробел | Закончить команду: меню закрывается, текст остаётся |
|
|
978
|
+
|
|
979
|
+
Свой символ-триггер (`@`) открывает такой же список отдельно от «/» — раздел «Свой символ-триггер». Свои Инструменты добавляют пункты в оба раздела описанием, без правки меню: они стоят в конце раздела, в порядке `tools`, и находятся по подписи в текущей локали, по подписи с `en-US` и по `aliases` (разделы «Свой Инструмент» и «Пункты вставки и Действия над Блоком»).
|
|
980
|
+
|
|
981
|
+
«+» на Панели — обычная кнопка меню (`Menu` кита): стрелки, `Enter`, `Escape` и первая буква подписи; пункт применяется к Блоку под кареткой или к выделению, после чего фокус возвращается в Документ.
|
|
982
|
+
|
|
983
|
+
## Раскладка
|
|
984
|
+
|
|
985
|
+
Инструмент `layout` (есть в `full`, в `basic` нет) — Блок из колонок. Вставляется пунктом **«Раскладка»** Меню вставки (`/layout` или «+»): две равные колонки, каретка в первой. Шесть вариантов — **Одна колонка, Две колонки, Три колонки, Четыре колонки, 60/40, 40/60** — выбираются в меню Ручки Блока. «Добавить › Раскладка ›» вставляет Раскладку под Блоком (из колонки — под всей Раскладкой). Вариант переключают «Раскладка ›» у Ручки Блока в колонке и «Формат ›» у Ручки Раскладки, выделенной целиком (`Backspace` на Блоке под ней); у обоих отмечен текущий вариант. Своей кнопки на Панели у Раскладки нет.
|
|
986
|
+
|
|
987
|
+
- **У Блоков колонки — своя Ручка**, у левого края колонки на уровне первой строки Блока (у правой колонки — в промежутке между колонками). Колонка для них — то же, что Документ: «Добавить ›» ставит новый Блок ниже в этой же колонке, «Вверх» и «Вниз» не выходят из неё, «Удалить» единственного Блока оставляет пустую колонку, «Формат ›» меняет тип самого Блока. Вариант Раскладки — отдельным пунктом «Раскладка ›»: каретка почти всегда в колонке, и Ручка всей Раскладки видна редко.
|
|
988
|
+
|
|
989
|
+
- **Колонка — отдельная область.** В ней любые Блоки, кроме другой Раскладки: вложенности нет на уровне Схемы, поэтому вложенную Раскладку не получить ни вставкой, ни через `value`. Раскладка — только Блок верхнего уровня: пункт «Раскладка» из колонки, списка или цитаты ставит новую Раскладку под текущим Блоком верхнего уровня (из колонки — под всей Раскладкой), а Раскладка из буфера, вставленная в колонку, список, цитату или ячейку, разворачивается в свои Блоки. Другие пункты «Добавить» из колонки встают под текущим Блоком **в этой же колонке**; перестановка (`Alt+Shift+↑/↓`) и дублирование Блока тоже не выходят из колонки.
|
|
990
|
+
- **В Редакторе колонки обведены серым пунктиром**, в `RichTextView` обводки нет — граница нужна автору, а не читателю. Ширины колонок задаёт вариант, перетаскиванием они не меняются.
|
|
991
|
+
- **Смена варианта не теряет текст.** Если колонок становится меньше, Блоки лишних колонок переезжают в конец последней оставшейся (пустые лишние колонки просто уходят), если больше — добавляются пустые. Вставка и смена варианта — одна правка, отмена — одним `Ctrl/⌘+Z`. «Одна колонка» — Раскладка из одной колонки; удалить Раскладку целиком — «Удалить» у её Ручки или `Backspace` на Блоке под ней (первое нажатие выделяет Раскладку, второе удаляет).
|
|
992
|
+
- **Узкий экран.** Колонки стоят рядом, только когда сам Документ достаточно широк: две колонки от 30 rem (480 px), три от 40 rem (640 px), четыре от 48 rem (768 px); при меньшей ширине — столбцом, в порядке чтения. Порог зависит от ширины Документа (контейнерный запрос), а не окна, поэтому в узком `Drawer` на широком мониторе колонки тоже встают столбцом. Корень Документа с Раскладкой становится контейнером (`container-type: inline-size`); если ширину задаёт содержимое (элемент flex-ряда без ширины), дайте полю или `RichTextView` ширину.
|
|
993
|
+
|
|
994
|
+
| Клавиша в колонке | Что делает |
|
|
995
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
996
|
+
| `Tab` / `Shift+Tab` | В начало следующей / предыдущей колонки. В списке, таблице и блоке кода `Tab` работает по их правилам; из крайней колонки уходит браузеру — фокус покидает Редактор |
|
|
997
|
+
| `→` / `←` у края текста | В начало соседней колонки справа / в конец соседней слева; из крайней — из Раскладки |
|
|
998
|
+
| `↓` / `↑` у нижней / верхней строки | Из Раскладки — к Блоку под ней / над ней; у края Документа — каретка-щель (gap cursor): каретка между двумя Блоками, где обычной каретке встать некуда |
|
|
999
|
+
| `Backspace` в начале колонки | Колонки не сливаются и текст из Раскладки не выходит; пустой заголовок, как везде, становится текстом |
|
|
1000
|
+
|
|
1001
|
+
**Разметка** — `div` с `data-block`, ширины — вариантом в `data-layout`, без `style` (доли колонок в процентах через дефис):
|
|
1002
|
+
|
|
1003
|
+
```html
|
|
1004
|
+
<div data-block="layout" data-layout="60-40">
|
|
1005
|
+
<div data-block="column"><p>Широкая колонка</p></div>
|
|
1006
|
+
<div data-block="column"><p>Узкая колонка</p></div>
|
|
1007
|
+
</div>
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
Значения `data-layout` — `100`, `50-50`, `33-33-33`, `25-25-25-25`, `60-40`, `40-60` (`RICH_TEXT_SCHEMA.layouts`). HTML чужого редактора с колонками `div[data-block="column"]` и ширинами в `style="flex-basis: 40%"` читается как Раскладка ближайшего варианта, `style` отбрасывается. Если число колонок разошлось с вариантом, Редактор приводит вариант к числу колонок. Без Инструмента `layout` Раскладка из `value` и вставки разворачивается в Блоки подряд.
|
|
1011
|
+
|
|
1012
|
+
**Markdown колонок не знает**: `getMarkdown()` выгружает Блоки колонок подряд, слева направо, а `setMarkdown()` Раскладку не создаёт, даже из сырого HTML. Строки — `editor.layout` и `editor.layoutVariant` в каталоге сообщений (английском и русском); иконка пункта — `ColumnLayoutIcon`. Решение — ADR-0056.
|
|
1013
|
+
|
|
1014
|
+
## Обёртка
|
|
1015
|
+
|
|
1016
|
+
Инструмент `wrapper` (есть в `full`, в `basic` нет) — Блок, собирающий группу Блоков, как Wrapper в Redactor. Без тона своего вида у Обёртки нет: она держит Блоки вместе — их можно двигать, дублировать и удалять одним Блоком. С тоном она становится заметкой или предупреждением, как alerts в GitHub и callout в Notion (подраздел «Тон» ниже). Вставляется пунктом **«Обёртка»** Меню вставки (`/wrapper` или «+») и **«Добавить › Обёртка»** у Ручки Блока: пустая Обёртка с одним абзацем встаёт под текущим Блоком, каретка — в абзац внутри. Своей кнопки на Панели у Обёртки нет.
|
|
1017
|
+
|
|
1018
|
+
- **Внутри — любые Блоки:** абзац, заголовок, списки, цитата, блок кода, таблица, картинка, разделитель; нет только другой Обёртки и Раскладки. Вложенности нет на уровне Схемы, поэтому вложенную Обёртку или Раскладку не получить ни вставкой, ни через `value`. Обёртка, вставленная в Обёртку, список, цитату или ячейку, разворачивается в свои Блоки, а Раскладка внутри Обёртки — в Блоки колонок.
|
|
1019
|
+
- **Место.** Обёртка стоит в Документе и в колонке Раскладки. Из колонки пункт «Обёртка» ставит её под текущим Блоком в той же колонке; из Обёртки, списка, цитаты или ячейки — под их Блоком. Вставка — одна правка, отмена — одним `Ctrl/⌘+Z`.
|
|
1020
|
+
- **У Блоков Обёртки — своя Ручка** у левого края Обёртки. Обёртка для них — то же, что Документ: «Добавить ›» ставит новый Блок ниже в этой же Обёртке, «Вверх», «Вниз» и `Alt+Shift+↑/↓` не выходят из неё, «Удалить» единственного Блока оставляет пустой абзац.
|
|
1021
|
+
- **Ручка самой Обёртки** — когда она выделена целиком (`Backspace` на Блоке под ней): «Вверх», «Вниз», «Дублировать» и «Удалить» берут её вместе с содержимым.
|
|
1022
|
+
- **«Снять обёртку»** — Действие над Блоком Инструмента `wrapper`: пункт меню Ручки у самой Обёртки и у Блока прямо в ней, в секции действий Инструментов перед «Удалить». Блоки встают на место Обёртки, каретка остаётся в своём тексте. Одна правка, одна отмена.
|
|
1023
|
+
- **В Редакторе Обёртка без тона обведена серым пунктиром** с внутренним отступом — тем же видом, что колонки Раскладки; в `RichTextView` обводки нет, а отступы между Блоками — как в Документе: граница нужна автору, а не читателю. Обёртку с тоном видно и читателю — тем же видом, что в Редакторе.
|
|
1024
|
+
|
|
1025
|
+
| Клавиша в Обёртке | Что делает |
|
|
1026
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1027
|
+
| `Enter` в пустом последнем абзаце | Если в Обёртке есть другие Блоки — абзац переезжает под Обёртку, каретка с ним: выход без мыши. Единственный абзац остаётся внутри |
|
|
1028
|
+
| `Backspace` в начале / `Delete` в конце | Текст не выходит из Обёртки и не сливается с Блоком снаружи; пустой заголовок, как везде, становится текстом |
|
|
1029
|
+
| `Backspace` в единственном пустом абзаце | Снимает пустую Обёртку целиком: на её месте пустой абзац, каретка в нём (в колонке — в той же колонке). Одна правка, одна отмена. `Delete` Обёртку не снимает |
|
|
1030
|
+
| `↑` / `↓` у края Обёртки | К Блоку над ней / под ней; у края Документа — каретка-щель, чтобы поставить абзац до или после Обёртки |
|
|
1031
|
+
| `Tab` / `Shift+Tab` | Не перехватываются: как в Документе (в колонке — переход по колонкам). Ловушки клавиатуры нет |
|
|
1032
|
+
|
|
1033
|
+
**Разметка** — `div` с `data-block="wrapper"` и Блоками внутри; единственный свой атрибут — тон `data-tone`, без тона его нет:
|
|
1034
|
+
|
|
1035
|
+
```html
|
|
1036
|
+
<div data-block="wrapper">
|
|
1037
|
+
<h3>Перед началом</h3>
|
|
1038
|
+
<p>Проверьте доступ к репозиторию.</p>
|
|
1039
|
+
</div>
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
Это разметка Redactor: его HTML с Обёрткой читается при вставке и в `value` как есть, а `class`, `style` и прочие атрибуты на ней отбрасываются. Без Инструмента `wrapper` Обёртка из `value` и вставки разворачивается в Блоки подряд, текст не теряется — тон уходит вместе с ней.
|
|
1043
|
+
|
|
1044
|
+
### Тон
|
|
1045
|
+
|
|
1046
|
+
Тон делает Обёртку заметкой: **мягкая подложка тона и полоса слева шириной 3 px** цвета сильной ступени тона, текст — обычный текст Документа, внутренний отступ — как у `Alert` (12 px по бокам, 10 px сверху и снизу). Вид **один в Редакторе и в `RichTextView`**: пунктира у Обёртки с тоном нет и в Редакторе — границу видно по подложке и полосе, а второй обводки, которой нет у читателя, автор не видит. Без тона Обёртка выглядит, как описано выше: пунктир только в Редакторе.
|
|
1047
|
+
|
|
1048
|
+
Выбирается в подменю **«Тон ›»** меню ручки — у Блока прямо в Обёртке и у самой Обёртки, выделенной целиком; «Снять обёртку» стоит ниже, в секции действий перед «Удалить». Пункты-радио (`menuitemradio`): «Без тона» и пять тонов, названных по смыслу заметки: «Заметка», «Совет», «Важно», «Внимание», «Осторожно» (как ключевые слова GitHub alerts); рядом с именем — маленький образец подложки и полосы. Текущий отмечен, а в имени пункта «Тон ›» назван текущий тон: «Tone: Note» / «Тон: Заметка», без тона — «Тон: Без тона». Смена тона — одна правка, одна отмена; каретка остаётся на месте, фокус — в Документе. Выбор текущего тона правки не делает.
|
|
1049
|
+
|
|
1050
|
+
| `data-tone` | Пункт меню (ru / en) | GitHub alert | Подложка (тема) | Полоса (тема) | Лист `paper`: подложка / полоса |
|
|
1051
|
+
| ----------- | -------------------- | ---------------- | ---------------- | ------------------ | ----------------------------------- |
|
|
1052
|
+
| `accent` | Заметка / Note | `> [!NOTE]` | `--accent-soft` | `--accent-strong` | `--accent-soft` / `--accent` ¹ |
|
|
1053
|
+
| `success` | Совет / Tip | `> [!TIP]` | `--success-soft` | `--success-strong` | `--od-green-2-a12` / `--od-green-1` |
|
|
1054
|
+
| `neutral` | Важно / Important | `> [!IMPORTANT]` | `--bg-3` | `--fg-3` | `--od-cloud-11` / `--od-cloud-4` |
|
|
1055
|
+
| `warning` | Внимание / Warning | `> [!WARNING]` | `--warning-soft` | `--warning-strong` | `--od-amber-2-a14` / `--od-amber-1` |
|
|
1056
|
+
| `danger` | Осторожно / Caution | `> [!CAUTION]` | `--danger-soft` | `--danger-strong` | `--od-red-3-a12` / `--od-red-1` |
|
|
1057
|
+
|
|
1058
|
+
¹ В светлом приложении полоса «Заметки» на Листе — `accent-strong` светлой темы, как ссылка (раздел «Поверхность»).
|
|
1059
|
+
|
|
1060
|
+
- **Имена — словарь `Tone` кита**, а не слова меню: в базу уходит `data-tone="warning"`, подпись меню и язык интерфейса его не меняют. Цвет подставляет тема, как у `data-color`: сохранённый Документ читается в `dark`, `light`, `dim`, с любым акцентом и брендом. Значения — роли `--rte-tone-<тон>-background` и `--rte-tone-<тон>-border` (`src/editor/styles/tokens.ts`): у темы они ведут в статусные токены кита, у Листа `surface="paper"` — в примитивы светлой темы. Свой цвет тона (`style`) Схема не знает.
|
|
1061
|
+
- **«Важно» — нейтральный тон**, и у него два отступления от `TONE_TOKENS.neutral`. Подложка — `bg-3`, а не `bg-2`: на `bg-2` стоит само поле Редактора, и «Важно» в нём осталось бы без подложки; `bg-3` — подложка приложения, под которую откалиброваны текстовые токены, и по весу она ближе к мягким подложкам остальных тонов в тёмных темах, чем `bg-4`. В светлой теме эта подложка почти белая — тон там держит полоса. Полоса — `fg-3`, а не декоративная `line`: она несёт смысл и держит 3:1.
|
|
1062
|
+
- **Контраст** (`npm run contrast`, пары тонов Обёртки): текст Документа на каждой подложке — не меньше 4.5:1 (худший — `warning`, 7.15 в тёмной теме на `surface-elevated`; на Листе — 13.3), полоса — не меньше 3:1 и к подложке приложения вокруг Обёртки (худшая — 4.51 у «Заметки» на Листе), и к своей заливке (худшая — 3.68, «Заметка» на Листе; в теме приложения — 4.60). Во всех темах, акцентах, бренде и на Листе.
|
|
1063
|
+
- **Вторичный текст в Обёртке с тоном берёт более контрастные роли.** Ступени `fg-1`–`fg-3` и `accent-strong` откалиброваны под подложки приложения, а на цветных подложках тонов цитата давала бы 4.23:1, ссылка — 3.97, подпись и серый текст — 3.59. Поэтому внутри Обёртки с тоном ссылка, цитата (и подпись языка в шапке блока кода), подпись картинки, выполненный пункт чек-листа, серый цвет текста (`data-color="muted"`) и плейсхолдер пустой подписи читают роли `--rte-tone-link`, `-secondary`, `-muted`, `-muted-foreground`:
|
|
1064
|
+
|
|
1065
|
+
| Что | Роль вне тона | В теме приложения | На Листе `paper` |
|
|
1066
|
+
| ------------------------------------------------------ | ---------------------------- | ----------------- | ---------------- |
|
|
1067
|
+
| ссылка | `--rte-link` | `--fg-0` | `--od-cloud-1` |
|
|
1068
|
+
| цитата, шапка блока кода | `--rte-secondary` | `--fg-0` | `--od-cloud-2` |
|
|
1069
|
+
| подпись картинки, выполненный пункт, серый цвет текста | `--rte-muted`, `-text-muted` | `--fg-0` | `--od-cloud-3` |
|
|
1070
|
+
| плейсхолдер пустой подписи | `--rte-muted-foreground` | `--fg-0` | `--od-cloud-3` |
|
|
1071
|
+
|
|
1072
|
+
В теме приложения на всех пяти подложках 4.5:1 держит только `fg-0` (`fg-1` даёт 4.23 на «Заметке»), поэтому внутри тона вторичный текст — цвета основного: цитату по-прежнему отличает полоса, выполненный пункт — зачёркивание, подпись — кегль, ссылку — подчёркивание (оно у ссылки Документа всегда). На Листе `cloud-2` и `cloud-3` проходят и на подложках тона, и остаются; поднимаются ссылка (акцент давал 3.68) и плейсхолдер. Худшая пара — 7.15:1 в теме (`warning`, тёмная тема), 5.30:1 на Листе (подпись и плейсхолдер на «Заметке»). Вне Обёртки и в Обёртке без тона роли не меняются. Цвет текста автора — палитра и свой цвет — на подложке тона гейт не меряет: это выбор автора.
|
|
1073
|
+
|
|
1074
|
+
- **Неизвестный тон отбрасывается** — и при разборе (`value`, вставка, HTML-режим), и в `sanitizeRichText`: `data-tone="purple"`, `"Accent"` и `data-tone` на любом другом теге снимаются, Обёртка остаётся без тона. Обёртка в Обёртке, списке, цитате или ячейке разворачивается в свои Блоки, тон пропадает. Если в `RichTextView` попал неочищенный HTML с неизвестным тоном, Обёртка показывается без оформления тона.
|
|
1075
|
+
- **Тон — оформление.** ARIA-ролей (`note`, `alert`) и атрибутов `aria-*` в сохранённом HTML нет: скринридер читает Блоки Обёртки, как Блоки вокруг. Поэтому смысл заметки пишется **словом** в её тексте — «Внимание: …», «Совет: …»: цвет подложки не должен быть единственным носителем смысла (WCAG 1.4.1).
|
|
1076
|
+
- **HTML-режим** показывает `data-tone` на Обёртке как есть; текст поля проходит через Схему, как `value`.
|
|
1077
|
+
|
|
1078
|
+
**Markdown — GitHub alerts.** `getMarkdown()` выгружает Обёртку с тоном цитатой, в первой строке которой стоит ключевое слово alert (`[!NOTE]`), дальше — её Блоки внутри цитаты:
|
|
1079
|
+
|
|
1080
|
+
```md
|
|
1081
|
+
> [!WARNING]
|
|
1082
|
+
>
|
|
1083
|
+
> ### Перед обновлением
|
|
1084
|
+
>
|
|
1085
|
+
> Сделайте резервную копию базы.
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
`setMarkdown()` и вставка Markdown читают такую цитату обратно в Обёртку с тоном: ключевое слово — одно, на первой строке, регистр любой (`[!note]`). Обычная цитата, ключевое слово с текстом на той же строке (`> [!NOTE] текст`), неизвестное ключевое слово (`[!INFO]`) и alert внутри списка или цитаты остаются цитатой: GitHub в этих случаях alert не показывает. Вставка alert-цитаты туда, где Обёртки быть не может (в Обёртку, список, цитату, ячейку), и `setMarkdown()` без Инструмента `wrapper` дают Блоки подряд, без ключевого слова — как Обёртка из HTML в том же месте. Обёртка без тона в Markdown по-прежнему — Блоки подряд, а сырой `<div data-block="wrapper">` внутри Markdown Обёртку не создаёт.
|
|
1089
|
+
|
|
1090
|
+
Строки — `editor.wrapper` («Обёртка»), `editor.unwrap` («Снять обёртку»), `editor.wrapperTone` («Тон: {current}»), `editor.wrapperToneNone` («Без тона») и `editor.wrapperToneName` (имена пяти тонов) в каталоге сообщений (английском и русском); иконки — `WrapperIcon`, `UnwrapIcon` и `PaletteIcon` у «Тон ›». Решение — ADR-0057.
|
|
1091
|
+
|
|
1092
|
+
## Эмодзи
|
|
1093
|
+
|
|
1094
|
+
Инструмент `emoji` (есть в `full`, в `basic` нет) — кнопка «Эмодзи» на Панели сразу за «Таблицей ▾», перед отменой и повтором. Она открывает **поповер кита** (`Popover`) с сеткой отобранного вручную набора: «Избранное» (20) и шесть категорий по 10 — «Смайлы», «Жесты», «Животные», «Еда», «Занятия», «Путешествия». Сетка — одна, категории идут подряд с подписями; полного набора Unicode, поиска, «недавних», оттенков кожи и `:шорткодов:` нет и не будет — это быстрый путь к привычным символам, а не окно выбора символов ОС.
|
|
1095
|
+
|
|
1096
|
+
**Эмодзи — обычный текст.** Своего Блока и Метки у него нет, как у `clearFormatting`: Схема, санитайзер и Markdown не меняются, а вставленный символ попадает в выходной HTML, в `RichTextView` и в `getMarkdown` как любой другой текст. Выключить кнопку — убрать `emoji` из `tools`; вместе с ней уходит и вся сетка.
|
|
1097
|
+
|
|
1098
|
+
- **Вставка** — щелчок по ячейке или `Enter` (пробел) в сетке. При пустом выделении эмодзи встаёт в позицию каретки, при непустом — **в конец выделения, не стирая его**. Каретка встаёт сразу за эмодзи, поповер закрывается, фокус возвращается в Документ. Вставка — одна правка на отмену.
|
|
1099
|
+
- **`maxLength`.** Вставка идёт обычной правкой, поэтому предел её останавливает: достигнут — эмодзи не вставляется; если осталось меньше места, чем занимает символ (большинство эмодзи занимают две кодовые единицы UTF-16, как их считает `length`), эмодзи тоже не вставляется: половинки не бывает.
|
|
1100
|
+
- **Нет куда вставлять** (выделена картинка или ячейки таблицы) — кнопка `aria-disabled`, поповер не открывается.
|
|
1101
|
+
- **Состав набора** — данные в одном серверно-безопасном модуле без React и DOM: категории по порядку, у каждого эмодзи символ и ключ имени. Один эмодзи вправе стоять в двух категориях («Избранное» повторяет часть остальных). Библиотеки эмодзи нет.
|
|
1102
|
+
|
|
1103
|
+
**Клавиатура.** Фокус при открытии — на сетке, как у сетки размера таблицы; текущую ячейку называет `aria-activedescendant`.
|
|
1104
|
+
|
|
1105
|
+
| Клавиша | Действие |
|
|
1106
|
+
| --------------- | -------------------------------------------------------- |
|
|
1107
|
+
| `←` / `→` | Предыдущий / следующий эмодзи, через границу категории |
|
|
1108
|
+
| `↑` / `↓` | На строку выше / ниже (в строке 10 эмодзи) |
|
|
1109
|
+
| `Home` / `End` | В начало / в конец строки; с `Ctrl` — первый / последний |
|
|
1110
|
+
| `Enter`, пробел | Вставить текущий эмодзи |
|
|
1111
|
+
| `Escape` | Закрыть поповер и вернуть фокус в Документ |
|
|
1112
|
+
|
|
1113
|
+
Внутри `Dialog` и `Drawer` выбор мышью и клавишами работает, а Escape закрывает только поповер: слои Radix — один стек (ADR-0041). Строки — `editor.emoji` (имя кнопки), `editor.emojiGrid` (имя сетки), `editor.emojiCategory` (подписи категорий) и `editor.emojiName` (имя каждого эмодзи) — в каталоге сообщений, английском и русском.
|
|
1114
|
+
|
|
1115
|
+
## HTML-режим
|
|
1116
|
+
|
|
1117
|
+
Инструмент `source` (есть в `full`, в `basic` нет) — кнопка-переключатель «HTML» на **фиксированной** Панели, в группе «Блок целиком» сразу за «+», как у Redactor. Она заменяет Документ моноширинным полем с HTML текущего Документа; повторное нажатие возвращает визуальный режим. Сочетания клавиш у режима нет, но Справочник сочетаний в режиме доступен.
|
|
1118
|
+
|
|
1119
|
+
```tsx
|
|
1120
|
+
<RichTextEditor preset="full" value={html} onChange={setHtml} /> // кнопка «HTML» уже на Панели
|
|
1121
|
+
<RichTextEditor tools={["bold", "italic", "link", "history", "source"]} /> // или явно в tools
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
**Что видно.** HTML отформатирован: Блок верхнего уровня — на своей строке, дети контейнеров Блоков (пункты списков, строки и ячейки таблицы, колонки Раскладки, части `figure`, пункт чек-листа) — с отступом в два пробела, содержимое Блока — на строке его тега (пробелы текста не меняются), содержимое `pre` — как есть. Форматирование без потерь: неизменённый текст при применении даёт тот же Документ. Пустой Документ открывается пустым полем, а не `<p></p>`. Подсветки синтаксиса нет.
|
|
1125
|
+
|
|
1126
|
+
**Вид.** Поле **всегда тёмное, цветом кита**, а не палитрой подсветки синтаксиса Блока кода: в `dark` и `dim` — самая глубокая подложка темы с её текстом и рамкой, в `light` и на Листе `paper` — те же значения тёмной темы (переменные `--rte-source-background`, `--rte-source-foreground`, `--rte-source-border`, их можно перекрыть). Скругление — как у кода, шрифт — моноширинный 13 px, как у `CodeView`; отступы — как у области Документа. Высота при входе — не меньше высоты Документа, дальше поле растёт по содержимому и растягивается по вертикали вручную. С `maxHeight` или `resizable` поле живёт в той же области прокрутки, что и Документ (раздел «Высота поля»). Выделение текста в поле — тоже тёмной темы текущего акцента (`--selection-dark`) со светлым текстом поля: светлое выделение светлой темы на тёмном поле не читалось.
|
|
1127
|
+
|
|
1128
|
+
**Применение — при выходе, уходе фокуса и чтении.** Текст поля проходит через Схему тем же разбором, что входящий `value` (всё, чего в Схеме нет, отбрасывается — `script`, обработчики `on*`, произвольные `style` и `class`, `javascript:`; разметка выключенных Инструментов снимается, текст остаётся), и заменяет Документ **одной правкой** — одна отмена `Ctrl/⌘+Z` в Документе возвращает его к состоянию до применения. Применение запускают:
|
|
1129
|
+
|
|
1130
|
+
- выход из режима кнопкой «HTML»;
|
|
1131
|
+
- уход фокуса из поля (`blur`) — текст поля при этом не переформатируется: автор продолжает со своим текстом, свежее форматирование видно при следующем входе;
|
|
1132
|
+
- отправка формы (`submit`, в фазе захвата — раньше `onSubmit` потребителя) и сборка `new FormData(form)`;
|
|
1133
|
+
- `ref.getHTML()`, `ref.getMarkdown()`, `ref.getText()` и `ref.getJSON()`, а также вставка `ref.insertHTML()` и `ref.insertText()`;
|
|
1134
|
+
- переход в `readOnly` и пропажа кнопки (смена `toolbar`) — режим закрывается;
|
|
1135
|
+
- размонтирование Редактора — например, Escape в поле HTML закрыл `Dialog` вокруг него: `blur` до поля при этом не доходит, а правка не теряется.
|
|
1136
|
+
|
|
1137
|
+
Если текст не менялся с входа или прошлого применения, либо разбор дал тот же Документ (поменялись только пробелы между Блоками), — ни правки, ни шага истории, ни `onChange`: «открыл — посмотрел — закрыл» безопасно. Иначе `onChange` получает новый HTML **сразу**, без `changeDelay`, как на `blur`; набор в поле HTML `onChange` не вызывает. Скрытое поле `name` обновляется вместе с применением.
|
|
1138
|
+
|
|
1139
|
+
Проверка `required` в режиме следует **тексту поля** на каждом его изменении, а не применённому Документу: браузер проверяет форму до события `submit`, поэтому `form.requestSubmit()` с фокусом в поле HTML пропускает текст, набранный в поле при пустом Документе, и блокирует форму, когда поле очищено, а Документ не пуст. Текст разбирается той же Схемой, что и при применении, и пустой он ровно тогда, когда пустым был бы Документ: `<p></p>` или одни отброшенные Схемой теги блокируют отправку, `<hr>` — нет. Без `required` текст на наборе не разбирается.
|
|
1140
|
+
|
|
1141
|
+
**Внешние замены в режиме.** Новый `value` (не равный последнему отданному), `setMarkdown()` и `clear()` заменяют Документ, а текст поля перезаписывается форматированием нового Документа: экран, который держит `value`, главнее несохранённой правки. Свой же отданный `value`, вернувшийся пропом, поле не сбивает.
|
|
1142
|
+
|
|
1143
|
+
- **`maxLength`** применение не режет — как `setMarkdown`: счётчик показывает превышение. В режиме счётчик отражает последнее применённое состояние.
|
|
1144
|
+
- **`disabled`** и `<fieldset disabled>` выключают поле HTML и кнопку; режим остаётся открытым.
|
|
1145
|
+
- **Что выключено в режиме.** Остальные кнопки и меню Панели — `aria-disabled` (остаются в обходе стрелками, но не срабатывают); Ручки Блока, Меню выделения и Меню вставки по `/` нет. Движок остаётся смонтированным и скрытым: история правок жива.
|
|
1146
|
+
- **Где кнопки нет.** В плавающей Панели и Меню выделения (`toolbar="floating"`, `"bubble"`) её нет: они появляются над выделением в Документе, а в режиме Документа нет; при `toolbar="none"` и в `readOnly` нет Панели. На сенсорном экране Панель всегда фиксированная — значит, режим там есть.
|
|
1147
|
+
- **SSR.** Режим — внутреннее состояние, не проп; он всегда стартует визуальным, поэтому на сервере разметка такая же, как без Инструмента `source`.
|
|
1148
|
+
|
|
1149
|
+
`RichTextView`, Схема, санитайзер `sanitizeRichText` и белый список режим не меняют: он не расширяет Схему: HTML из поля проходит через неё так же, как входящий `value`. Строки — `editor.source` (имя кнопки, «HTML») и `editor.sourceField` (имя поля, «HTML-код документа» / «HTML source») в каталоге сообщений, английском и русском; иконка — `SourceIcon`.
|
|
1150
|
+
|
|
1151
|
+
## Поиск и замена
|
|
1152
|
+
|
|
1153
|
+
Инструмент `search` (есть в `full` и в `basic`) — поиск и замена по Документу. Кнопка «Найти» (лупа) стоит у правого края фиксированной Панели, перед Справочником сочетаний; `Ctrl/⌘+F`, когда фокус в Документе, открывает панель поиска (вне поля работает поиск браузера), `Ctrl/⌘+Alt+F` — с фокусом в поле замены, из кода экрана — `openSearch({ replace? })` Хэндла.
|
|
1154
|
+
|
|
1155
|
+
```tsx
|
|
1156
|
+
<RichTextEditor preset="basic" /> // кнопка «Найти» и Ctrl/⌘+F уже есть
|
|
1157
|
+
<RichTextEditor tools={["bold", "italic", "history", "search"]} /> // или явно в tools
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
- **Панель поиска** — немодальная, под правым краем Панели (без фиксированной Панели — в верхнем углу поля; на узком экране — во всю ширину). Сверху счётчик «3 / 12» и три кнопки-иконки: «Предыдущее совпадение», «Следующее совпадение», «Закрыть поиск»; ниже поле поиска, поле замены, флажки «Учитывать регистр» и «Слово целиком», переключатель «Регулярное выражение», кнопки «Заменить» и «Заменить все». Фокус не заперт: найденное правят в Документе и возвращаются в поле.
|
|
1161
|
+
- **Открытие.** Выделенный текст одного Блока становится запросом (выделение через Блоки — нет: совпадение не переходит границу Блока). Повторный `Ctrl/⌘+F` при открытой панели поиска ставит фокус в поле поиска и выделяет его текст. Кнопка «Найти» — переключатель с `aria-pressed`.
|
|
1162
|
+
- **Подсветка совпадений.** Все совпадения — мягкой заливкой тона `warning`, текущее — плотнее и с рамкой; при переходе, наборе запроса и «Заменить» текущее выделяется, а если его не видно — встаёт посередине свободной области: под липкой Панелью и панелью поиска, внутри поля с `maxHeight`; видимое совпадение не двигается. Совпадения пересчитываются в той же транзакции, что меняет Документ. Классы `rte-search-match` и `rte-search-current` — декорации, в Документ они не уходят.
|
|
1163
|
+
- **Область.** Текст Блоков, включая ячейки таблиц и блоки кода. Совпадение не пересекает границу Блока и строчного Атома (картинка, упоминание, мягкий перенос); Метки внутри не мешают — «bo**ld**» находится как «bold».
|
|
1164
|
+
- **Опции.** Пока флажок «Учитывать регистр» снят, регистр не учитывается. «Слово целиком» — граница по буквам и цифрам Unicode, поэтому работает и для кириллицы (`\b` JavaScript знает только латиницу). С «Регулярным выражением» запрос — выражение JavaScript; неверное — ошибка под полем поиска (`aria-invalid`), подсветки нет. Пустые совпадения выражения (`x*`) пропускаются.
|
|
1165
|
+
- **Клавиши.** `Enter` в поле поиска — следующее совпадение, `Shift+Enter` — предыдущее, по кругу; `Enter` в поле замены — «Заменить». `Escape` закрывает панель поиска, снимает подсветку и ставит каретку на текущее совпадение.
|
|
1166
|
+
- **Замена.** «Заменить» — текущее совпадение, одна правка и одна отмена; дальше текущим становится следующее. «Заменить все» — одна транзакция и одна отмена. Новый текст берёт Метки начала совпадения; пустая замена удаляет найденное. С регулярным выражением в замене работают группы `$1…$9`, `$&` (совпадение целиком) и `$$` (знак доллара) — по тексту, где совпадение нашлось, поэтому и выражения с просмотром назад и вперёд заменяют верно; без выражения `$` — обычный символ. В `readOnly` поля замены и кнопок нет — только поиск.
|
|
1167
|
+
- **Доступность.** Панель — область `role="search"` с именем «Поиск и замена»; счётчик озвучивается через живой регион с `aria-live="polite"` («3 из 12», «Нет совпадений»), у полей подписи, у переключателя — `role="switch"`.
|
|
1168
|
+
- **Вес.** Плагин подсветки совпадений и сочетания — в движке сразу; панель поиска (поля, флажки, переключатель) — ленивый чанк, приходит при первом открытии и Готовность не задерживает.
|
|
1169
|
+
|
|
1170
|
+
Строки каталога сообщений: `editor.search` (кнопка и строка Справочника сочетаний `Ctrl/⌘+F`), `searchReplace` (`Ctrl/⌘+Alt+F`), `searchPanel`, `searchField`, `replaceField`, `searchPrevious`, `searchNext`, `searchClose`, `searchMatchCase`, `searchWholeWord`, `searchRegex`, `replaceOne`, `replaceAll`, `searchCount({ current, total })`, `searchNoMatches`, `searchInvalid`.
|
|
1171
|
+
|
|
1172
|
+
## Строка статуса и Сообщение поля
|
|
1173
|
+
|
|
1174
|
+
Под полем внутри рамки две независимые области. **Строка статуса** — то, что экран кладёт сам: «Сохранено», число слов, соавтор. **Сообщение поля** — то, что сообщают кит и Инструменты: результат загрузки, отказ вставки. Первое — React-узел пропа `status`, второе — данные и команда движка.
|
|
1175
|
+
|
|
1176
|
+
### Строка статуса: `status`
|
|
1177
|
+
|
|
1178
|
+
```tsx
|
|
1179
|
+
<RichTextEditor
|
|
1180
|
+
value={html}
|
|
1181
|
+
onChange={setHtml}
|
|
1182
|
+
maxLength={2000}
|
|
1183
|
+
status={
|
|
1184
|
+
<span className="flex gap-3">
|
|
1185
|
+
<span role="status">{saved ? "Сохранено" : ""}</span>
|
|
1186
|
+
<span>{countWords(html)} слов</span>
|
|
1187
|
+
</span>
|
|
1188
|
+
}
|
|
1189
|
+
/>
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
- Строка стоит внутри рамки поля, в строке счётчика `maxLength`: **своё содержимое у начала, счётчик кита в конце**. Без `maxLength` или пока счётчика нет (он появляется у 80 % предела) строка тянется одним своим содержимым. Пустой `status` (`null`, `undefined`, `false`, `""`) строки не рисует.
|
|
1193
|
+
- **Контейнер без `role` и `aria-live`: кит строку не озвучивает.** Число слов меняется на каждую правку, и зачитывать его вслух значило бы говорить поверх набора. Чтобы «Сохранено» прозвучало, оберните именно его в `role="status"` (пример выше; с автосохранением по `onBlur` — [рецепт на странице API расширения](../../api/editor-extension.md#сохранено-в-строке-статуса)). Не оборачивайте в `role="status"` всю строку.
|
|
1194
|
+
- Видна и при `readOnly`, и при `disabled`: она часть рамки поля, а не правки. При `disabled` рамка приглушена целиком, как у прочих полей, вместе со строкой.
|
|
1195
|
+
- Счётчик сохраняет свой `id` и остаётся в `aria-describedby` поля, пока виден; содержимое экрана в описание поля не входит.
|
|
1196
|
+
- Данные берите из своего состояния: число слов — из `value`/`onChange`, «Сохранено» — из ответа сервера. У Инструментов к Строке статуса доступа нет: у неё нет Инструмента-владельца.
|
|
1197
|
+
- Цвет и размер по умолчанию — вторая ступень текста, 11 px, цифры моноширинные. Свой цвет задайте у своего узла: на подложке поля третья ступень текста не даёт контраста 4.5:1.
|
|
1198
|
+
- В `RichTextView` Строки статуса нет: он показывает очищенный HTML без движка.
|
|
1199
|
+
|
|
1200
|
+
### Сообщение поля: `showNotice`
|
|
1201
|
+
|
|
1202
|
+
Одна область под полем и одно последнее сообщение: **новое заменяет прежнее**, `showNotice(null)` снимает. Сообщение — данные, а не React-узел: тон и готовый текст.
|
|
1203
|
+
|
|
1204
|
+
```tsx
|
|
1205
|
+
type FieldNotice = { tone: "status" | "error"; text: string };
|
|
1206
|
+
|
|
1207
|
+
// Экран — через `ref.editor`:
|
|
1208
|
+
ref.current?.editor?.commands.showNotice({ tone: "error", text: "Не удалось вставить карточку." });
|
|
1209
|
+
ref.current?.editor?.commands.showNotice(null);
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
- **`status`** невидим и только озвучивается: текст идёт в живой регион `role="status"` вне Документа. Ход загрузки глазу показывает плейсхолдер в Документе, а живые регионы внутри `contenteditable` читаются не везде. Остаётся, пока его не заменят или не снимут; Правка автора его не трогает.
|
|
1213
|
+
- **`error`** видна строкой `role="alert"` под полем, над Строкой статуса, **до следующей Правки автора**, нового сообщения или `showNotice(null)`. Правка — любое изменение Документа; замена `value` снаружи и `clear()` правкой не считаются; `setMarkdown()` и `setJSON()` ошибку снимают. Ошибку показывают там, где Документ при этом не изменился: без строки это выглядело бы как «ничего не случилось».
|
|
1214
|
+
- Своих длительностей, кнопок закрытия и спиннеров у сообщения нет: оно уходит само только с правкой, остальное — командой.
|
|
1215
|
+
- **Документ команда не меняет**: ни транзакции, ни шага истории, ни `onChange`. `editor.can().showNotice(…)` сообщения не показывает.
|
|
1216
|
+
- Сообщение без текста (`text: ""`) — то же, что `null`. Текст кит не переводит: он показывается как есть, без разметки. Сообщения самого кита (файлы) идут через каталог сообщений `editor.*`, строки своих Инструментов — из их каталога.
|
|
1217
|
+
- **Кто вызывает.** Экран — `ref.current.editor.commands.showNotice(…)`. Свой Инструмент — из Части для движка: `this.editor.commands.showNotice(…)` в расширении Tiptap. Команда есть у каждого Редактора независимо от `tools`, в том числе без `image`. Тип `FieldNotice` экспортируется из `@olddevs/ui/editor`; им же типизирована команда `editor.commands.showNotice`.
|
|
1218
|
+
- Сообщения о картинках идут этим же каналом: «Загружается файл photo.png…» и «Файл photo.png загружен» — `status`, отказ загрузки, «не картинка» и «файлы не принимаются» — `error`. Свой `error` и сообщение о файле делят одну область: приходит новое — прежнее уходит.
|
|
1219
|
+
|
|
1220
|
+
## Высота поля: `maxHeight` и `resizable`
|
|
1221
|
+
|
|
1222
|
+
По умолчанию Редактор растёт вместе с Документом, и длинный текст раздвигает страницу. `maxHeight` ограничивает **область Документа**: до предела поле растёт как обычно, дальше Документ прокручивается внутри рамки. `resizable` добавляет у нижнего края области ручку высоты. Решение — ADR-0059.
|
|
1223
|
+
|
|
1224
|
+
```tsx
|
|
1225
|
+
<RichTextEditor aria-label="Описание" maxHeight={320} value={html} onChange={setHtml} />
|
|
1226
|
+
<RichTextEditor aria-label="Заметка" maxHeight="24rem" resizable defaultValue={draft} />
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
- **Единицы.** Число — пиксели, строка — любая CSS-длина (`"24rem"`, `"60vh"`). Ограничения по числу строк, как `maxRows` у `TextControl`, нет: у Блоков разная высота.
|
|
1230
|
+
- **Что прокручивается.** Только область Документа: Документ, его Скелет, поле HTML-режима и Ручка Блока. Фиксированная Панель над ней и Строка статуса с Сообщением поля под ней остаются на месте, рамка не уезжает. Полоса прокрутки стоит у рамки.
|
|
1231
|
+
- **`resizable` с `maxHeight`** — `maxHeight` становится **стартовой высотой**, а не пределом: поле тянется и выше, и ниже. Две системы, которые одновременно распоряжаются высотой, дают поле, прыгающее на каждой правке. Тот же довод у `autosize` в `TextControl`. Без `maxHeight` старт — высота Документа. Ниже 128 px (несколько строк) область не сжимается.
|
|
1232
|
+
- **Ручка высоты — своя, а не уголок браузера.** Нативный `resize: vertical` браузер рисует своим уголком, который не берёт тему: в тёмной это белый квадрат на рамке. Ручка кита — полоса у нижнего края области с хватом по центру, в словаре полосы `SplitPanel`: хват `--line`, при наведении `--fg-2`, при перетаскивании и фокусе — акцент. Видимая полоса 10 px, зона попадания 26 px (WCAG 2.5.8). С клавиатуры — ползунок «Изменить высоту поля» (`editor.resizeField`, значение — `editor.fieldHeight`): стрелки вверх и вниз по 16 px, с Shift — по пикселю; перетаскивание мышью переводит фокус на него, чтобы дотянуть стрелками. Двойной щелчок по полосе возвращает стартовую высоту. Высота, заданная перетаскиванием, живёт в поле до размонтирования: сохранить её между визитами — дело экрана.
|
|
1233
|
+
- **Ручка Блока и всплывающие поверхности едут с текстом.** Ручка «⋮» лежит внутри области и прокручивается вместе с Блоком, за краем области она срезается и на Панель не заходит. Плавающая Панель, Меню выделения и панель картинки слушают прокрутку области, а не страницы; у края области они переворачиваются под выделение. Карточка у текста и Меню вставки по «/» следуют за диапазоном и кареткой так же: их якорь привязан к Документу (`contextElement`).
|
|
1234
|
+
- **Без пропов ничего не меняется:** область — простой элемент без стилей, поле растёт с Документом, Панель липнет к верху окна браузера.
|
|
1235
|
+
|
|
1236
|
+
## Лист: `surface`
|
|
1237
|
+
|
|
1238
|
+
`surface="theme"` (по умолчанию) — Документ в теме приложения, как в Notion, Confluence и Slack. `surface="paper"` — **светлый Лист в любой теме** (`dark`, `dim`, бренд), как «Сменить режим» в Word: текст, ссылки, цвет текста и маркер, таблицы, подписи картинок, чек-лист, блок кода (GitHub Light) и цвет автора выглядят как в светлой теме.
|
|
1239
|
+
|
|
1240
|
+
```tsx
|
|
1241
|
+
<RichTextEditor surface="paper" aria-label="Текст письма" value={html} onChange={setHtml} />
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
**Когда брать Лист.** Документ, который уйдёт из приложения светлым: экран подготовки к печати или PDF, редактор письма, шаблон документа. Автор видит текст таким, каким его прочтут, а цвет автора не теряет контраст при смене темы. Для заметок, комментариев и описаний задач Лист не нужен: Документ живёт в приложении и следует его теме.
|
|
1245
|
+
|
|
1246
|
+
**Что остаётся в теме приложения.** Лист — только редактируемая область. Панель (фиксированная и плавающая), Меню вставки, поповеры ссылки, картинки и цвета, меню таблицы, окна, подписи и счётчик под полем, рамка поля и кольцо фокуса вокруг него — в теме приложения: это интерфейс, а не Документ. Край Листа — рамка `--line` и тень `--shadow-1` темы приложения, поэтому Лист отделён и на тёмной подложке, и на светлой.
|
|
1247
|
+
|
|
1248
|
+
**Как устроено.** На Листе стоит вложенный `data-theme="light"` — по нему маркеры (`--marker-*`) и тема подсветки синтаксиса берут светлые значения. Токены кита (`--fg-0`, поверхности, `--accent-strong`) объявлены только на `:root[data-theme]` и вложенную тему не понимают, поэтому Документ читает не их, а роли `--rte-*` (`src/editor/styles/tokens.ts`): у темы они ведут в токены кита, у Листа — в те же примитивы `--od-*`, что назначает светлая тема. Совпадение со светлой темой сверяет тест разбором `styles.css`.
|
|
1249
|
+
|
|
1250
|
+
- **Акцент на Листе — заливка темы приложения** (`--accent`), а не светлый `accent-strong`: у бренда и фиолетового акцента светлая ступень существует только в светлом приложении. Заливка рассчитана под белый текст поверх и поэтому как текст на белом Листе даёт контраст 4.5:1 у любого бренда. В светлом приложении ссылка, цвет `accent` и фокус на Листе — ровно светлый `accent-strong`.
|
|
1251
|
+
- **Фокус внутри Листа** (кнопка языка блока кода, флажки чек-листа) — контур того же акцента: светлый `accent-strong` тёмной темы на белом не даёт контраста 3:1.
|
|
1252
|
+
- **Шапка блока кода на Листе** — светлая: фон и текст — тема подсветки синтаксиса по вложенному `data-theme`, рамка и черта — роли Листа. Кнопка языка, подпись языка и «Копировать» окрашены ролью `--rte-secondary`, наведение — `--rte-control-hover`, а не токенами `ghost`-кнопки: те остались бы значениями тёмной темы, и светлая иконка оказалась бы на белой шапке.
|
|
1253
|
+
- **Тон Обёртки на Листе** — свои роли `--rte-tone-*` со значениями светлой темы (раздел «Обёртка», «Тон»): статусные `*-soft` и `*-strong` темы приложения на белом Листе были бы тёмными.
|
|
1254
|
+
- **Контраст** проверяет `npm run contrast`: каждая пара Листа — против самого Листа, в каждой теме × акценте × бренде.
|
|
1255
|
+
- `RichTextView` с тем же `surface` показывает Документ тем же Листом.
|
|
1256
|
+
|
|
1257
|
+
## Хэндл (`ref`) и своя панель вне поля
|
|
1258
|
+
|
|
1259
|
+
Экран управляет полем через `ref` (`RichTextEditorHandle`) и рисует свою панель на хуке `useRichTextEditorState`. Оба — из `@olddevs/ui/editor`:
|
|
1260
|
+
|
|
1261
|
+
```tsx
|
|
1262
|
+
import { RichTextEditor, useRichTextEditorState } from "@olddevs/ui/editor";
|
|
1263
|
+
import type { RichTextEditorHandle } from "@olddevs/ui/editor";
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
### Методы `ref`
|
|
1267
|
+
|
|
1268
|
+
| Метод | Что делает |
|
|
1269
|
+
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1270
|
+
| `insertHTML(html): boolean` | Вставляет HTML у каретки на месте выделения. Блок вставляют так же: его HTML передают этому методу. |
|
|
1271
|
+
| `insertText(text): boolean` | Вставляет текст буквально, без разбора HTML; перенос строки — мягкий перенос (в блоке кода — символ переноса). |
|
|
1272
|
+
| `getText()` | Текст Документа без разметки; Блоки разделены пустой строкой, Атом — его Текстом атома. |
|
|
1273
|
+
| `getSelectedText()` | Выделенный текст; для каретки — `""`. Атом входит в него только целиком. |
|
|
1274
|
+
| `isEmpty(): boolean` | Пустота по правилу кита — то же, что решает `required` и что пишется в форму `""`: любой Атом (`hr`, картинка, свой), мягкий перенос и таблица (даже с пустыми ячейками) — содержимое; Блок без текста (абзац, заголовок, список, цитата) — нет. |
|
|
1275
|
+
| `focus(position?)` | `"end"` (по умолчанию), `"start"` или `"restore"`: вернуть фокус туда, где было выделение, например после своего диалога или меню. |
|
|
1276
|
+
| `blur()` | Снять фокус с Документа. |
|
|
1277
|
+
| `undo()`, `redo()` | Отменить и повторить правку; работают при включённом Инструменте `history`. |
|
|
1278
|
+
| `openSearch({ replace? })` | Открыть панель «Поиск и замена», как кнопка «Найти» и `Ctrl/⌘+F`; `replace: true` — фокус в поле замены. Без Инструмента `search`, в HTML-режиме и до монтажа — ничего. |
|
|
1279
|
+
| `canUndo()`, `canRedo()` | Доступность `undo` и `redo`: `false` без `history`. |
|
|
1280
|
+
| `getHTML()`, `getMarkdown()`, `setMarkdown(md)`, `clear()` | См. «Public API» и «Markdown». |
|
|
1281
|
+
| `getJSON()`, `setJSON(document): boolean` | Документ JSON-конвертом без потерь и замена Документа конвертом; `false` — конверт отвергнут. См. «JSON». |
|
|
1282
|
+
| `editor` | Исходный `Editor` Tiptap для команд Меток, Блоков и своих Инструментов; его API **следует мажорной версии Tiptap**, а не контракту кита. |
|
|
1283
|
+
|
|
1284
|
+
**Вставка.** `insertHTML` и `insertText` возвращают `true`, если Документ изменился, и `false`, если нет: движка ещё нет, поле только для чтения или выключено, вставка вышла бы за `maxLength`, либо Схема отбросила всё вставленное. HTML проходит через Схему, как входящий `value`: Блок выключенного Инструмента отбрасывается (текст внутри него остаётся обычным абзацем), и если не осталось ничего — вставка не выполняется. **Вставка, выходящая за `maxLength`, не выполняется вовсе** и возвращает `false`: шаблон, обрезанный посередине, хуже отказа; что сказать человеку, решает экран. Каретка встаёт после вставленного, фокус метод сам не берёт — если вставка идёт из кнопки или диалога, начните с `focus("restore")`. У поля, которое ни разу не фокусировали, каретка в начале Документа.
|
|
1285
|
+
|
|
1286
|
+
**Правка автора и Замена снаружи.** Вставка, `undo` и `redo` — Правка автора: отдельный шаг истории (отмена убирает вставку одну, а не вместе с набором перед ней), `onChange` приходит после `changeDelay`, предел `maxLength` действует. `clear()`, `setMarkdown()`, `setJSON()` и новый `value` — Замена снаружи, предел её не режет; `onChange` приходит сразу у `clear()`, `setMarkdown()` и `setJSON()`, а на новый `value` не вызывается. В `readOnly` и у выключенного поля вставка возвращает `false`, а `undo` и `redo` ничего не делают.
|
|
1287
|
+
|
|
1288
|
+
**Вызов метода до Готовности безопасен** — на сервере, до монтажа и пока работают Загрузчики Инструментов (раздел «Свой Инструмент»). Движок создаётся в браузере после первого рендера и Загрузчиков, поэтому вызов в этот момент не бросает исключений и не копит очередь.
|
|
1289
|
+
|
|
1290
|
+
- Чтение отдаёт известное значение: `getHTML()` — входящий `value`, `getText()` и `getSelectedText()` — `""`, `getJSON()` — `null`, `isEmpty()` судит по входящему значению по тому же правилу, что после монтажа (свой Блок до монтажа — всегда содержимое: Атом ли он, знает только его Часть для движка).
|
|
1291
|
+
- Запись и фокус ничего не делают.
|
|
1292
|
+
- Вставка и `setJSON()` возвращают `false`, `canUndo()` и `canRedo()` — тоже `false`.
|
|
1293
|
+
|
|
1294
|
+
На сервере `ref.current` пуст, пока Редактор не смонтируется, поэтому пишите `ref.current?.…`.
|
|
1295
|
+
|
|
1296
|
+
**HTML-режим.** `getText()` применяет правку поля HTML, как `getHTML()`; вставка применяет её, вставляет в Документ и перезаписывает поле; `focus` и `blur` относятся к полю HTML; `undo` и `redo` ничего не делают (у `textarea` своя отмена); `getSelectedText()` отдаёт `""`.
|
|
1297
|
+
|
|
1298
|
+
### Своя панель: `useRichTextEditorState`
|
|
1299
|
+
|
|
1300
|
+
`useRichTextEditorState(ref, selector)` — хук-селектор поверх `useEditorState` Tiptap. `selector` получает `editor` и возвращает срез состояния; компонент перерисовывается, **только когда срез изменился** (сравнение по значению, поэтому можно вернуть объект), а набор текста, не меняющий срез, панель не трогает. Пока движка нет — до монтажа и на сервере — хук возвращает `null`, а селектор не вызывается; когда движок появится, экран перерисуется сам.
|
|
1301
|
+
|
|
1302
|
+
Команды панели — через `ref.current.editor.chain().focus()…run()` — тот же язык, что у команд Инструментов. Своего реестра команд у кита нет.
|
|
1303
|
+
|
|
1304
|
+
```tsx
|
|
1305
|
+
import { useRef } from "react";
|
|
1306
|
+
import { RichTextEditor, useRichTextEditorState } from "@olddevs/ui/editor";
|
|
1307
|
+
import type { RichTextEditorHandle } from "@olddevs/ui/editor";
|
|
1308
|
+
|
|
1309
|
+
function Screen() {
|
|
1310
|
+
const ref = useRef<RichTextEditorHandle>(null);
|
|
1311
|
+
const state = useRichTextEditorState(ref, (editor) => ({
|
|
1312
|
+
bold: editor.isActive("bold"),
|
|
1313
|
+
// Без Инструмента `history` команды `undo` у движка нет.
|
|
1314
|
+
canUndo: editor.can().undo?.() === true,
|
|
1315
|
+
}));
|
|
1316
|
+
|
|
1317
|
+
return (
|
|
1318
|
+
<>
|
|
1319
|
+
<button
|
|
1320
|
+
type="button"
|
|
1321
|
+
aria-pressed={state?.bold ?? false}
|
|
1322
|
+
// Клик в панель не должен уводить фокус из Документа.
|
|
1323
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
1324
|
+
onClick={() => ref.current?.editor?.chain().focus().toggleBold().run()}
|
|
1325
|
+
>
|
|
1326
|
+
Жирный
|
|
1327
|
+
</button>
|
|
1328
|
+
<button
|
|
1329
|
+
type="button"
|
|
1330
|
+
disabled={!state?.canUndo}
|
|
1331
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
1332
|
+
onClick={() => ref.current?.undo()}
|
|
1333
|
+
>
|
|
1334
|
+
Отменить
|
|
1335
|
+
</button>
|
|
1336
|
+
<RichTextEditor ref={ref} aria-label="Текст" />
|
|
1337
|
+
</>
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
**Рецепт: кнопки внешней панели делают `preventDefault` на `mousedown`.** Клик по кнопке переводит фокус на неё, поле теряет фокус, и команда без `.focus()` в цепочке пошла бы мимо. `preventDefault` на `mousedown` оставляет фокус в Документе, поэтому выделение не пропадает ни визуально, ни для плавающей Панели и Меню выделения, которые прячутся при потере фокуса. Выделение ProseMirror переживает потерю фокуса и без этого, но видимость плавающей Панели, Меню выделения и панели картинки от фокуса зависит. Правило «клик в свою панель не снимает выделение» публичным контрактом не становится: это рецепт, а не свойство поля. Кнопка, открывающая свой диалог, где фокус уйдёт неизбежно, вызывает `ref.current?.focus("restore")` при закрытии диалога.
|
|
1343
|
+
|
|
1344
|
+
**История с внешней панелью.** Хэндл даёт `undo()` и `redo()` и их доступность. Для `disabled` кнопки «Отменить» берите доступность из среза хука — `editor.can().undo?.() === true`: срез перерисует панель сразу после правки. `ref.current.canUndo()` — разовое чтение в обработчике, на перерисовку оно не подписано. Без Инструмента `history` команды `undo` у движка нет (отсюда `?.`), и обе кнопки остаются недоступными.
|
|
1345
|
+
|
|
1346
|
+
## События
|
|
1347
|
+
|
|
1348
|
+
Экран узнаёт о Редакторе колбэк-пропсами; шины событий нет. Решение — ADR-0058, раздел 9.
|
|
1349
|
+
|
|
1350
|
+
```tsx
|
|
1351
|
+
<RichTextEditor
|
|
1352
|
+
ref={ref}
|
|
1353
|
+
aria-label="Письмо"
|
|
1354
|
+
value={html}
|
|
1355
|
+
onChange={setHtml}
|
|
1356
|
+
onReady={() => ref.current?.insertHTML("<p>Здравствуйте!</p>")}
|
|
1357
|
+
onFocus={() => setEditing(true)}
|
|
1358
|
+
onBlur={() => save(ref.current?.getHTML() ?? "")}
|
|
1359
|
+
/>
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
- **`onReady()`** — без аргументов, **один раз за монтаж**, когда движок создан. `ref` к этому моменту уже действует: в колбэке вставляйте шаблон или ставьте фокус. Срабатывает и при `readOnly`, и при `disabled`; перерисовка, смена `placeholder` или самого колбэка повторного вызова не дают. Ошибка набора `tools` (движка нет, см. «Свой Инструмент») и серверный рендер `onReady` не вызывают. Готовность — движок создан **и все Загрузчики Инструментов отработали** (раздел «Свой Инструмент»): `onReady` ждёт их, а при сбое Загрузчика не вызывается — и после «Повторить» приходит один раз, когда движок создан.
|
|
1363
|
+
- **`onFocus()` и `onBlur()`** — о Фокусе поля, без аргументов (DOM-событие не пробрасывается). Поле — целиком: Документ, Панель, поповеры и окна кита (ссылка, цвет, картинка, «Вставить таблицу»), поле HTML-режима. Переход между ними Фокус поля не теряет: из текста в Панель, в поповер ссылки и обратно ни `onBlur`, ни `onFocus` не приходят — как у составного поля формы. Фокус Документа (`focus` и `blur` движка) — другое: клик в Панель его снимает, а Фокус поля остаётся.
|
|
1364
|
+
- **Перед `onBlur` отложенный `onChange` выталкивается**, откуда бы фокус ни ушёл (из Документа, Панели, поповера), поэтому автосохранение по `onBlur` получает свежий Документ. `onBlur` приходит на такт позже ухода фокуса: за это время он мог вернуться в поле. Если Редактор размонтирован с фокусом внутри, `onBlur` не вызывается (отложенный `onChange` при этом всё равно отдаётся).
|
|
1365
|
+
|
|
1366
|
+
### Порядок вставки и drop
|
|
1367
|
+
|
|
1368
|
+
Во вставку из буфера и drop вмешивается только Часть для движка своего Инструмента — плагинами ProseMirror `transformPastedHTML`, `transformPasted`, `handlePaste` и `handleDrop` в `engine`. Своих имён хуков и пропсов `onPaste` и `onDrop` у Редактора нет.
|
|
1369
|
+
|
|
1370
|
+
- **Обработчики своих Инструментов видят событие раньше встроенных** — вставки Markdown, загрузки картинок и Раскладки, — где бы свой Инструмент ни стоял в `tools`. `handlePaste` и `handleDrop`, вернувшие `true`, забирают событие целиком: встроенные до него не доходят. Так ссылку на видео можно превратить в свой Блок до того, как её разберёт Markdown.
|
|
1371
|
+
- **Разбор по Схеме и предел действуют всегда.** Что вставка внесла сверх Схемы (`<script>`, `onclick`, неизвестные Схеме теги, значения вне правил атрибутов), отбрасывается, как у входящего `value`; очистка HTML (`transformPastedHTML`) идёт до Схемы и ослабить её не может. Транзакция своего обработчика, которая уводит текст за `maxLength`, **отклоняется целиком**; урезание хвоста есть только у встроенной вставки из буфера (транзакция с признаком `paste`).
|
|
1372
|
+
- **Файлы, кроме картинок, перехватывает свой Инструмент** в `handlePaste` и `handleDrop`: своя функция загрузки, свой адрес — опциями его расширения. Общего `onUploadFile` у Редактора нет. Файл, который не обработал ни один обработчик, доходит до кита: картинка загружается через `onUploadImage`, остальное получает сообщение «Файл `имя` — не картинка» (`editor.imageNotImage` в каталоге сообщений).
|
|
1373
|
+
|
|
1374
|
+
```tsx
|
|
1375
|
+
const PdfDrop = Extension.create({
|
|
1376
|
+
name: "acme_pdfDrop",
|
|
1377
|
+
addProseMirrorPlugins: () => [
|
|
1378
|
+
new Plugin({
|
|
1379
|
+
key: new PluginKey("acme_pdfDrop"),
|
|
1380
|
+
props: {
|
|
1381
|
+
handlePaste: (_view, event) => takePdf(event.clipboardData?.files),
|
|
1382
|
+
handleDrop: (_view, event) => takePdf(event.dataTransfer?.files),
|
|
1383
|
+
},
|
|
1384
|
+
}),
|
|
1385
|
+
],
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
// Инструмент без Блока: только поведение.
|
|
1389
|
+
export const pdfTool: CustomTool = { name: "acme_pdfDrop", engine: [PdfDrop] };
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
## Public API
|
|
1393
|
+
|
|
1394
|
+
- `value` + `onChange(html)` либо `defaultValue` — как у остальных полей ([conventions.md](../../conventions.md), «Управление состоянием»). Пустой Документ — `""`, а не `<p></p>`. `defaultValue` принимает и JSON-конверт (`RichTextDocument`, раздел «JSON»); `value` и `onChange` — всегда HTML.
|
|
1395
|
+
- **`onChange` вызывается не чаще раза в `changeDelay` мс** (по умолчанию 300), по заднему фронту, и сразу — на `blur`, перед `submit` формы и при сборке `new FormData(form)`. Сериализация Документа в HTML — проход по всему дереву, и на каждую букву она тормозила бы ввод на длинных Документах. `changeDelay={0}` — отдавать каждую правку.
|
|
1396
|
+
- **Входящий `value`, равный последнему отданному HTML, Документ не пересоздаёт** и каретку не сбивает: обычный цикл «отдал — получил обратно» безопасен. Другой `value` заменяет Документ целиком; такая замена — не правка, и `onChange` на неё не вызывается.
|
|
1397
|
+
- `name` рендерит `<input type="hidden">` с текущим HTML — значение попадает в `FormData` и в отправку нативной формы.
|
|
1398
|
+
- `maxLength` — предел символов: текст Документа без разметки и без разделителей Блоков, Атом весит длину своего Текста атома (разделитель — 0, мягкий перенос — 1, картинка — длину подписи; см. «Свой Инструмент»). Счётчик остатка появляется у 80 % предела и входит в описание поля; ввод сверх предела не проходит, вставка обрезается до того, что поместилось: хвост срезается по символам, а Атом, которому не хватило места, уходит целиком вместе со всем, что после него. **`value` длиннее предела грузится целиком**: это данные экрана, их можно сокращать, но не удлинять, а счётчик говорит, на сколько превышен лимит.
|
|
1399
|
+
- `readOnly` — движок жив, текст выделяется и уходит в форму, правка закрыта. `disabled` — как у прочих полей: правка закрыта, поле приглушено, в форму не уходит.
|
|
1400
|
+
- **`<fieldset disabled>` выключает Редактор, как нативное поле** — и `Form pending`, и `fieldset` потребителя. Переключение на лету подхватывается (`MutationObserver` на атрибуте `disabled` предков-`fieldset`), без перемонтажа. Читается DOM, а не контекст `Form`: так покрываются оба случая одним правилом.
|
|
1401
|
+
- `placeholder` — по умолчанию из каталога сообщений (`editor.placeholder`).
|
|
1402
|
+
- `onReady()`, `onFocus()`, `onBlur()` — события поля, без аргументов; см. «События». `onReady` — один раз за монтаж, `onFocus` и `onBlur` — о Фокусе поля целиком (Документ, Панель, поповеры, поле HTML), перед `onBlur` выталкивается отложенный `onChange`.
|
|
1403
|
+
- `onUploadImage(file) → Promise<string>` — загрузчик картинок, см. «Картинки». Без него файлы не принимаются.
|
|
1404
|
+
- `maxHeight` — предел высоты области Документа (число — пиксели, строка — CSS-длина), дальше Документ прокручивается внутри рамки; `resizable` — ручка высоты у нижнего края области (ползунок с клавиатуры), `maxHeight` с ней — стартовая высота. См. «Высота поля».
|
|
1405
|
+
- `surface` — `"theme"` (по умолчанию) или `"paper"`: светлый Лист в любой теме, см. «Поверхность». Тип — `RichTextSurface`.
|
|
1406
|
+
- `status` — React-узел Строки статуса внутри рамки поля, у начала строки счётчика; кит её не озвучивает. Команда движка `showNotice({ tone, text } | null)` — Сообщение поля. Оба — в «Строка статуса и Сообщение поля»; подчиняются semver кита.
|
|
1407
|
+
- `tools` — имена кита и описания своих Инструментов (`CustomTool`, в том числе их `buttons`, `insertItems`, `blockActions`, `textCards` и `trigger`), Пресеты массивами — `RICH_TEXT_PRESETS`; см. «Инструменты и Пресеты» и «Свой Инструмент». Описание с ошибкой — ошибка поля.
|
|
1408
|
+
- `ref` (`RichTextEditorHandle`): `getHTML()` — текущий HTML, без ожидания задержки и без `onChange` (в HTML-режиме сначала применяет правку поля — тогда `onChange` вызывается, если Документ изменился; то же у `getMarkdown()`); `getMarkdown()` и `setMarkdown(md)` — см. «Markdown»; `getJSON()` и `setJSON(document)` — см. «JSON»; `clear()` — очистить Документ, экран узнаёт об этом через `onChange("")`; `focus(position?)`, `blur()`, `insertHTML`, `insertText`, `getText`, `getSelectedText`, `isEmpty`, `undo`, `redo`, `canUndo`, `canRedo` — см. «Хэндл (`ref`) и своя панель вне поля»; `openSearch({ replace? })` — открыть панель поиска, см. «Поиск и замена»; `editor` — исходный `Editor` Tiptap, его API **следует мажорной версии Tiptap**, до монтажа и на сервере он `null`. `focus()` без аргумента ставит каретку в конец Документа (в HTML-режиме — в поле HTML).
|
|
1409
|
+
- `useRichTextEditorState(ref, selector)` — хук для своей панели вне поля, см. «Хэндл (`ref`) и своя панель вне поля».
|
|
1410
|
+
- SSR: движок создаётся только в браузере (`immediatelyRender: false`). На сервере рисуется Скелет без Документа — рамка и зарезервированное под Панель место — и скрытое поле с входящим значением. После гидратации Скелет показывает Документ, очищенный Схемой, а редактируемая область встаёт на его место после Загрузчиков, без прыжка высоты. Исключение — JSON-конверт в `defaultValue`: его HTML собирает только движок, поэтому Скелет при нём — рамка без Документа, скрытое поле до Готовности пустое, а высота один раз меняется, когда Документ встаёт.
|
|
1411
|
+
|
|
1412
|
+
## Markdown
|
|
1413
|
+
|
|
1414
|
+
Движок Markdown — пакет `@tiptap/markdown` (тот же диапазон версий, что у остального Tiptap, в команде установки выше) и `marked` (`^17`, как в зависимостях `@tiptap/markdown`): у каждого Редактора свой экземпляр `marked`, поэтому набор `tools` одного не меняет разбор Markdown у другого на той же странице.
|
|
1415
|
+
|
|
1416
|
+
- **Вставка.** Обычный текст из буфера, в котором есть разметка Markdown — заголовок `#`, пункт списка (в том числе `- [ ]`), цитата `>`, ограждение кода, строка-разделитель таблицы, `**жирный**`, `~~зачёркнутый~~`, `` `код` ``, ссылка (`[текст](url)`, с заголовком `[текст](url "заголовок")`, автоссылка `<https://…>` и строка определения сноски `[1]: https://…`), `*курсив*` и `_курсив_` на границах слов, — превращается в Блоки и Метки. Текст без этой разметки вставляется абзацами: «6 = 2 * 3» или `file_name_v2` разметкой не станут. Одна строка с разметкой продолжает абзац, а не рвёт его.
|
|
1417
|
+
- **Ссылки.** Понимаются все формы ссылки Markdown: строчная, с заголовком, со сноской (`[текст][1]`, `[текст][]`, `[текст]` и строка определения `[1]: адрес`) и автоссылка в угловых скобках; так же и в `setMarkdown`. У ссылки в Схеме только адрес: заголовок отбрасывается, строка определения в Документ не попадает, угловые скобки тоже, а `getMarkdown()` пишет любую из них обычной ссылкой `[текст](адрес)`. Адрес проходит ту же политику, что и в поповере: относительный (`/docs`, `#part`) ссылкой не станет — останется её текст.
|
|
1418
|
+
- **Копия из редактора кода.** VS Code, JetBrains IDE, Sublime Text и подобные кладут в буфер рядом с текстом HTML — тот же исходник, раскрашенный подсветкой синтаксиса. Если этот HTML — один моноширинный элемент (`div` или `pre` с моноширинным шрифтом), внутри только строки и раскраска `span`, его текст совпадает с обычным текстом буфера с точностью до пробелов, а обычный текст похож на Markdown по разметке выше, — разбирается обычный текст как Markdown. Цвета подсветки синтаксиса в Документ не попадают: это тема редактора кода, а не цвет автора. Код, не похожий на Markdown, вставляется обычным образом.
|
|
1419
|
+
- **Когда вставка остаётся текстом.** Если рядом с текстом в буфере лежит любой другой HTML (копия из браузера, из Word, из Google Docs), работает обычный разбор HTML — Markdown в нём не ищется. Вставка «как простой текст» (`Ctrl/⌘+Shift+V`) и вставка внутрь кода тоже дают исходные символы. Если из вставленного Markdown не осталось ничего, что Схема может принять (при выключенном `table` — одна таблица, при выключенном `codeBlock` — один блок кода), вставляется исходный текст, а не ничего.
|
|
1420
|
+
- **Таблицы.** Таблица GFM (`| a | b |` и строка-разделитель, для распознавания вставки — не короче `---` в ячейке) становится таблицей, первая строка — заголовком; `getMarkdown()` выгружает таблицу обратно, всегда со строкой-заголовком (без неё — с пустой). Многострочное содержимое ячейки в Markdown уходит одной строкой, а переносы — `<br>`; `|` в ячейке экранируется. **Выравнивание столбца сохраняется при переносе в обе стороны:** `:---:` и `---:` при вставке и `setMarkdown` становятся `data-align` на ячейках столбца, а `getMarkdown()` пишет их обратно; `:---` — умолчание, атрибута не даёт. **Слияние ячеек Markdown не выражает:** `getMarkdown()` раскладывает слитую ячейку по сетке — текст остаётся в её левой верхней позиции, накрытые позиции становятся пустыми ячейками, поэтому столбцы ниже не съезжают; само слияние теряется, и `setMarkdown(getMarkdown())` вернёт таблицу без него.
|
|
1421
|
+
- **`getMarkdown()`** — Документ как Markdown, сейчас, без ожидания `changeDelay`. Пустой Документ и вызов до монтажа (на сервере движка нет) — `""`.
|
|
1422
|
+
- **`setMarkdown(md)`** заменяет Документ разобранным Markdown. Как `clear()`, это Замена снаружи: `onChange` получает новый HTML сразу и один раз, предел `maxLength` её не режет. До монтажа ничего не делает.
|
|
1423
|
+
- **Схема главнее.** Разобранное проходит через Схему Редактора: всё, чего в ней сейчас нет, отбрасывается, в том числе из `setMarkdown`. В этой версии кита теряются картинки с адресом вне `http`/`https` (пропадают целиком, вместе с `alt`), ссылки с адресом вне политики `http`/`https`/`mailto` (текст ссылки остаётся) и исходный HTML внутри Markdown (пропадает целиком). Без Инструмента `taskList` чек-листы становятся обычными пунктами списка, без `codeBlock` блок кода пропадает, без `table` пропадает таблица. Раскладки и Обёртки без тона из Markdown не бывает: их Блоки встают подряд. Обёртку с тоном даёт только GitHub alert (`> [!NOTE]` и ещё четыре значения, раздел «Обёртка», «Тон»), и `getMarkdown()` выгружает её им же. С `heading1` `#` даёт H1, а `getMarkdown` пишет H1 как `#`. Без него `#` прижимается к H2; `#####` в любом случае — к H4. Что именно принимается, определяет набор Инструментов, а не этот список.
|
|
1424
|
+
- **Свои Инструменты.** Свои правила Markdown — в Части для движка; без них свой Блок выгружается содержимым, Атом — Текстом атома (раздел «Свой Инструмент», «Вне Редактора»).
|
|
1425
|
+
- **Что теряется при выгрузке.** Markdown не выражает цвет текста и маркера, выравнивание Блока, подчёркивание, ширину и подпись картинки, слияние ячеек таблицы и список внутри ячейки: картинка выгружается как ``, `getMarkdown()` остальное опускает, а `setMarkdown(getMarkdown())` вернёт Документ без них. Так устроен формат: для хранения без потерь берите HTML, для файла без потерь — JSON-конверт (раздел «JSON»).
|
|
1426
|
+
|
|
1427
|
+
## JSON
|
|
1428
|
+
|
|
1429
|
+
JSON-конверт — Документ для файла, шаблона или переноса между продуктами **без потерь**: в нём остаётся всё, что Markdown теряет (цвет, маркер, выравнивание, подчёркивание, картинка с шириной и подписью, слияние ячеек, Раскладка, Обёртка с тоном). Конверт — формат обмена **рядом с HTML, а не вместо него**: `value`, `onChange`, скрытое поле формы, `RichTextView` и серверная очистка по-прежнему работают с HTML (ADR-0061).
|
|
1430
|
+
|
|
1431
|
+
```ts
|
|
1432
|
+
import type { RichTextDocument } from "@olddevs/ui/editor";
|
|
1433
|
+
|
|
1434
|
+
// { format: "olddevs-rich-text", version: 1, doc: { type: "doc", content: [...] } }
|
|
1435
|
+
const saved: RichTextDocument | null = ref.current?.getJSON() ?? null;
|
|
1436
|
+
const opened: boolean = ref.current?.setJSON(JSON.parse(fileText)) ?? false;
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
- **Форма.** `format` — всегда `"olddevs-rich-text"`, `version` — номер формата дерева (сейчас `1`), `doc` — дерево Документа с верхним узлом `"doc"`. Имена узлов и атрибутов в `doc` — контракт формата: если Схема их переименует, кит поднимет `version` и сам прочтёт старые файлы. Не сверяйте и не стройте `doc` руками: берите то, что отдал `getJSON()`.
|
|
1440
|
+
- **`getJSON()`** — Документ конвертом, сейчас, без ожидания `changeDelay`. В HTML-режиме сначала применяется правка поля HTML, как у `getHTML()`. До монтажа и на сервере — `null`.
|
|
1441
|
+
- **`setJSON(document): boolean`** заменяет Документ конвертом. Параметр — `unknown`: передавайте прямо результат `JSON.parse` файла, форму проверяет Редактор. Как `setMarkdown()`, это Замена снаружи: `onChange` получает новый HTML сразу и один раз, предел `maxLength` её не режет, фокус метод не берёт, в HTML-режиме текст поля переписывается. `false` — конверт отвергнут или движка ещё нет; Документ при этом не тронут, а что сказать человеку («файл не подходит»), решает экран.
|
|
1442
|
+
- **Что отвергается целиком:** не объект, другое значение `format`, другая `version` (в том числе её отсутствие), `doc` не с верхним узлом `"doc"` и дерево, из которого не собрать узлы (некорректный узел, пустой текстовый узел). Голое дерево Tiptap из `editor.getJSON()` — тоже: без конверта не отличить файл Редактора от любого другого JSON.
|
|
1443
|
+
- **Схема главнее — и для JSON.** Конверт проходит Схему так же строго, как HTML: Блок выключенного Инструмента разворачивается в свой текст, Метка, которой нет в Схеме, снимается, а атрибуты проверяются **теми же правилами, что у HTML**. Отбрасываются опасный адрес ссылки или картинки, цвет вне палитры, цвет автора не в форме `#rrggbb`, `data-*` вне списка Схемы и значения своих Инструментов вне их правил, причём в самом Документе, а не только при выводе. Пробелы автора при переносе туда и обратно (`getJSON()` → `setJSON()`) не схлопываются.
|
|
1444
|
+
- **`defaultValue` с конвертом** — начальный Документ из JSON, только неуправляемый режим (`value` главнее). Он проходит ту же проверку, что `setJSON()`, встаёт при появлении движка без `onChange` и без шага отмены; отвергнутый — пустой Документ и одна ошибка в консоли разработки. До Готовности Скелет при конверте — рамка без Документа, `getHTML()` — `""`, а `isEmpty()` судит по дереву конверта по правилу `required`.
|
|
1445
|
+
- **Свои Инструменты** правил JSON не пишут: Блок, Атом или Метка своего Инструмента, включённого в `tools`, проходят этот путь сами и проверяются правилами своего описания, выключенный — разворачивается, Атом без содержимого пропадает, как в HTML.
|
|
1446
|
+
- **Кнопок «Скачать» и «Открыть файл» в ките нет** — это код экрана из примитивов кита; пример — история `JsonExportImport` в Storybook. Для хранения в базе берите HTML из `onChange`: его чистит сервер (`sanitizeRichText`), очистки JSON на сервере у кита нет.
|
|
1447
|
+
|
|
1448
|
+
## Схема и безопасность
|
|
1449
|
+
|
|
1450
|
+
Схема — единственный источник допустимой разметки. Всё, чего в ней нет, отбрасывается **и при загрузке `value`, и при вставке**: `<script>`, обработчики `on*`, атрибут `style` (кроме цвета автора, ниже), ссылки на `javascript:`, `data:` и любые адреса, кроме абсолютных `http`, `https` и `mailto`, картинки с адресом вне `http`/`https` (`data:` — base64 — и `blob:` в том числе), `width`, `height` и `class` на `<img>`, неразрешённые теги. Текст из отброшенного тега сохраняется, теряется сам тег.
|
|
1451
|
+
|
|
1452
|
+
Белый список этого выпуска при `preset="full"` (выключенные Инструменты сужают его):
|
|
1453
|
+
|
|
1454
|
+
<!-- allow-list -->
|
|
1455
|
+
|
|
1456
|
+
| Что | Разрешено |
|
|
1457
|
+
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1458
|
+
| Блоки | `p`, `h1`, `h2`, `h3`, `h4`, `ul`, `ol`, `li`, `blockquote`, `hr`, `pre`, `table`, `tbody`, `tr`, `th`, `td`; `label`, `input`, `span` — только внутри пункта чек-листа; `div` — там же, а ещё Раскладка и её колонки (Раскладка — только на верхнем уровне, колонка — только прямо в ней) и Обёртка (на верхнем уровне и прямо в колонке, не в другой Обёртке); `figure` — картинка, внутри неё `img` и необязательная `figcaption` (вне `figure` их нет) |
|
|
1459
|
+
| Метки | `strong`, `em`, `u`, `s`, `code`, `sup`, `sub`, `mark` (только с цветом), `br`, `a` |
|
|
1460
|
+
| Атрибуты | `ol[start]` — только когда нумерация не с 1; `ul[data-type]`, `li[data-type]`, `li[data-checked]` — чек-лист; `input[type]`, `input[checked]` — его флажок; `code[class]` — язык блока кода, только «language-python», «language-typescript», «language-javascript», «language-go», «language-html», «language-json», «language-yaml» или «language-markdown» (без языка класса нет); `span[data-color]`, `mark[data-color]` — цвет текста и маркер, только имена палитры Редактора — на `span` цвета текста, на `mark` маркера; `span[style]`, `mark[style]` — произвольный цвет автора, только «color:#rrggbb» и «background-color:#rrggbb», и не вместе с data-color; `p[data-align]`, `h1[data-align]`, `h2[data-align]`, `h3[data-align]`, `h4[data-align]` — выравнивание Блока, по центру или справа; `th[colspan]`, `td[colspan]` — слияние по горизонтали, целое от 2 до 20; `th[rowspan]`, `td[rowspan]` — по вертикали, от 2 до 50; `th[data-align]`, `td[data-align]` — выравнивание столбца, по центру или справа, на каждой его ячейке; `a[href]` — только http, https или mailto; `a[rel]` — всегда «noopener noreferrer»; `img[src]` — только абсолютный http или https, без data и blob; `img[alt]` — всегда, пустой у картинки без описания; `img[data-width]` — 50, 75 или 100; `div[data-block]` — Раскладка («layout»), её колонка («column») и Обёртка («wrapper»); `div[data-layout]` — вариант Раскладки, доли колонок в процентах: «100», «50-50», «33-33-33», «25-25-25-25», «60-40» или «40-60»; `div[data-tone]` — тон Обёртки, только на ней: «neutral», «accent», «success», «warning» или «danger» (без тона атрибута нет) |
|
|
1461
|
+
|
|
1462
|
+
Пункт чек-листа Tiptap пишет так: `<li data-type="taskItem" data-checked="true"><label><input type="checkbox" checked><span></span></label><div><p>…</p></div></li>`. Санитайзеру стоит пускать `input`, `label`, `span` и `div` только внутри такого пункта, а `input` — только с `type="checkbox"`.
|
|
1463
|
+
|
|
1464
|
+
Таблица — `table > tbody > tr > th|td`; у ячейки — только `colspan` и `rowspan` (`colspan` — целое от 2 до 20, `rowspan` — от 2 до 50) и `data-align` (`center` или `right`), ни `style`, ни `colwidth`. `thead`, `tfoot`, `colgroup` и `caption` в Схеме нет и Редактор их не пишет (строка-заголовок — первая строка из `th`); `tbody`, `tr`, `th` и `td` вне таблицы парсер HTML выбрасывает сам, поэтому хук для них не нужен.
|
|
1465
|
+
|
|
1466
|
+
Других классов, `style` (кроме цвета автора), `aria-*` и `data-*` (кроме восьми имён Схемы: `data-type`, `data-checked`, `data-color`, `data-align`, `data-width`, `data-block`, `data-layout`, `data-tone`; у своих Инструментов к ним добавляются `data-mark` и `data-<имя>`) в отдаваемом HTML нет: типографика живёт на контейнере, а не на узлах Документа.
|
|
1467
|
+
|
|
1468
|
+
**`style` — узкое исключение.** Цвет автора пишется как `<span style="color:#rrggbb">` и `<mark style="background-color:#rrggbb">` и больше ничего в `style` не бывает: ни другие свойства, ни другие теги, ни другие формы цвета. `ALLOWED_ATTR` у DOMPurify общий на все теги и значений не проверяет, поэтому в голом `richTextSanitizeConfig()` `style` нет вовсе, а пускает его только `sanitizeRichText` ([RichTextView](../content/rich-text-view.md#санитизация--на-сервере-потребителя)), сверив тег и значение с `RICH_TEXT_SCHEMA.customColor.styles`.
|
|
1469
|
+
|
|
1470
|
+
**Схема — не санитизация.** Клиентская очистка от XSS не защищает: запрос в обход интерфейса принесёт что угодно. HTML от пользователя чистит сервер потребителя — `sanitizeRichText(html, DOMPurify)` по белому списку выше. Список растёт вместе с набором Блоков, и хелпер — вместе с ним; он и закрытый конфиг `richTextSanitizeConfig()` описаны на странице [RichTextView](../content/rich-text-view.md#санитизация--на-сервере-потребителя). Таблица выше повторяет `src/editor/core/schema.ts` и проверяется тестом. Скелет до Готовности показывает `value`, уже разобранный Схемой в браузере (раздел «Свой Инструмент»), а на сервере Документа не рисует, так что Редактор, в отличие от `RichTextView`, неочищенный HTML в страницу не кладёт. Серверной очистки это не отменяет: значение уходит дальше в базу и в `RichTextView`.
|
|
1471
|
+
|
|
1472
|
+
## Доступность и форма
|
|
1473
|
+
|
|
1474
|
+
- Редактируемая область — `role="textbox"` с `aria-multiline`.
|
|
1475
|
+
- **Панель** — `role="toolbar"` с именем из каталога сообщений (`editor.toolbar`, «Форматирование») и **одной остановкой Tab**: Tab входит в Панель, стрелки влево и вправо идут по кнопкам, Home и End — к краям, следующий Tab уводит в Документ. Панель запоминает, где был фокус. Собрана на `Toolbar` кита.
|
|
1476
|
+
- Кнопки — иконки с `aria-label`; кнопки-переключатели (жирный, курсив, подчёркнутый в Меню выделения, ссылка, картинка) объявляют состояние **`aria-pressed`** — скринридер слышит, включён ли курсив под кареткой. Разовые действия (отмена, повтор) — обычные кнопки. Кнопка своего Инструмента объявляет `aria-pressed`, только если у неё есть `isActive`, а её сочетание — в подсказке и `aria-keyshortcuts`; пустая подпись заменяется именем Инструмента. Недоступное сейчас действие — `aria-disabled`, а не `disabled`: оно остаётся в обходе стрелками.
|
|
1477
|
+
- Подсказка на наведение и фокус называет действие и сочетание; сочетание же объявлено `aria-keyshortcuts` (`Control+B` или `Meta+B` на Apple). На Apple подпись — `⌘+⇧+S`, у остальных — `Ctrl+Shift+S`, по тому же условию, по которому движок решает, какая клавиша `mod`.
|
|
1478
|
+
- **Меню вставки по `/`** — паттерн `combobox` из WAI-ARIA Authoring Practices Guide (APG): фокус остаётся в поле, список — `role="listbox"` с разделами `role="group"`, а поле указывает на него `aria-controls` и на текущую строку — `aria-activedescendant` (у строки `aria-selected`). Поле объявляет `aria-autocomplete="list"`. `aria-expanded` на поле не ставится: ARIA 1.2 не разрешает его `role="textbox"`, а менять роль многострочного Документа на `combobox` нельзя. Имя строки — её подпись, напоминание `##` скринридеру не читается.
|
|
1479
|
+
- Список по `/` — слой `Popover` кита (Radix): Escape закрывает только его, а не `Dialog` или `Drawer` вокруг поля, и набранное остаётся текстом. «+» — `Menu` кита с `aria-haspopup`, Escape в нём ведёт себя так же (ADR-0041).
|
|
1480
|
+
- **Справочник сочетаний** — `Popover` кита: кнопка с `aria-haspopup="dialog"` и `aria-expanded`, поповер — `role="dialog"` с именем «Сочетания клавиш», внутри `table` с заголовками столбцов (`columnheader`) и заголовком строки (`rowheader`) у каждой команды, сочетание — `<kbd>`. Escape и щелчок снаружи закрывают поповер и возвращают фокус на кнопку; в `Dialog` — только поповер (ADR-0041). Тесты: `tests/rich-text-editor-shortcuts.test.tsx`, в браузере — `tests-visual/interactions.spec.ts` («RichTextEditor inside Dialog: Escape closes the shortcuts reference»).
|
|
1481
|
+
- **Плавающая Панель** — та же `role="toolbar"` с одной остановкой Tab и теми же стрелками; появляется над непустым выделением, на сенсорном экране заменяется фиксированной. Из Документа в любую Панель — `Alt+F10` (раздел «Клавиатура: `Alt+F10`»).
|
|
1482
|
+
- **Поповер ссылки** — `role="dialog"` с именем «Ссылка» (`editor.link`); поле подписано (`editor.linkAddress`), ошибка адреса (`editor.linkInvalid`) связана с полем через `aria-describedby` и ставит `aria-invalid`. Кнопка «Ссылка» — переключатель с `aria-pressed`, когда каретка в ссылке.
|
|
1483
|
+
- **Картинка.** Поповер — `role="dialog"` с именем «Картинка» (`editor.image`); поля «Адрес картинки» и «Альтернативный текст» подписаны, у второго пояснение связано через `aria-describedby`, ошибка адреса (`editor.imageInvalid`) ставит `aria-invalid`. Пресеты ширины — `role="group"` «Ширина» из кнопок-переключателей с `aria-pressed`. Панель выделенной картинки — `role="toolbar"` «Картинка» с одной остановкой Tab. Ручки ширины — только для мыши (`aria-hidden`, без фокуса): та же ширина есть в поповере и на панели. Ход загрузки — живой регион `role="status"` вне Документа (живые регионы внутри `contenteditable` читаются не везде), отказ — видимая строка `role="alert"` под полем: это Сообщение поля, оно же у `showNotice`.
|
|
1484
|
+
- **Меню «¶»** — `Menu` кита: пункты-радио (`menuitemradio`) с отметкой `aria-checked` у текущего типа Блока, сочетания подписаны в пунктах. Enter, пробел и `↓` на кнопке открывают меню, стрелки ходят по пунктам. Escape закрывает меню и возвращает фокус на кнопку; внутри `Dialog` и `Drawer` — только меню, не окно (ADR-0041). Имя кнопки включает текущий тип: «Тип блока: Текст»; меню названо своей кнопкой. Состояние в меню, а не кнопками-переключателями в ряд: пункт с отметкой объявляет его так же, как `aria-pressed`, а значение на кнопке слышно без открытия — ADR-0054.
|
|
1485
|
+
- **Меню «…»** — `Menu` кита с кнопкой-иконкой «Ещё форматирование»: Метки и «Блок кода» — пункты-флажки (`menuitemcheckbox`) с `aria-checked`, сочетания подписаны в пунктах. Внутри `Dialog` и `Drawer` пункты выбираются и мышью, и клавиатурой, а Escape не закрывает окно, пока открыто меню (ADR-0041).
|
|
1486
|
+
- **Поповеры «Маркер» и «Цвет»** — кнопка с `aria-haspopup="menu"` и `aria-expanded`; внутри — `menu` с именем кнопки, сетки — `group` с заголовком («Цвет текста», «Цвет маркера»). Кружки — `menuitemradio` с `aria-checked` и именем **словом** («Красный», «Розовый»): цвет — не единственный способ различить (WCAG 1.4.1). «Свой цвет…» — тоже `menuitemradio`, отмечен при цвете автора; «Убрать цвет» и «Убрать маркер» — `menuitem` с подсказкой. Выбранность — кольцо вокруг кружка на подложке поповера, фокус — акцентный контур снаружи этого кольца: оба видны разом. Имя кнопки «Маркер» включает текущий цвет («Маркер: Розовый»). Обход — роли меню APG по сетке: стрелки ходят, а не выбирают.
|
|
1487
|
+
- **Поповер своего цвета** — `role="dialog"` с именем «Свой цвет текста» или «Свой цвет маркера»; фокус при открытии — на кнопке выбора цвета, чьё видимое имя называет значение («Цвет #d92d20») и меняется вместе с ним. Предупреждение о контрасте — `role="status"`: скринридер объявляет его появление. Escape закрывает один слой за раз (ADR-0041): выбор цвета, затем поповер, затем `Dialog` или `Drawer` вокруг поля; фокус возвращается в Документ.
|
|
1488
|
+
- **Меню «Выравнивание» и «Списки»** — как «¶»: кнопка с `aria-haspopup="menu"` и текущим значением в имени («Выравнивание: по центру», «Список: нумерованный»), пункты-радио с `aria-checked`, сочетания подписаны в пунктах; Enter, пробел и `↓` открывают, Escape закрывает только меню и возвращает фокус на кнопку. Выравнивание и списки — меню, а не по три кнопки-переключателя в ряд: значение, слышимое в имени кнопки, и отметка в меню объявляют то же, что `aria-pressed`, а ряд короче на четыре кнопки — ADR-0054.
|
|
1489
|
+
- **Меню «Таблица»** — `Menu` кита с подменю (`SubmenuTrigger`): кнопка с `aria-haspopup="menu"`, пункты с `›` — `aria-haspopup` и `aria-expanded`, «Строка-заголовок» — `menuitemcheckbox`, выравнивание столбца — `menuitemradio`, недоступные пункты получают `aria-disabled` и остаются в обходе стрелками. Подменю «Таблица ›» — поповер react-aria с сеткой (паттерн `grid` из APG, `aria-activedescendant`, размер словами в живой области). Escape закрывает по одному уровню, в `Dialog` окно — последним (ADR-0041). Окно «Вставить таблицу» — `Dialog` кита: фокус при открытии — в поле «Столбцы», при закрытии — обратно в Документ. Тесты: `tests/rich-text-editor-table-menu.test.tsx`, в браузере — `tests-visual/rich-text-editor-table-menu.spec.ts`.
|
|
1490
|
+
- **Эмодзи.** Кнопка — `aria-haspopup="dialog"`; поповер — `role="dialog"` с именем «Эмодзи» (`editor.emoji`) и сеткой `role="grid"` («Выберите эмодзи»), в которой фокус остаётся на сетке, а текущую ячейку называет `aria-activedescendant`, как у сетки размера таблицы. Категории — `rowgroup` с именем («Избранное», «Смайлы»…), эмодзи — `gridcell` с именем из каталога сообщений (`editor.emojiName`: «Огонь», «Красное сердце»), а не символом: скринридеры озвучивают символы по-разному, а иногда молчат. Символ в ячейке скрыт от скринридера. Escape закрывает поповер и возвращает фокус в Документ.
|
|
1491
|
+
- **Раскладка.** Колонки идут в разметке слева направо — в том же порядке их читает скринридер и в том же порядке они встают столбцом на узком экране. Своей роли у колонки нет: это группа Блоков Документа, а не виджет. Tab, который обрабатывает сам Документ (колонки, список, таблица), не уходит к ловушке фокуса `Dialog` и `Drawer` вокруг поля; Tab, который Документ пропускает (последняя колонка при Tab, первая при Shift+Tab; абзац вне списка), переводит фокус дальше — ловушки клавиатуры нет.
|
|
1492
|
+
- **Обёртка.** Своей роли у неё нет: это группа Блоков Документа, а не виджет, и скринридер читает её Блоки по порядку, как Блоки вокруг. `Tab` в Обёртке не перехватывается — ловушки клавиатуры нет; из Обёртки выводят `Enter` в пустом последнем абзаце и стрелки у её края. **Тон** роли и `aria-*` не добавляет — он только оформление: смысл заметки («Внимание: …») автор пишет словом (WCAG 1.4.1). Подменю «Тон ›» — `Menu` кита: пункты-радио с `aria-checked`, имя пункта подменю — с текущим тоном.
|
|
1493
|
+
- **HTML-режим.** Кнопка «HTML» — переключатель с `aria-pressed`, в обходе стрелками Панели, как любая кнопка. Поле — нативный `textarea` с именем из каталога сообщений («HTML-код документа», `editor.sourceField`); пояснение и ошибка `FormField` уходят в его `aria-describedby` тем же слиянием, что у Документа; `aria-invalid` зеркалит Документ. Автоисправления выключены: `spellcheck="false"`, `autocapitalize`, `autocorrect` и `autocomplete` — `off`, `dir="ltr"`. Tab и `Shift+Tab` — нативные: табуляцию поле не вставляет, ловушки клавиатуры нет; отмена внутри поля — обычная отмена текстового поля. Вход ставит фокус в поле (каретка в начале), выход кнопкой — в Документ (каретка в конце). `Alt+F10` из поля — на кнопку «HTML», как из Документа на Панель: Панель объявляет это сочетание и в режиме. Остальные кнопки Панели в режиме — `aria-disabled`, в обходе стрелками. Щелчок по подписи `FormField` и отказ `required` ведут фокус в поле HTML. Тесты: `tests/rich-text-editor-source.test.tsx`, в браузере — `tests-visual/rich-text-editor-source.spec.ts`.
|
|
1494
|
+
- **Сообщение поля и Строка статуса.** Живой регион `role="status"` стоит в каждом Редакторе и пуст, пока нет сообщения `status`; ошибка — `role="alert"` под полем. Строка статуса — контейнер без `role` и `aria-live`: кит её не озвучивает, «Сохранено» экран оборачивает в `role="status"` сам. Строка видна при `readOnly` и `disabled`, счётчик остаётся в `aria-describedby`. Тесты: `tests/rich-text-editor-notice.test.tsx`; в витрине — `StatusLine`, `StatusLineReadOnly`, `FieldNotice`.
|
|
1495
|
+
- **Карточка у текста.** `role="dialog"` без `aria-modal`, имя — подпись записи (`label`); фокус при открытии остаётся в Документе. О появлении объявляет живой регион `role="status"` фразой `editor.textCardShown` с подписью и сочетанием `Alt+F10`; `Alt+F10` переводит фокус в Карточку, Escape возвращает каретку и прячет её. См. раздел «Карточка у текста».
|
|
1496
|
+
- Флажок пункта чек-листа назван из каталога сообщений (`editor.taskCheckbox`): «Выполнено: <текст пункта>».
|
|
1497
|
+
- **Имя.** `<label for>` браузер связывает только с нативными полями, а `contenteditable` к ним не относится. Поэтому `FormField` публикует `id` своей подписи контекстом, а `RichTextEditor` внутри него ссылается на неё через `aria-labelledby` и ставит фокус в Документ по щелчку на подписи. `id` у Редактора и `htmlFor` у `FormField` для этого не нужны (но `htmlFor` проставляйте по привычке — он ничему не мешает). DOM подписи Редактор не правит, только слушает щелчок. Явные `aria-label` и `aria-labelledby` сильнее подписи. Без всего этого имя берётся из каталога сообщений (`editor.label`) — безымянного поля не бывает, но такое имя ничего не говорит о содержимом: давайте своё.
|
|
1498
|
+
- Пояснение и ошибка `FormField` объявляются вместе с полем: их `id` уходит в `aria-describedby` и **сливается** со своим описанием из пропа. Ошибка ставит `aria-invalid` и красит рамку; явный проп `aria-invalid` сильнее.
|
|
1499
|
+
- **`required` — как нативное.** Пустой Документ (нет ни текста, ни Атома — `hr`, картинки, своего Атома, — ни таблицы) блокирует отправку **обычной** `<form>`: браузер показывает всплывающее сообщение с текстом из каталога сообщений (`editor.required`, следует локали кита), а фокус уходит в Редактор. Как только в Документе появилось содержимое, блокировка снимается сразу, без ожидания `changeDelay`. В HTML-режиме пустоту решает текст поля HTML, разобранный Схемой, — тоже сразу, на наборе (раздел «HTML-режим»). `readOnly` и `disabled` не проверяются, как у нативных полей. Звёздочка у подписи ставится только через `FormField required`.
|
|
1500
|
+
- Как это устроено: скрытое `<input type="hidden" name>` из проверки ограничений **исключено спецификацией**, а `contenteditable` не form-associated, поэтому рядом с ним стоит невидимое текстовое поле без `name` — оно несёт `setCustomValidity`, а в `FormData` значение по-прежнему кладёт скрытое.
|
|
1501
|
+
- В `Form` кита (`noValidate`) браузер не проверяет ни одного поля — обязательность там выражает потребитель через `FormField error`, как и для `TextControl`.
|
|
1502
|
+
- `readOnly` и `disabled` объявляются `aria-readonly` и `aria-disabled`.
|
|
1503
|
+
- Все строки — из каталога сообщений `editor.*`, с русской локалью из `@olddevs/ui/locales/ru`. Названия клавиш в подсказках (`Ctrl`, `Shift`) не переводятся: это надписи на клавишах.
|
|
1504
|
+
|
|
1505
|
+
**Ручной прогон** — вне CI, перед выпуском значимых изменений Редактора: VoiceOver на macOS и iOS, NVDA, ввод через IME (японский и китайский), Android Chrome с Gboard, вставка из Word и Google Docs.
|
|
1506
|
+
|
|
1507
|
+
## Антипаттерны
|
|
1508
|
+
|
|
1509
|
+
- Хранить и показывать HTML Редактора без санитизации на сервере.
|
|
1510
|
+
- Хранить Markdown из `getMarkdown()` как основное значение: цвет, маркер, выравнивание и подчёркивание в нём теряются.
|
|
1511
|
+
- Грузить JSON-файл командой `ref.editor.commands.setContent(json)` в обход `setJSON()`: на Блоке выключенного Инструмента Tiptap молча заменит весь Документ пустым, атрибуты из JSON пройдут без правил Схемы, а экран не узнает о замене через `onChange`.
|
|
1512
|
+
- Хранить JSON-конверт в базе вместо HTML из `onChange`: серверная очистка кита (`sanitizeRichText`) читает HTML, а конверт — формат файла и шаблона.
|
|
1513
|
+
- Разрешать `style` на сервере в обход `sanitizeRichText` (например, дописав его в `ALLOWED_ATTR` голого конфига): Редактор пишет его только как цвет автора (`color:#rrggbb` на `span`, `background-color:#rrggbb` на `mark`), а любой другой CSS пришёл бы не от него.
|
|
1514
|
+
- Брать цвет автора там, где Документ показывается в нескольких темах: он не следует за темой, в отличие от палитры.
|
|
1515
|
+
- Ставить `changeDelay={0}` «на всякий случай» на длинных Документах: форма и так получает свежий HTML перед отправкой.
|
|
1516
|
+
- Ставить ссылку командой `ref.editor` в обход поповера с относительным адресом или схемой вне `http`/`https`/`mailto`: в HTML такая ссылка уйдёт без `href`.
|
|
1517
|
+
- Класть Редактор с `toolbar="floating"` или `"bubble"` в контейнер с `overflow: hidden` вплотную к его верхнему краю: Панель или Меню выделения над первой строкой срежется.
|
|
1518
|
+
- Перекрашивать всплывающие поверхности Редактора сплошным цветом через `className` или CSS потребителя: Стекло снимается системной настройкой прозрачности и контраста, а ручная подложка эту настройку перестаёт слушать.
|
|
1519
|
+
- Красить Меню выделения своими цветами поверх токенов: инверсная пара проверена на контраст во всех темах и брендах, а ручной цвет — нет. Свой бренд меняет `--accent-on-inverse` в своём правиле CSS, как `--accent-strong`.
|
|
1520
|
+
- Вызывать методы `ref.editor` как часть контракта кита — команды Tiptap следуют мажорам движка; частые задачи закрывает Хэндл (`insertHTML`, `getText`, `focus`, `undo`), а `editor` — выход для команд Меток, Блоков и своих Инструментов. Исключение — `showNotice`: она команда кита и подчиняется semver.
|
|
1521
|
+
- Делать кнопки своей панели без `preventDefault` на `mousedown`: клик уводит фокус из поля, и плавающая Панель вместе с Меню выделения исчезают.
|
|
1522
|
+
- Считывать `ref.current.editor.isActive(...)` в рендере своей панели вместо `useRichTextEditorState`: панель не узнает о смене выделения и не перерисуется.
|
|
1523
|
+
- Оборачивать всю Строку статуса в `role="status"`: число слов зачитывалось бы на каждую правку. Озвучивайте только то, что нужно (`Сохранено`).
|
|
1524
|
+
- Рисовать свою строку ошибки под полем, дублируя Сообщение поля (ошибки вставки и загрузки): `showNotice` уже даёт строку с `role="alert"`, и своя копия будет озвучена дважды.
|
|
1525
|
+
- Заворачивать в `RichTextEditor` короткие подписи и заголовки: это поле для Документа, а не для строки.
|
|
1526
|
+
- Прятать кнопку стилем, оставив Инструмент в `tools`: Схема его по-прежнему пропустит — из вставки и по сочетанию. Выключайте Инструмент, а не кнопку.
|
|
1527
|
+
- Менять `tools` у смонтированного поля в расчёте на то, что Схема перестроится: набор читается при создании движка.
|
|
1528
|
+
- Передавать смысл заметки одним тоном Обёртки: подложку не видит скринридер и не различает дальтоник, а в Markdown без поддержки alerts остаётся цитата. Пишите словом — «Внимание: …» (WCAG 1.4.1).
|
|
1529
|
+
- Красить текст внутри Обёртки с тоном цветом палитры или своим цветом: на цветной подложке его контраст может оказаться ниже 4.5:1 — гейт проверяет основной и вторичный текст Документа, а не цвет автора.
|
|
1530
|
+
|
|
1531
|
+
## Ссылки
|
|
1532
|
+
|
|
1533
|
+
- Исходник: `src/editor/RichTextEditor.tsx`; Панель — `src/editor/ui/EditorToolbar.tsx`, меню форматирования («¶», «…», «Выравнивание», «Списки») — `src/editor/ui/FormatMenus.tsx` и `src/editor/core/blockTypes.ts`, плавающая — `src/editor/ui/FloatingToolbar.tsx`, Меню выделения — `src/editor/ui/SelectionBubble.tsx`, общее условие показа и позиция — `src/editor/ui/selectionMenu.ts`; ссылка — `src/editor/features/link/link.ts`, описание Инструмента `src/editor/features/link/linkTool.tsx` и форма адреса `src/editor/features/link/LinkPopover.tsx`; эмодзи — описание Инструмента `src/editor/features/emoji/emojiTool.tsx` и сетка `src/editor/features/emoji/EmojiGrid.tsx`; кнопки из описаний Инструментов и рамка их поповеров — `src/editor/ui/ToolButtonView.tsx`, их сочетания — `src/editor/core/toolButtons.ts`, сочетания их Пунктов вставки и Действий над Блоком — `src/editor/core/toolEntryShortcuts.ts`; картинка — `src/editor/features/image/image.ts` (узел, загрузка, вставка файлов), `src/editor/features/image/ImagePopover.tsx`, `src/editor/features/image/ImageToolbar.tsx`; Сообщение поля — команда `src/editor/core/fieldNotice.ts`, тексты о файлах `src/editor/features/image/imageNotice.ts`, живой регион, строка ошибки и Строка статуса `src/editor/ui/FieldStatus.tsx`; свой Инструмент — публичная форма `src/editor/core/customTool.ts`, движок и проверка набора `src/editor/core/customToolEngine.ts`, разворот `tools` `src/editor/features/builtins.ts`; таблица — `src/editor/features/table/tables.ts` (узлы, слияние, выравнивание, Markdown), `src/editor/features/table/tableActions.ts` (команды меню, сортировка, память размера), `src/editor/features/table/TableMenuButton.tsx` (меню «Таблица»), `src/editor/features/table/TableSizeGrid.tsx` и `src/editor/features/table/TableInsertDialog.tsx`; Раскладка — `src/editor/features/layout/layout.ts` (узлы, команды, клавиатура) и `src/editor/features/layout/columnLayouts.ts` (варианты данными); Обёртка — `src/editor/features/wrapper/wrapper.ts` (узел, вставка, снятие, тон, клавиатура, Markdown) и `src/editor/features/wrapper/wrapperTones.ts` (тоны и метки GitHub alerts данными); Документ с контейнерами Блоков — `src/editor/core/document.ts`; Хэндл — `src/editor/core/handle.ts`, хук — `src/editor/hooks/useRichTextEditorState.ts`; HTML-режим — состояние и применение в `src/editor/RichTextEditor.tsx`, форматирование HTML — `src/editor/core/sourceFormat.ts`; Инструменты и Пресеты — `src/editor/core/tools.ts`; команды над Блоками — `src/editor/core/blockCommands.ts`; Ручка Блока — `src/editor/ui/BlockHandle.tsx`; таблица сочетаний и подписи — `src/editor/core/shortcuts.ts`, Справочник сочетаний — `src/editor/ui/ShortcutsButton.tsx`; Стекло всплывающих поверхностей — `src/editor/styles/glass.ts`; Меню вставки — `src/editor/core/insertItems.ts` (реестр пунктов), `src/editor/ui/SlashMenu.tsx`, `src/editor/ui/InsertMenuButton.tsx`, `src/editor/core/slashCommand.ts`; Схема данными — `src/editor/core/schema.ts`; палитра и выравнивания — `src/editor/styles/palette.ts`, расширения цвета — `src/editor/features/color/styleExtensions.ts`; цвет автора — форма и разбор `src/editor/features/color/customColor.ts`, контраст `src/editor/features/color/contrast.ts`, поповер `src/editor/features/color/CustomColorPopover.tsx`; типографика — `src/editor/styles/prose.ts`; блок кода — `src/editor/features/code/codeBlock.ts` (расширение, псевдонимы, `Alt+F10`), `src/editor/features/code/CodeBlockView.tsx` (шапка: язык и «Копировать»), `src/editor/features/code/CodeCopyButton.tsx` (кнопка с подсказкой), `src/editor/features/code/codeHeader.ts` (классы шапки, общие с Просмотром), Лист `surface="paper"` — `src/editor/styles/surface.ts` и `src/editor/styles/tokens.ts`, `src/editor/features/code/codeLanguages.ts` (языки данными), `src/editor/features/code/lowlight.ts` (грамматики), `src/editor/styles/codeTheme.ts` (тема подсветки синтаксиса), серверный хелпер — `src/editor/features/code/highlight.ts`; поиск и замена — `src/editor/features/search/searchTool.ts` (Инструмент), `src/editor/features/search/searchPlugin.ts` и `src/editor/features/search/SearchPanel.tsx`; JSON — `src/editor/core/jsonDocument.ts`; Высота поля — Ручка высоты `src/editor/ui/FieldResizer.tsx`; эмодзи (данные набора) — `src/editor/features/emoji/emoji.ts`; предел символов — `src/editor/core/maxLength.ts`; Markdown — `src/editor/core/markdown.ts`.
|
|
1534
|
+
- Тесты: `tests/rich-text-editor-text-card.test.tsx` (Карточка у текста), `tests/rich-text-editor.test.tsx`, `tests/rich-text-editor-handle.test.tsx` и `tests/rich-text-editor-handle-ssr.test.tsx` (Хэндл и хук), `tests/rich-text-editor-toolbar.test.tsx`, `tests/rich-text-editor-block-menu.test.tsx`, `tests/rich-text-editor-more-menu.test.tsx`, `tests/rich-text-editor-align-list-menus.test.tsx`, `tests/rich-text-editor-insert-menu.test.tsx`, `tests/rich-text-editor-schema.test.tsx`, `tests/rich-text-editor-markdown.test.tsx`, `tests/rich-text-editor-color.test.tsx`, `tests/rich-text-editor-custom-color.test.tsx`, `tests/rich-text-editor-link.test.tsx`, `tests/rich-text-editor-floating.test.tsx`, `tests/rich-text-editor-bubble.test.tsx`, `tests/rich-text-editor-glass.test.tsx`, `tests/rich-text-editor-table.test.tsx`, `tests/rich-text-editor-lazy-builtins.test.tsx` (ленивые блок кода и таблица: `basic` их не грузит, `full` — Скелет до чанков), `tests/rich-text-editor-image.test.tsx`, `tests/rich-text-editor-notice.test.tsx`, `tests/rich-text-editor-code-block.test.tsx`, `tests/rich-text-editor-block-commands.test.tsx`, `tests/rich-text-editor-block-handle.test.tsx`, `tests/rich-text-editor-shortcuts.test.tsx`, `tests/rich-text-editor-layout.test.tsx`, `tests/rich-text-editor-wrapper.test.tsx`, `tests/rich-text-editor-wrapper-tone.test.tsx`, `tests/rich-text-editor-source.test.tsx`, `tests/rich-text-editor-custom-tool.test.tsx` (свой Инструмент; тестовые Инструменты — `tests/editor-test-tools.ts`), `tests/rich-text-editor-custom-ui.test.tsx` (Пункты вставки и Действия над Блоком своего Инструмента), `tests/rich-text-editor-trigger.test.tsx` (свой символ-триггер: открытие, выбор, запрос, асинхронные пункты, ошибки набора), `tests/rich-text-editor-custom-button.test.tsx` (кнопки своего Инструмента), `tests/rich-text-editor-custom-markdown.test.tsx` (Markdown своего Блока по умолчанию и по своим правилам), `tests/rich-text-editor-atom-text.test.tsx` (Текст атома, вес в `maxLength`, пустота), `tests/rich-text-editor-empty-table.test.tsx` (пустая таблица — содержимое), `tests/rich-text-editor-max-height.test.tsx` (`maxHeight` и `resizable`: что внутри области прокрутки, место Ручки Блока при прокрутке), `tests/rich-text-editor-events.test.tsx` (`onReady`, `onFocus`, `onBlur`, порядок вставки и drop), `tests/rich-text-editor-loaders.test.tsx` (Загрузчики Части для движка, «Повторить»), `tests/rich-text-editor-skeleton.test.tsx` (Скелет), `tests/rich-text-editor-emoji.test.tsx` (эмодзи), `tests/rich-text-editor-json.test.tsx` (JSON), `tests/editor-search.test.tsx`, `tests/editor-search-panel.test.tsx` и `tests/editor-search-replace.test.tsx` (поиск и замена), `tests/rich-text-paper.test.tsx`, `tests/rich-text-paper-css.test.tsx`, `tests/editor-entry.test.ts`, `tests/editor-schema-entry.test.ts`, `tests/editor-highlight-entry.test.ts`; в браузере — `tests-visual/interactions.spec.ts` («RichTextEditor inside Dialog» — Escape в списке стилей, в Меню вставки, в поповере ссылки и в меню языка блока кода; «RichTextEditor inside-dialog» и «inside-drawer» — меню «…» и поповеры цвета мышью и клавиатурой; «RichTextEditor insert menu»; «RichTextEditor block commands» — настоящие `⌥⇧↑/↓`, `⌘]`, `⌘[` и `⌘⇧D`), `tests-visual/rich-text-editor-image.spec.ts` (перетаскивание файла и ручки ширины), `tests-visual/rich-text-editor-bubble.spec.ts` (Меню выделения в `Dialog`, `Drawer` и контейнере прокрутки, у края окна, инверсная поверхность в трёх темах), `tests-visual/rich-text-editor-layout.spec.ts` (Раскладка в `Dialog`: колонки рядом и Tab по ним, стрелки, столбец на телефоне), `tests-visual/rich-text-editor-wrapper.spec.ts` (Обёртка: пунктир в Редакторе и без рамки в `RichTextView`, тон одним видом в обоих, выход по Enter, `↑` из Обёртки), `tests-visual/rich-text-editor-block-handle.spec.ts` (меню ручки в `Dialog`: `Ctrl/⌘+/`, Escape по уровням; место ручки у первой строки Блока), `tests-visual/rich-text-editor-trigger.spec.ts` (свой символ-триггер: список у каретки, настоящий ввод `@`, выбор мышью и Enter, `a@b` внутри слова, axe в трёх темах), `tests-visual/rich-text-editor-source.spec.ts` (HTML-режим: высота поля, рост по содержимому, фокус на входе и выходе), `tests-visual/rich-text-editor-height.spec.ts` (Высота поля), `tests-visual/rich-text-editor-emoji.spec.ts` (эмодзи), `tests-visual/rich-text-editor-skeleton.spec.ts` (Скелет).
|
|
1535
|
+
- API расширения одной картой, «Стабильность» и рецепты — [API расширения Редактора](../../api/editor-extension.md).
|
|
1536
|
+
- Показ без правки — [RichTextView](../content/rich-text-view.md).
|
|
1537
|
+
- Storybook: `Inputs/RichTextEditor`.
|
|
1538
|
+
- Происхождение кода: minimal-tiptap (MIT); цвета темы подсветки синтаксиса — highlight.js (BSD-3-Clause); лицензии — в `NOTICE` пакета.
|