@eocrm/design-system 0.0.0-stage → 0.6.18
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/AI-PRIMER.md +341 -0
- package/LICENSE +202 -0
- package/README.md +266 -2
- package/docs/components/Accordion.md +114 -0
- package/docs/components/AgentAvatar.md +58 -0
- package/docs/components/Alert.md +81 -0
- package/docs/components/AppLayout.md +70 -0
- package/docs/components/Avatar.md +49 -0
- package/docs/components/AvatarGroup.md +47 -0
- package/docs/components/Badge.md +60 -0
- package/docs/components/Banner.md +89 -0
- package/docs/components/BrandIcon.md +32 -0
- package/docs/components/Breadcrumb.md +72 -0
- package/docs/components/Button.md +107 -0
- package/docs/components/ButtonGroup.md +78 -0
- package/docs/components/Calendar.md +158 -0
- package/docs/components/Card.md +165 -0
- package/docs/components/CatalogPicker.md +83 -0
- package/docs/components/Chart.md +114 -0
- package/docs/components/ChatBubble.md +55 -0
- package/docs/components/ChatLauncher.md +97 -0
- package/docs/components/Checkbox.md +55 -0
- package/docs/components/CircularProgress.md +53 -0
- package/docs/components/Cluster.md +96 -0
- package/docs/components/Code.md +35 -0
- package/docs/components/CodeBlock.md +67 -0
- package/docs/components/ColorPicker.md +115 -0
- package/docs/components/ConfirmationPopover.md +70 -0
- package/docs/components/Constrain.md +39 -0
- package/docs/components/CurrencySelect.md +57 -0
- package/docs/components/CursorPagination.md +43 -0
- package/docs/components/DashboardCanvas.md +79 -0
- package/docs/components/DashboardWidget.md +131 -0
- package/docs/components/DataTable.md +88 -0
- package/docs/components/DatePicker.md +68 -0
- package/docs/components/DateRangePicker.md +71 -0
- package/docs/components/DateStrip.md +56 -0
- package/docs/components/DefinitionList.md +82 -0
- package/docs/components/Divider.md +57 -0
- package/docs/components/Dot.md +26 -0
- package/docs/components/Drawer.md +94 -0
- package/docs/components/DropdownMenu.md +299 -0
- package/docs/components/EmojiPicker.md +84 -0
- package/docs/components/EmptyState.md +55 -0
- package/docs/components/EntityChip.md +102 -0
- package/docs/components/ErrorState.md +72 -0
- package/docs/components/Field.md +91 -0
- package/docs/components/FileUpload.md +109 -0
- package/docs/components/FilterChip.md +88 -0
- package/docs/components/FlowCanvas.md +108 -0
- package/docs/components/FormRow.md +31 -0
- package/docs/components/FormSection.md +37 -0
- package/docs/components/Grid.md +103 -0
- package/docs/components/Highlight.md +75 -0
- package/docs/components/HighlightedText.md +50 -0
- package/docs/components/IconPicker.md +134 -0
- package/docs/components/IconTile.md +30 -0
- package/docs/components/Image.md +84 -0
- package/docs/components/ImageCrop.md +163 -0
- package/docs/components/Indent.md +31 -0
- package/docs/components/InlineDatePicker.md +58 -0
- package/docs/components/InlineDateRangePicker.md +57 -0
- package/docs/components/Input.md +54 -0
- package/docs/components/Kanban.md +122 -0
- package/docs/components/Kbd.md +41 -0
- package/docs/components/Lightbox.md +105 -0
- package/docs/components/Link.md +65 -0
- package/docs/components/LinkCard.md +41 -0
- package/docs/components/LiquidEditor.md +84 -0
- package/docs/components/List.md +68 -0
- package/docs/components/LiveRegion.md +29 -0
- package/docs/components/LocaleProvider.md +25 -0
- package/docs/components/Logo.md +41 -0
- package/docs/components/Masonry.md +48 -0
- package/docs/components/MediaTile.md +95 -0
- package/docs/components/Modal.md +107 -0
- package/docs/components/MoneyInput.md +62 -0
- package/docs/components/OptionsPicker.md +123 -0
- package/docs/components/OtpInput.md +65 -0
- package/docs/components/Page.md +35 -0
- package/docs/components/PageHeader.md +133 -0
- package/docs/components/Pagination.md +44 -0
- package/docs/components/Palette.md +32 -0
- package/docs/components/PasswordInput.md +45 -0
- package/docs/components/PasswordStrengthMeter.md +27 -0
- package/docs/components/PersonDisplay.md +153 -0
- package/docs/components/PhoneInput.md +54 -0
- package/docs/components/PillMenu.md +91 -0
- package/docs/components/Popover.md +127 -0
- package/docs/components/Progress.md +56 -0
- package/docs/components/QrCode.md +34 -0
- package/docs/components/Radio.md +37 -0
- package/docs/components/RadioGroup.md +56 -0
- package/docs/components/Rail.md +191 -0
- package/docs/components/RelativeDateInput.md +114 -0
- package/docs/components/RichText.md +44 -0
- package/docs/components/RichTextEditor.md +112 -0
- package/docs/components/Screen.md +37 -0
- package/docs/components/ScrollArea.md +39 -0
- package/docs/components/Select.md +131 -0
- package/docs/components/SettingRow.md +98 -0
- package/docs/components/Skeleton.md +68 -0
- package/docs/components/Slider.md +92 -0
- package/docs/components/SlotGrid.md +55 -0
- package/docs/components/SocialButton.md +31 -0
- package/docs/components/Sortable.md +127 -0
- package/docs/components/SortableGroup.md +52 -0
- package/docs/components/Sparkline.md +51 -0
- package/docs/components/Split.md +76 -0
- package/docs/components/Stack.md +59 -0
- package/docs/components/StagePath.md +66 -0
- package/docs/components/StatGroup.md +61 -0
- package/docs/components/StatTile.md +58 -0
- package/docs/components/Sticky.md +69 -0
- package/docs/components/Switch.md +89 -0
- package/docs/components/Table.md +184 -0
- package/docs/components/TableOfContents.md +76 -0
- package/docs/components/Tabs.md +94 -0
- package/docs/components/Text.md +63 -0
- package/docs/components/Textarea.md +59 -0
- package/docs/components/Thread.md +52 -0
- package/docs/components/TimeField.md +70 -0
- package/docs/components/Timeline.md +57 -0
- package/docs/components/Title.md +54 -0
- package/docs/components/ToastViewport.md +61 -0
- package/docs/components/Tooltip.md +63 -0
- package/docs/components/TopBar.md +214 -0
- package/docs/components/Tour.md +85 -0
- package/docs/components/TreeView.md +113 -0
- package/docs/components/VisuallyHidden.md +43 -0
- package/docs/components/WidgetPreview.md +54 -0
- package/docs/components/useBelowBreakpoint.md +11 -0
- package/docs/components/useMonth.md +18 -0
- package/docs/setup.md +51 -0
- package/docs/theming.md +95 -0
- package/docs/tokens.md +123 -0
- package/docs/transient-state.md +56 -0
- package/package.json +86 -4
- package/src/_meta/manifest.ts +293 -0
- package/src/app/AppProvider.tsx +78 -0
- package/src/app/index.ts +3 -0
- package/src/app/themeTokens.ts +95 -0
- package/src/calendar/dateMath.ts +114 -0
- package/src/calendar/formatters.ts +75 -0
- package/src/calendar/index.ts +39 -0
- package/src/calendar/types.ts +36 -0
- package/src/calendar/useAgenda.ts +70 -0
- package/src/calendar/useDay.ts +51 -0
- package/src/calendar/useMonth.ts +82 -0
- package/src/calendar/useWeek.ts +62 -0
- package/src/calendar/weekInfo.ts +83 -0
- package/src/components/Accordion/Accordion.module.scss +304 -0
- package/src/components/Accordion/Accordion.tokens.scss +64 -0
- package/src/components/Accordion/Accordion.tsx +272 -0
- package/src/components/Accordion/AccordionContent.tsx +36 -0
- package/src/components/Accordion/AccordionItem.tsx +66 -0
- package/src/components/Accordion/AccordionTrigger.tsx +121 -0
- package/src/components/Accordion/context.ts +47 -0
- package/src/components/Accordion/index.ts +14 -0
- package/src/components/Alert/Alert.module.scss +92 -0
- package/src/components/Alert/Alert.tokens.scss +60 -0
- package/src/components/Alert/Alert.tsx +136 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/AppLayout/AppLayout.module.scss +130 -0
- package/src/components/AppLayout/AppLayout.tokens.scss +27 -0
- package/src/components/AppLayout/AppLayout.tsx +211 -0
- package/src/components/AppLayout/index.ts +2 -0
- package/src/components/Avatar/AgentAvatar.module.scss +117 -0
- package/src/components/Avatar/AgentAvatar.tsx +122 -0
- package/src/components/Avatar/Avatar.module.scss +181 -0
- package/src/components/Avatar/Avatar.tokens.scss +107 -0
- package/src/components/Avatar/Avatar.tsx +210 -0
- package/src/components/Avatar/AvatarGroup.module.scss +128 -0
- package/src/components/Avatar/AvatarGroup.tsx +119 -0
- package/src/components/Avatar/AvatarGroupContext.tsx +20 -0
- package/src/components/Avatar/index.ts +6 -0
- package/src/components/Badge/Badge.module.scss +124 -0
- package/src/components/Badge/Badge.tokens.scss +74 -0
- package/src/components/Badge/Badge.tsx +164 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/Banner/Banner.module.scss +73 -0
- package/src/components/Banner/Banner.tokens.scss +51 -0
- package/src/components/Banner/Banner.tsx +138 -0
- package/src/components/Banner/index.ts +2 -0
- package/src/components/BrandIcon/BrandIcon.module.scss +5 -0
- package/src/components/BrandIcon/BrandIcon.tsx +99 -0
- package/src/components/BrandIcon/index.ts +2 -0
- package/src/components/Breadcrumb/Breadcrumb.module.scss +32 -0
- package/src/components/Breadcrumb/Breadcrumb.tokens.scss +9 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +146 -0
- package/src/components/Breadcrumb/index.ts +2 -0
- package/src/components/Button/Button.module.scss +167 -0
- package/src/components/Button/Button.tokens.scss +90 -0
- package/src/components/Button/Button.tsx +205 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/ButtonGroup/ButtonGroup.module.scss +134 -0
- package/src/components/ButtonGroup/ButtonGroup.tokens.scss +42 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +278 -0
- package/src/components/ButtonGroup/ButtonGroupItem.tsx +74 -0
- package/src/components/ButtonGroup/context.ts +45 -0
- package/src/components/ButtonGroup/index.ts +2 -0
- package/src/components/Calendar/AgendaView.module.scss +231 -0
- package/src/components/Calendar/AgendaView.tsx +214 -0
- package/src/components/Calendar/AllDayBand.module.scss +18 -0
- package/src/components/Calendar/AllDayBand.tsx +75 -0
- package/src/components/Calendar/Calendar.module.scss +23 -0
- package/src/components/Calendar/Calendar.tokens.scss +262 -0
- package/src/components/Calendar/Calendar.tsx +370 -0
- package/src/components/Calendar/DayCell.module.scss +30 -0
- package/src/components/Calendar/DayCell.tsx +50 -0
- package/src/components/Calendar/DayView.module.scss +10 -0
- package/src/components/Calendar/DayView.tsx +163 -0
- package/src/components/Calendar/EventChip.module.scss +171 -0
- package/src/components/Calendar/EventChip.tsx +115 -0
- package/src/components/Calendar/HourGrid.module.scss +123 -0
- package/src/components/Calendar/HourGrid.tsx +381 -0
- package/src/components/Calendar/MonthView.module.scss +99 -0
- package/src/components/Calendar/MonthView.tsx +296 -0
- package/src/components/Calendar/TimedEvent.module.scss +205 -0
- package/src/components/Calendar/TimedEvent.tsx +247 -0
- package/src/components/Calendar/ViewSwitcher.module.scss +3 -0
- package/src/components/Calendar/ViewSwitcher.tsx +47 -0
- package/src/components/Calendar/WeekView.module.scss +10 -0
- package/src/components/Calendar/WeekView.tsx +137 -0
- package/src/components/Calendar/eventColor.ts +60 -0
- package/src/components/Calendar/index.ts +18 -0
- package/src/components/Calendar/types.ts +451 -0
- package/src/components/Calendar/useEventDrag.ts +783 -0
- package/src/components/Calendar/utils.ts +622 -0
- package/src/components/Card/Card.module.scss +264 -0
- package/src/components/Card/Card.tokens.scss +55 -0
- package/src/components/Card/Card.tsx +170 -0
- package/src/components/Card/CardBody.tsx +45 -0
- package/src/components/Card/CardHeader.tsx +113 -0
- package/src/components/Card/CardList.tsx +24 -0
- package/src/components/Card/CardListRow.tsx +24 -0
- package/src/components/Card/index.ts +7 -0
- package/src/components/CatalogPicker/CatalogPicker.module.scss +193 -0
- package/src/components/CatalogPicker/CatalogPicker.tokens.scss +26 -0
- package/src/components/CatalogPicker/CatalogPicker.tsx +339 -0
- package/src/components/CatalogPicker/index.ts +2 -0
- package/src/components/Chart/Chart.module.scss +287 -0
- package/src/components/Chart/Chart.tokens.scss +63 -0
- package/src/components/Chart/Chart.tsx +607 -0
- package/src/components/Chart/ChartLegend.tsx +99 -0
- package/src/components/Chart/ChartTable.tsx +62 -0
- package/src/components/Chart/ChartTooltip.tsx +91 -0
- package/src/components/Chart/chartModel.ts +689 -0
- package/src/components/Chart/index.ts +2 -0
- package/src/components/ChatBubble/ChatBubble.module.scss +93 -0
- package/src/components/ChatBubble/ChatBubble.tokens.scss +17 -0
- package/src/components/ChatBubble/ChatBubble.tsx +61 -0
- package/src/components/ChatBubble/ChatBubbleList.tsx +27 -0
- package/src/components/ChatBubble/index.ts +3 -0
- package/src/components/ChatLauncher/ChatLauncher.module.scss +68 -0
- package/src/components/ChatLauncher/ChatLauncher.tokens.scss +27 -0
- package/src/components/ChatLauncher/ChatLauncher.tsx +202 -0
- package/src/components/ChatLauncher/index.ts +2 -0
- package/src/components/ChatLauncher/isAtBottom.ts +12 -0
- package/src/components/Checkbox/Checkbox.module.scss +147 -0
- package/src/components/Checkbox/Checkbox.tokens.scss +44 -0
- package/src/components/Checkbox/Checkbox.tsx +188 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/CircularProgress/CircularProgress.module.scss +133 -0
- package/src/components/CircularProgress/CircularProgress.tokens.scss +40 -0
- package/src/components/CircularProgress/CircularProgress.tsx +139 -0
- package/src/components/CircularProgress/index.ts +7 -0
- package/src/components/Cluster/Cluster.module.scss +145 -0
- package/src/components/Cluster/Cluster.tokens.scss +11 -0
- package/src/components/Cluster/Cluster.tsx +215 -0
- package/src/components/Cluster/index.ts +2 -0
- package/src/components/Code/Code.module.scss +28 -0
- package/src/components/Code/Code.tokens.scss +15 -0
- package/src/components/Code/Code.tsx +44 -0
- package/src/components/Code/index.ts +2 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +146 -0
- package/src/components/CodeBlock/CodeBlock.tokens.scss +27 -0
- package/src/components/CodeBlock/CodeBlock.tsx +274 -0
- package/src/components/CodeBlock/highlight.ts +179 -0
- package/src/components/CodeBlock/index.ts +8 -0
- package/src/components/CodeBlock/sampleSync.ts +42 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +286 -0
- package/src/components/ColorPicker/ColorPicker.tokens.scss +60 -0
- package/src/components/ColorPicker/ColorPicker.tsx +367 -0
- package/src/components/ColorPicker/ColorPickerPanel.tsx +265 -0
- package/src/components/ColorPicker/SVSquare.tsx +189 -0
- package/src/components/ColorPicker/colorMath.ts +141 -0
- package/src/components/ColorPicker/index.ts +6 -0
- package/src/components/ConfirmationPopover/ConfirmationPopover.module.scss +39 -0
- package/src/components/ConfirmationPopover/ConfirmationPopover.tokens.scss +13 -0
- package/src/components/ConfirmationPopover/ConfirmationPopover.tsx +395 -0
- package/src/components/ConfirmationPopover/index.ts +2 -0
- package/src/components/Constrain/Constrain.module.scss +99 -0
- package/src/components/Constrain/Constrain.tsx +79 -0
- package/src/components/Constrain/index.ts +2 -0
- package/src/components/CurrencySelect/CurrencySelect.module.scss +7 -0
- package/src/components/CurrencySelect/CurrencySelect.tsx +167 -0
- package/src/components/CurrencySelect/index.ts +3 -0
- package/src/components/CursorPagination/CursorPagination.module.scss +69 -0
- package/src/components/CursorPagination/CursorPagination.tokens.scss +32 -0
- package/src/components/CursorPagination/CursorPagination.tsx +121 -0
- package/src/components/CursorPagination/index.ts +2 -0
- package/src/components/DashboardCanvas/DashboardCanvas.module.scss +360 -0
- package/src/components/DashboardCanvas/DashboardCanvas.tokens.scss +48 -0
- package/src/components/DashboardCanvas/DashboardCanvas.tsx +1270 -0
- package/src/components/DashboardCanvas/DashboardCanvasItem.tsx +140 -0
- package/src/components/DashboardCanvas/DashboardCanvasSection.tsx +174 -0
- package/src/components/DashboardCanvas/engine.ts +363 -0
- package/src/components/DashboardCanvas/index.ts +9 -0
- package/src/components/DashboardWidget/DashboardWidget.module.scss +100 -0
- package/src/components/DashboardWidget/DashboardWidget.tokens.scss +16 -0
- package/src/components/DashboardWidget/DashboardWidget.tsx +188 -0
- package/src/components/DashboardWidget/index.ts +7 -0
- package/src/components/DataTable/BodyRow.tsx +197 -0
- package/src/components/DataTable/ColumnVisibilityTrigger.module.scss +7 -0
- package/src/components/DataTable/ColumnVisibilityTrigger.tsx +105 -0
- package/src/components/DataTable/DataTable.module.scss +511 -0
- package/src/components/DataTable/DataTable.tokens.scss +80 -0
- package/src/components/DataTable/DataTable.tsx +734 -0
- package/src/components/DataTable/HeaderCell.module.scss +215 -0
- package/src/components/DataTable/HeaderCell.tsx +440 -0
- package/src/components/DataTable/columnShift.ts +117 -0
- package/src/components/DataTable/index.ts +23 -0
- package/src/components/DataTable/pinStyle.ts +47 -0
- package/src/components/DataTable/reorderColumns.ts +66 -0
- package/src/components/DataTable/types.ts +289 -0
- package/src/components/DataTable/useColumnDragShift.ts +216 -0
- package/src/components/DataTable/useDataTable.ts +372 -0
- package/src/components/DataTable/useResizeHandle.ts +97 -0
- package/src/components/DatePicker/DatePicker.module.scss +180 -0
- package/src/components/DatePicker/DatePicker.tokens.scss +152 -0
- package/src/components/DatePicker/DatePicker.tsx +664 -0
- package/src/components/DatePicker/DatePickerGrid.module.scss +172 -0
- package/src/components/DatePicker/DatePickerGrid.tsx +341 -0
- package/src/components/DatePicker/InlineDatePicker.module.scss +32 -0
- package/src/components/DatePicker/InlineDatePicker.tsx +201 -0
- package/src/components/DatePicker/index.ts +4 -0
- package/src/components/DatePicker/utils.ts +364 -0
- package/src/components/DateRangePicker/DateRangePicker.module.scss +237 -0
- package/src/components/DateRangePicker/DateRangePicker.tokens.scss +78 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +720 -0
- package/src/components/DateRangePicker/InlineDateRangePicker.module.scss +94 -0
- package/src/components/DateRangePicker/InlineDateRangePicker.tsx +376 -0
- package/src/components/DateRangePicker/index.ts +5 -0
- package/src/components/DateRangePicker/utils.ts +151 -0
- package/src/components/DateStrip/DateStrip.module.scss +78 -0
- package/src/components/DateStrip/DateStrip.tokens.scss +23 -0
- package/src/components/DateStrip/DateStrip.tsx +205 -0
- package/src/components/DateStrip/index.ts +2 -0
- package/src/components/DefinitionList/DefinitionList.module.scss +102 -0
- package/src/components/DefinitionList/DefinitionList.tokens.scss +29 -0
- package/src/components/DefinitionList/DefinitionList.tsx +198 -0
- package/src/components/DefinitionList/index.ts +9 -0
- package/src/components/Divider/Divider.module.scss +144 -0
- package/src/components/Divider/Divider.tokens.scss +21 -0
- package/src/components/Divider/Divider.tsx +103 -0
- package/src/components/Divider/index.ts +2 -0
- package/src/components/Dot/Dot.module.scss +40 -0
- package/src/components/Dot/Dot.tokens.scss +4 -0
- package/src/components/Dot/Dot.tsx +45 -0
- package/src/components/Dot/index.ts +2 -0
- package/src/components/Drawer/Body.tsx +23 -0
- package/src/components/Drawer/Close.tsx +35 -0
- package/src/components/Drawer/Content.tsx +93 -0
- package/src/components/Drawer/Drawer.module.scss +232 -0
- package/src/components/Drawer/Drawer.tokens.scss +38 -0
- package/src/components/Drawer/Drawer.tsx +23 -0
- package/src/components/Drawer/DrawerRoot.tsx +190 -0
- package/src/components/Drawer/Footer.tsx +24 -0
- package/src/components/Drawer/Header.tsx +64 -0
- package/src/components/Drawer/Overlay.tsx +82 -0
- package/src/components/Drawer/context.ts +44 -0
- package/src/components/Drawer/index.ts +12 -0
- package/src/components/Drawer/useDragToClose.ts +149 -0
- package/src/components/DropdownMenu/CheckboxItem.tsx +144 -0
- package/src/components/DropdownMenu/Content.tsx +419 -0
- package/src/components/DropdownMenu/DropdownMenu.module.scss +280 -0
- package/src/components/DropdownMenu/DropdownMenu.tokens.scss +88 -0
- package/src/components/DropdownMenu/DropdownMenu.tsx +43 -0
- package/src/components/DropdownMenu/Group.tsx +76 -0
- package/src/components/DropdownMenu/Item.tsx +148 -0
- package/src/components/DropdownMenu/ItemIndicator.tsx +30 -0
- package/src/components/DropdownMenu/Radio.tsx +178 -0
- package/src/components/DropdownMenu/Root.tsx +100 -0
- package/src/components/DropdownMenu/Sub.tsx +332 -0
- package/src/components/DropdownMenu/Trigger.tsx +76 -0
- package/src/components/DropdownMenu/context.ts +65 -0
- package/src/components/DropdownMenu/index.ts +20 -0
- package/src/components/EmojiPicker/EmojiPicker.module.scss +84 -0
- package/src/components/EmojiPicker/EmojiPicker.tokens.scss +24 -0
- package/src/components/EmojiPicker/EmojiPicker.tsx +341 -0
- package/src/components/EmojiPicker/emojiData.ts +245 -0
- package/src/components/EmojiPicker/index.ts +3 -0
- package/src/components/EmptyState/EmptyState.module.scss +102 -0
- package/src/components/EmptyState/EmptyState.tokens.scss +28 -0
- package/src/components/EmptyState/EmptyState.tsx +112 -0
- package/src/components/EmptyState/index.ts +7 -0
- package/src/components/EntityChip/EntityChip.module.scss +275 -0
- package/src/components/EntityChip/EntityChip.tokens.scss +66 -0
- package/src/components/EntityChip/EntityChip.tsx +649 -0
- package/src/components/EntityChip/index.ts +10 -0
- package/src/components/ErrorState/ErrorState.module.scss +105 -0
- package/src/components/ErrorState/ErrorState.tokens.scss +32 -0
- package/src/components/ErrorState/ErrorState.tsx +125 -0
- package/src/components/ErrorState/index.ts +8 -0
- package/src/components/Field/Field.module.scss +46 -0
- package/src/components/Field/Field.tsx +185 -0
- package/src/components/Field/index.ts +2 -0
- package/src/components/FileUpload/FileUpload.module.scss +187 -0
- package/src/components/FileUpload/FileUpload.tokens.scss +81 -0
- package/src/components/FileUpload/FileUpload.tsx +569 -0
- package/src/components/FileUpload/formatBytes.ts +34 -0
- package/src/components/FileUpload/iconForFile.ts +40 -0
- package/src/components/FileUpload/index.ts +2 -0
- package/src/components/FilterChip/FilterChip.module.scss +61 -0
- package/src/components/FilterChip/FilterChip.tokens.scss +25 -0
- package/src/components/FilterChip/FilterChip.tsx +208 -0
- package/src/components/FilterChip/index.ts +5 -0
- package/src/components/FlowCanvas/FlowCanvas.module.scss +329 -0
- package/src/components/FlowCanvas/FlowCanvas.tokens.scss +45 -0
- package/src/components/FlowCanvas/FlowCanvas.tsx +1867 -0
- package/src/components/FlowCanvas/FlowControls.tsx +46 -0
- package/src/components/FlowCanvas/FlowEdge.tsx +58 -0
- package/src/components/FlowCanvas/FlowNode.tsx +76 -0
- package/src/components/FlowCanvas/edgePath.ts +85 -0
- package/src/components/FlowCanvas/index.ts +4 -0
- package/src/components/FlowCanvas/layout.ts +148 -0
- package/src/components/FlowCanvas/spatialNav.ts +76 -0
- package/src/components/FlowCanvas/types.ts +59 -0
- package/src/components/FlowCanvas/useViewport.ts +122 -0
- package/src/components/FormRow/FormRow.module.scss +2 -0
- package/src/components/FormRow/FormRow.tsx +37 -0
- package/src/components/FormRow/index.ts +2 -0
- package/src/components/FormSection/FormSection.module.scss +24 -0
- package/src/components/FormSection/FormSection.tsx +46 -0
- package/src/components/FormSection/index.ts +2 -0
- package/src/components/Grid/Grid.module.scss +162 -0
- package/src/components/Grid/Grid.tokens.scss +13 -0
- package/src/components/Grid/Grid.tsx +226 -0
- package/src/components/Grid/GridItem.tsx +77 -0
- package/src/components/Grid/index.ts +11 -0
- package/src/components/Highlight/Highlight.module.scss +43 -0
- package/src/components/Highlight/Highlight.tokens.scss +35 -0
- package/src/components/Highlight/Highlight.tsx +164 -0
- package/src/components/Highlight/HighlightOverlay.tsx +100 -0
- package/src/components/Highlight/index.ts +2 -0
- package/src/components/HighlightedText/HighlightedText.module.scss +11 -0
- package/src/components/HighlightedText/HighlightedText.tokens.scss +9 -0
- package/src/components/HighlightedText/HighlightedText.tsx +50 -0
- package/src/components/HighlightedText/highlightRanges.ts +44 -0
- package/src/components/HighlightedText/index.ts +2 -0
- package/src/components/IconPicker/IconPicker.module.scss +175 -0
- package/src/components/IconPicker/IconPicker.tokens.scss +40 -0
- package/src/components/IconPicker/IconPicker.tsx +629 -0
- package/src/components/IconPicker/index.ts +9 -0
- package/src/components/IconTile/IconTile.module.scss +57 -0
- package/src/components/IconTile/IconTile.tokens.scss +16 -0
- package/src/components/IconTile/IconTile.tsx +91 -0
- package/src/components/IconTile/index.ts +2 -0
- package/src/components/Image/Image.module.scss +266 -0
- package/src/components/Image/Image.tokens.scss +55 -0
- package/src/components/Image/Image.tsx +342 -0
- package/src/components/Image/index.ts +2 -0
- package/src/components/ImageCrop/ImageCrop.module.scss +274 -0
- package/src/components/ImageCrop/ImageCrop.tokens.scss +33 -0
- package/src/components/ImageCrop/ImageCrop.tsx +542 -0
- package/src/components/ImageCrop/cropGeometry.ts +120 -0
- package/src/components/ImageCrop/extractCropBlob.ts +140 -0
- package/src/components/ImageCrop/index.ts +11 -0
- package/src/components/ImageCrop/useCropPreview.ts +161 -0
- package/src/components/Indent/Indent.module.scss +33 -0
- package/src/components/Indent/Indent.tokens.scss +10 -0
- package/src/components/Indent/Indent.tsx +46 -0
- package/src/components/Indent/index.ts +2 -0
- package/src/components/Input/Input.module.scss +99 -0
- package/src/components/Input/Input.tokens.scss +54 -0
- package/src/components/Input/Input.tsx +105 -0
- package/src/components/Input/Suggestions.tsx +260 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Kanban/Kanban.module.scss +39 -0
- package/src/components/Kanban/Kanban.tokens.scss +15 -0
- package/src/components/Kanban/Kanban.tsx +951 -0
- package/src/components/Kanban/index.ts +7 -0
- package/src/components/Kbd/Kbd.module.scss +47 -0
- package/src/components/Kbd/Kbd.tokens.scss +26 -0
- package/src/components/Kbd/Kbd.tsx +73 -0
- package/src/components/Kbd/index.ts +2 -0
- package/src/components/Lightbox/Lightbox.module.scss +283 -0
- package/src/components/Lightbox/Lightbox.tokens.scss +36 -0
- package/src/components/Lightbox/Lightbox.tsx +506 -0
- package/src/components/Lightbox/index.ts +2 -0
- package/src/components/Link/Link.module.scss +78 -0
- package/src/components/Link/Link.tokens.scss +28 -0
- package/src/components/Link/Link.tsx +114 -0
- package/src/components/Link/index.ts +2 -0
- package/src/components/LinkCard/LinkCard.module.scss +92 -0
- package/src/components/LinkCard/LinkCard.tokens.scss +9 -0
- package/src/components/LinkCard/LinkCard.tsx +72 -0
- package/src/components/LinkCard/index.ts +2 -0
- package/src/components/LiquidEditor/AutocompleteMenu.tsx +133 -0
- package/src/components/LiquidEditor/HighlightLayer.tsx +53 -0
- package/src/components/LiquidEditor/InsertVariableMenu.tsx +81 -0
- package/src/components/LiquidEditor/LiquidEditor.module.scss +339 -0
- package/src/components/LiquidEditor/LiquidEditor.tokens.scss +81 -0
- package/src/components/LiquidEditor/LiquidEditor.tsx +412 -0
- package/src/components/LiquidEditor/PreviewPane.tsx +29 -0
- package/src/components/LiquidEditor/caretCoordinates.ts +97 -0
- package/src/components/LiquidEditor/index.ts +7 -0
- package/src/components/LiquidEditor/liquidFilters.ts +23 -0
- package/src/components/LiquidEditor/liquidTokenizer.ts +235 -0
- package/src/components/LiquidEditor/types.ts +101 -0
- package/src/components/LiquidEditor/useLiquidAutocomplete.ts +178 -0
- package/src/components/List/List.module.scss +105 -0
- package/src/components/List/List.tokens.scss +42 -0
- package/src/components/List/List.tsx +124 -0
- package/src/components/List/index.ts +9 -0
- package/src/components/LiveRegion/LiveRegion.module.scss +3 -0
- package/src/components/LiveRegion/LiveRegion.tsx +116 -0
- package/src/components/LiveRegion/index.ts +2 -0
- package/src/components/Logo/Logo.module.scss +139 -0
- package/src/components/Logo/Logo.tokens.scss +14 -0
- package/src/components/Logo/Logo.tsx +126 -0
- package/src/components/Logo/index.ts +2 -0
- package/src/components/Masonry/Masonry.module.scss +42 -0
- package/src/components/Masonry/Masonry.tokens.scss +8 -0
- package/src/components/Masonry/Masonry.tsx +136 -0
- package/src/components/Masonry/index.ts +2 -0
- package/src/components/Masonry/masonryUtils.ts +42 -0
- package/src/components/MediaTile/MediaTile.module.scss +183 -0
- package/src/components/MediaTile/MediaTile.tokens.scss +60 -0
- package/src/components/MediaTile/MediaTile.tsx +208 -0
- package/src/components/MediaTile/index.ts +8 -0
- package/src/components/Modal/Body.tsx +27 -0
- package/src/components/Modal/Close.tsx +45 -0
- package/src/components/Modal/Content.tsx +98 -0
- package/src/components/Modal/Footer.tsx +28 -0
- package/src/components/Modal/Header.tsx +51 -0
- package/src/components/Modal/Modal.module.scss +262 -0
- package/src/components/Modal/Modal.tokens.scss +50 -0
- package/src/components/Modal/Modal.tsx +23 -0
- package/src/components/Modal/ModalRoot.tsx +299 -0
- package/src/components/Modal/Overlay.tsx +101 -0
- package/src/components/Modal/context.ts +60 -0
- package/src/components/Modal/index.ts +11 -0
- package/src/components/MoneyInput/MoneyInput.module.scss +57 -0
- package/src/components/MoneyInput/MoneyInput.tsx +433 -0
- package/src/components/MoneyInput/index.ts +2 -0
- package/src/components/OptionsPicker/OptionsPicker.module.scss +216 -0
- package/src/components/OptionsPicker/OptionsPicker.tokens.scss +68 -0
- package/src/components/OptionsPicker/OptionsPicker.tsx +936 -0
- package/src/components/OptionsPicker/index.ts +9 -0
- package/src/components/OtpInput/OtpInput.module.scss +69 -0
- package/src/components/OtpInput/OtpInput.tokens.scss +45 -0
- package/src/components/OtpInput/OtpInput.tsx +368 -0
- package/src/components/OtpInput/index.ts +2 -0
- package/src/components/Page/Page.module.scss +30 -0
- package/src/components/Page/Page.tokens.scss +11 -0
- package/src/components/Page/Page.tsx +53 -0
- package/src/components/Page/index.ts +2 -0
- package/src/components/PageHeader/PageHeader.module.scss +197 -0
- package/src/components/PageHeader/PageHeader.tokens.scss +41 -0
- package/src/components/PageHeader/PageHeader.tsx +310 -0
- package/src/components/PageHeader/index.ts +20 -0
- package/src/components/Pagination/Pagination.module.scss +117 -0
- package/src/components/Pagination/Pagination.tokens.scss +54 -0
- package/src/components/Pagination/Pagination.tsx +165 -0
- package/src/components/Pagination/index.ts +4 -0
- package/src/components/Pagination/paginationRange.ts +72 -0
- package/src/components/PasswordInput/PasswordInput.module.scss +120 -0
- package/src/components/PasswordInput/PasswordInput.tokens.scss +44 -0
- package/src/components/PasswordInput/PasswordInput.tsx +210 -0
- package/src/components/PasswordInput/index.ts +2 -0
- package/src/components/PasswordStrengthMeter/PasswordStrengthMeter.module.scss +80 -0
- package/src/components/PasswordStrengthMeter/PasswordStrengthMeter.tokens.scss +40 -0
- package/src/components/PasswordStrengthMeter/PasswordStrengthMeter.tsx +96 -0
- package/src/components/PasswordStrengthMeter/index.ts +2 -0
- package/src/components/PersonDisplay/PersonDisplay.module.scss +180 -0
- package/src/components/PersonDisplay/PersonDisplay.tokens.scss +35 -0
- package/src/components/PersonDisplay/PersonDisplay.tsx +420 -0
- package/src/components/PersonDisplay/index.ts +10 -0
- package/src/components/PhoneInput/PhoneInput.module.scss +23 -0
- package/src/components/PhoneInput/PhoneInput.tsx +271 -0
- package/src/components/PhoneInput/index.ts +4 -0
- package/src/components/PhoneInput/phone.ts +128 -0
- package/src/components/PillMenu/PillMenu.module.scss +117 -0
- package/src/components/PillMenu/PillMenu.tokens.scss +22 -0
- package/src/components/PillMenu/PillMenu.tsx +324 -0
- package/src/components/PillMenu/index.ts +2 -0
- package/src/components/Popover/Anchor.tsx +37 -0
- package/src/components/Popover/Close.tsx +45 -0
- package/src/components/Popover/Content.tsx +213 -0
- package/src/components/Popover/Heading.tsx +33 -0
- package/src/components/Popover/Popover.module.scss +177 -0
- package/src/components/Popover/Popover.tokens.scss +22 -0
- package/src/components/Popover/Popover.tsx +23 -0
- package/src/components/Popover/PopoverRoot.tsx +75 -0
- package/src/components/Popover/Trigger.tsx +86 -0
- package/src/components/Popover/context.ts +22 -0
- package/src/components/Popover/index.ts +7 -0
- package/src/components/Progress/Progress.module.scss +110 -0
- package/src/components/Progress/Progress.tokens.scss +44 -0
- package/src/components/Progress/Progress.tsx +129 -0
- package/src/components/Progress/index.ts +2 -0
- package/src/components/QrCode/QrCode.module.scss +97 -0
- package/src/components/QrCode/QrCode.tokens.scss +17 -0
- package/src/components/QrCode/QrCode.tsx +233 -0
- package/src/components/QrCode/index.ts +2 -0
- package/src/components/QrCode/qr.ts +161 -0
- package/src/components/Radio/Radio.module.scss +175 -0
- package/src/components/Radio/Radio.tokens.scss +59 -0
- package/src/components/Radio/Radio.tsx +127 -0
- package/src/components/Radio/RadioGroup.module.scss +42 -0
- package/src/components/Radio/RadioGroup.tsx +120 -0
- package/src/components/Radio/RadioGroupContext.tsx +30 -0
- package/src/components/Radio/index.ts +4 -0
- package/src/components/Rail/Rail.module.scss +510 -0
- package/src/components/Rail/Rail.tokens.scss +92 -0
- package/src/components/Rail/Rail.tsx +287 -0
- package/src/components/Rail/RailCollapseToggle.tsx +57 -0
- package/src/components/Rail/RailFooter.tsx +17 -0
- package/src/components/Rail/RailGroup.tsx +728 -0
- package/src/components/Rail/RailGroupContext.ts +9 -0
- package/src/components/Rail/RailHeader.tsx +23 -0
- package/src/components/Rail/RailItem.tsx +119 -0
- package/src/components/Rail/RailSection.tsx +39 -0
- package/src/components/Rail/RailSpacer.tsx +19 -0
- package/src/components/Rail/index.ts +9 -0
- package/src/components/RelativeDateInput/RelativeDateInput.module.scss +15 -0
- package/src/components/RelativeDateInput/RelativeDateInput.tokens.scss +6 -0
- package/src/components/RelativeDateInput/RelativeDateInput.tsx +474 -0
- package/src/components/RelativeDateInput/index.ts +10 -0
- package/src/components/RelativeDateInput/relativeDate.ts +127 -0
- package/src/components/RichText/RichText.module.scss +5 -0
- package/src/components/RichText/RichText.tsx +70 -0
- package/src/components/RichText/RichTextAttachment.module.scss +82 -0
- package/src/components/RichText/RichTextAttachment.tsx +122 -0
- package/src/components/RichText/_prose.scss +79 -0
- package/src/components/RichText/attachmentIcon.tsx +24 -0
- package/src/components/RichText/engine/attachment.ts +154 -0
- package/src/components/RichText/engine/autolink.ts +64 -0
- package/src/components/RichText/engine/blockUnit.ts +174 -0
- package/src/components/RichText/engine/colorMarks.ts +85 -0
- package/src/components/RichText/engine/escape.ts +11 -0
- package/src/components/RichText/engine/fromHtml.ts +353 -0
- package/src/components/RichText/engine/fromMarkdown.ts +20 -0
- package/src/components/RichText/engine/inlines.ts +82 -0
- package/src/components/RichText/engine/listDepths.ts +32 -0
- package/src/components/RichText/engine/marks.ts +80 -0
- package/src/components/RichText/engine/mdToHtml.ts +178 -0
- package/src/components/RichText/engine/model.ts +157 -0
- package/src/components/RichText/engine/position.ts +118 -0
- package/src/components/RichText/engine/renderDoc.tsx +395 -0
- package/src/components/RichText/engine/renderLink.ts +15 -0
- package/src/components/RichText/engine/renderMention.ts +16 -0
- package/src/components/RichText/engine/safeHref.ts +22 -0
- package/src/components/RichText/engine/toHtml.ts +154 -0
- package/src/components/RichText/engine/toMarkdown.ts +97 -0
- package/src/components/RichText/engine/transforms.ts +434 -0
- package/src/components/RichText/imageMaxHeightStyle.ts +10 -0
- package/src/components/RichText/index.ts +31 -0
- package/src/components/RichTextEditor/RichTextAttachmentConfig.tsx +264 -0
- package/src/components/RichTextEditor/RichTextBlockControls.tsx +365 -0
- package/src/components/RichTextEditor/RichTextBlockMenu.tsx +162 -0
- package/src/components/RichTextEditor/RichTextColorMenu.tsx +115 -0
- package/src/components/RichTextEditor/RichTextEditor.module.scss +298 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +1910 -0
- package/src/components/RichTextEditor/RichTextImageResizer.tsx +131 -0
- package/src/components/RichTextEditor/RichTextLinkEditor.tsx +151 -0
- package/src/components/RichTextEditor/RichTextMentionMenu.tsx +99 -0
- package/src/components/RichTextEditor/RichTextToolbar.tsx +379 -0
- package/src/components/RichTextEditor/autolinkInput.ts +65 -0
- package/src/components/RichTextEditor/blockDrop.ts +9 -0
- package/src/components/RichTextEditor/blockReflow.ts +62 -0
- package/src/components/RichTextEditor/commands.ts +300 -0
- package/src/components/RichTextEditor/history.ts +93 -0
- package/src/components/RichTextEditor/icons.tsx +152 -0
- package/src/components/RichTextEditor/imageSize.ts +70 -0
- package/src/components/RichTextEditor/index.ts +5 -0
- package/src/components/RichTextEditor/input.ts +209 -0
- package/src/components/RichTextEditor/inputRules.ts +71 -0
- package/src/components/RichTextEditor/links.ts +103 -0
- package/src/components/RichTextEditor/mentionContext.ts +37 -0
- package/src/components/RichTextEditor/mentions.ts +28 -0
- package/src/components/RichTextEditor/preventSelectionLoss.ts +15 -0
- package/src/components/RichTextEditor/selection.ts +415 -0
- package/src/components/RichTextEditor/shortcuts.ts +44 -0
- package/src/components/RichTextEditor/useAnchoredFloating.ts +50 -0
- package/src/components/RichTextEditor/useAttachmentConfig.ts +134 -0
- package/src/components/RichTextEditor/useDismissOnOutsidePointerDown.ts +26 -0
- package/src/components/RichTextEditor/useLinkEditor.ts +107 -0
- package/src/components/RichTextEditor/useMention.ts +226 -0
- package/src/components/RichTextEditor/useOverlayRect.ts +73 -0
- package/src/components/RichTextEditor/useUpload.ts +246 -0
- package/src/components/Screen/Screen.module.scss +46 -0
- package/src/components/Screen/Screen.tokens.scss +20 -0
- package/src/components/Screen/Screen.tsx +97 -0
- package/src/components/Screen/index.ts +2 -0
- package/src/components/ScrollArea/ScrollArea.module.scss +39 -0
- package/src/components/ScrollArea/ScrollArea.tokens.scss +15 -0
- package/src/components/ScrollArea/ScrollArea.tsx +154 -0
- package/src/components/ScrollArea/index.ts +2 -0
- package/src/components/Select/Chip.tsx +45 -0
- package/src/components/Select/Empty.tsx +39 -0
- package/src/components/Select/Error.tsx +51 -0
- package/src/components/Select/HiddenInputs.tsx +44 -0
- package/src/components/Select/Listbox.tsx +557 -0
- package/src/components/Select/Loading.tsx +30 -0
- package/src/components/Select/Select.module.scss +512 -0
- package/src/components/Select/Select.tokens.scss +140 -0
- package/src/components/Select/Select.tsx +685 -0
- package/src/components/Select/Trigger.tsx +952 -0
- package/src/components/Select/context.ts +101 -0
- package/src/components/Select/emptyStateText.ts +19 -0
- package/src/components/Select/index.ts +9 -0
- package/src/components/Select/useSelectState.ts +157 -0
- package/src/components/Select/utils-types.ts +7 -0
- package/src/components/Select/utils.ts +122 -0
- package/src/components/SettingRow/SettingRow.module.scss +166 -0
- package/src/components/SettingRow/SettingRow.tokens.scss +37 -0
- package/src/components/SettingRow/SettingRow.tsx +242 -0
- package/src/components/SettingRow/index.ts +7 -0
- package/src/components/Skeleton/Skeleton.module.scss +55 -0
- package/src/components/Skeleton/Skeleton.tokens.scss +17 -0
- package/src/components/Skeleton/Skeleton.tsx +131 -0
- package/src/components/Skeleton/index.ts +4 -0
- package/src/components/Skeleton/useSkeletonVisibility.ts +95 -0
- package/src/components/Slider/Slider.module.scss +293 -0
- package/src/components/Slider/Slider.tokens.scss +98 -0
- package/src/components/Slider/Slider.tsx +624 -0
- package/src/components/Slider/index.ts +9 -0
- package/src/components/SlotGrid/SlotGrid.module.scss +61 -0
- package/src/components/SlotGrid/SlotGrid.tokens.scss +21 -0
- package/src/components/SlotGrid/SlotGrid.tsx +111 -0
- package/src/components/SlotGrid/index.ts +2 -0
- package/src/components/SocialButton/SocialButton.module.scss +4 -0
- package/src/components/SocialButton/SocialButton.tsx +40 -0
- package/src/components/SocialButton/index.ts +2 -0
- package/src/components/Sortable/Sortable.module.scss +177 -0
- package/src/components/Sortable/Sortable.tokens.scss +27 -0
- package/src/components/Sortable/Sortable.tsx +572 -0
- package/src/components/Sortable/containerAwareCollision.ts +125 -0
- package/src/components/Sortable/index.ts +13 -0
- package/src/components/SortableGroup/SortableGroup.module.scss +23 -0
- package/src/components/SortableGroup/SortableGroup.tsx +315 -0
- package/src/components/SortableGroup/index.ts +4 -0
- package/src/components/SortableGroup/moveSortableItem.ts +39 -0
- package/src/components/Sparkline/Sparkline.module.scss +28 -0
- package/src/components/Sparkline/Sparkline.tokens.scss +7 -0
- package/src/components/Sparkline/Sparkline.tsx +133 -0
- package/src/components/Sparkline/index.ts +2 -0
- package/src/components/Sparkline/sparklineModel.ts +119 -0
- package/src/components/Split/Split.module.scss +158 -0
- package/src/components/Split/Split.tokens.scss +10 -0
- package/src/components/Split/Split.tsx +231 -0
- package/src/components/Split/index.ts +2 -0
- package/src/components/Stack/Stack.module.scss +67 -0
- package/src/components/Stack/Stack.tokens.scss +11 -0
- package/src/components/Stack/Stack.tsx +100 -0
- package/src/components/Stack/index.ts +2 -0
- package/src/components/StagePath/StagePath.module.scss +255 -0
- package/src/components/StagePath/StagePath.tokens.scss +43 -0
- package/src/components/StagePath/StagePath.tsx +176 -0
- package/src/components/StagePath/index.ts +2 -0
- package/src/components/StatGroup/StatGroup.module.scss +6 -0
- package/src/components/StatGroup/StatGroup.tsx +70 -0
- package/src/components/StatGroup/StatGroupContext.ts +10 -0
- package/src/components/StatGroup/StatTile.module.scss +99 -0
- package/src/components/StatGroup/StatTile.tokens.scss +35 -0
- package/src/components/StatGroup/StatTile.tsx +143 -0
- package/src/components/StatGroup/index.ts +4 -0
- package/src/components/Sticky/Sticky.module.scss +98 -0
- package/src/components/Sticky/Sticky.tokens.scss +17 -0
- package/src/components/Sticky/Sticky.tsx +103 -0
- package/src/components/Sticky/index.ts +2 -0
- package/src/components/Switch/Switch.module.scss +182 -0
- package/src/components/Switch/Switch.tokens.scss +59 -0
- package/src/components/Switch/Switch.tsx +214 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Table/Table.module.scss +308 -0
- package/src/components/Table/Table.tokens.scss +79 -0
- package/src/components/Table/Table.tsx +381 -0
- package/src/components/Table/index.ts +15 -0
- package/src/components/TableOfContents/TableOfContents.module.scss +83 -0
- package/src/components/TableOfContents/TableOfContents.tokens.scss +26 -0
- package/src/components/TableOfContents/TableOfContents.tsx +192 -0
- package/src/components/TableOfContents/activeSection.ts +18 -0
- package/src/components/TableOfContents/index.ts +2 -0
- package/src/components/Tabs/Tabs.module.scss +359 -0
- package/src/components/Tabs/Tabs.tokens.scss +50 -0
- package/src/components/Tabs/Tabs.tsx +538 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Text/Text.module.scss +115 -0
- package/src/components/Text/Text.tokens.scss +55 -0
- package/src/components/Text/Text.tsx +197 -0
- package/src/components/Text/index.ts +2 -0
- package/src/components/Textarea/Textarea.module.scss +94 -0
- package/src/components/Textarea/Textarea.tokens.scss +48 -0
- package/src/components/Textarea/Textarea.tsx +246 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/Thread/Thread.module.scss +161 -0
- package/src/components/Thread/Thread.tokens.scss +32 -0
- package/src/components/Thread/Thread.tsx +176 -0
- package/src/components/Thread/index.ts +2 -0
- package/src/components/TimeField/TimeField.module.scss +232 -0
- package/src/components/TimeField/TimeField.tsx +793 -0
- package/src/components/TimeField/index.ts +2 -0
- package/src/components/Timeline/Timeline.module.scss +58 -0
- package/src/components/Timeline/Timeline.tokens.scss +13 -0
- package/src/components/Timeline/Timeline.tsx +53 -0
- package/src/components/Timeline/index.ts +2 -0
- package/src/components/Title/Title.module.scss +84 -0
- package/src/components/Title/Title.tokens.scss +37 -0
- package/src/components/Title/Title.tsx +150 -0
- package/src/components/Title/index.ts +2 -0
- package/src/components/Toast/Toast.module.scss +299 -0
- package/src/components/Toast/Toast.tokens.scss +65 -0
- package/src/components/Toast/Toast.tsx +122 -0
- package/src/components/Toast/ToastViewport.tsx +166 -0
- package/src/components/Toast/api.ts +175 -0
- package/src/components/Toast/index.ts +5 -0
- package/src/components/Toast/store.ts +133 -0
- package/src/components/Toast/useToastTimer.ts +94 -0
- package/src/components/Tooltip/Tooltip.module.scss +99 -0
- package/src/components/Tooltip/Tooltip.tokens.scss +23 -0
- package/src/components/Tooltip/Tooltip.tsx +341 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/components/TopBar/TopBar.module.scss +235 -0
- package/src/components/TopBar/TopBar.tokens.scss +79 -0
- package/src/components/TopBar/TopBar.tsx +87 -0
- package/src/components/TopBar/TopBarEnd.tsx +20 -0
- package/src/components/TopBar/TopBarIconButton.tsx +86 -0
- package/src/components/TopBar/TopBarSearch.tsx +319 -0
- package/src/components/TopBar/TopBarSearchPanel.tsx +268 -0
- package/src/components/TopBar/TopBarStart.tsx +20 -0
- package/src/components/TopBar/index.ts +11 -0
- package/src/components/Tour/Spotlight.tsx +121 -0
- package/src/components/Tour/Tour.module.scss +210 -0
- package/src/components/Tour/Tour.tokens.scss +58 -0
- package/src/components/Tour/Tour.tsx +617 -0
- package/src/components/Tour/index.ts +2 -0
- package/src/components/Tour/useTourTarget.ts +115 -0
- package/src/components/TreeView/TreeView.module.scss +268 -0
- package/src/components/TreeView/TreeView.tokens.scss +36 -0
- package/src/components/TreeView/TreeView.tsx +536 -0
- package/src/components/TreeView/index.ts +3 -0
- package/src/components/TreeView/treeModel.ts +224 -0
- package/src/components/VisuallyHidden/VisuallyHidden.module.scss +7 -0
- package/src/components/VisuallyHidden/VisuallyHidden.tsx +29 -0
- package/src/components/VisuallyHidden/index.ts +2 -0
- package/src/components/WidgetPreview/WidgetPreview.module.scss +11 -0
- package/src/components/WidgetPreview/WidgetPreview.tokens.scss +26 -0
- package/src/components/WidgetPreview/WidgetPreview.tsx +36 -0
- package/src/components/WidgetPreview/WidgetShape.module.scss +140 -0
- package/src/components/WidgetPreview/WidgetShape.tsx +159 -0
- package/src/components/WidgetPreview/index.ts +2 -0
- package/src/components/_internal/Trend.tsx +27 -0
- package/src/components/_internal/choiceTile.scss +74 -0
- package/src/components/_internal/collapse.scss +10 -0
- package/src/components/_internal/collapse.ts +58 -0
- package/src/components/_internal/currency.ts +257 -0
- package/src/components/_internal/dragAnnouncements.ts +200 -0
- package/src/components/_internal/fieldWiring.ts +225 -0
- package/src/components/_internal/gridSpan.ts +54 -0
- package/src/components/_internal/isoDay.ts +35 -0
- package/src/components/_internal/letterKey.ts +26 -0
- package/src/components/_internal/overlay/focusRestore.ts +21 -0
- package/src/components/_internal/overlay/index.ts +12 -0
- package/src/components/_internal/overlay/tourHold.ts +23 -0
- package/src/components/_internal/overlay/useFocusTrap.ts +120 -0
- package/src/components/_internal/overlay/useInOverlay.ts +81 -0
- package/src/components/_internal/overlay/useOverlayStack.ts +232 -0
- package/src/components/_internal/overlay/useScrollLock.ts +112 -0
- package/src/components/_internal/refs.ts +48 -0
- package/src/components/_internal/statusColor.ts +26 -0
- package/src/components/_internal/timeUtils.ts +23 -0
- package/src/components/_internal/useAsyncOptions.ts +145 -0
- package/src/components/_internal/useClippedTooltip.ts +50 -0
- package/src/components/_internal/useControllableState.ts +50 -0
- package/src/components.manifest.json +1141 -0
- package/src/hooks/useBelowBreakpoint.ts +58 -0
- package/src/i18n/I18nProvider.tsx +52 -0
- package/src/i18n/LocaleProvider.tsx +17 -0
- package/src/i18n/en.ts +648 -0
- package/src/i18n/format.ts +71 -0
- package/src/i18n/index.ts +13 -0
- package/src/i18n/messages.ts +1275 -0
- package/src/i18n/ru.ts +677 -0
- package/src/i18n/useLocale.ts +19 -0
- package/src/i18n/useTranslation.ts +58 -0
- package/src/index.ts +913 -0
- package/src/palette/index.ts +2 -0
- package/src/palette/palette.ts +108 -0
- package/src/styles/dark.scss +4 -0
- package/src/styles/global.scss +7 -0
- package/src/styles/mixins.scss +143 -0
- package/src/styles/reset.scss +78 -0
- package/src/styles/tokens.scss +18 -0
- package/src/styles/typography.scss +44 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# `<Radio>` — single radio button
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<Radio name="plan" value="pro" checked={plan === 'pro'} onChange={setPlan} label="Pro" />
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
<!-- props:start -->
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
|
|
11
|
+
<!-- prettier-ignore -->
|
|
12
|
+
| Prop | Type | Required | Description |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
| `value` | `string` | yes | The value submitted when this radio is selected. |
|
|
15
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | no | Ring diameter + label type scale. Defaults to `'md'`. - `'sm'` — 14px ring, font-size-sm label. - `'md'` — 16px ring, font-size-md label. - `'lg'` — 20px ring, font-size-lg label. Inside `<RadioGroup>`, the group's `size` becomes the default; explicit per-radio `size` still wins. |
|
|
16
|
+
| `checked` | `boolean` | no | Controlled checked state. Inside `<RadioGroup>`, leave this unset — the group computes `checked` from its `value`. If you explicitly set `checked` on a Radio inside a group, your prop wins and the group's controlled invariant breaks (don't do this). |
|
|
17
|
+
| `defaultChecked` | `boolean` | no | Initial checked state for uncontrolled standalone use. |
|
|
18
|
+
| `label` | `ReactNode` | no | Label rendered next to the ring. The whole `<label>` is the click target. Omit for icon-only radios + pass `aria-label`. |
|
|
19
|
+
| `invalid` | `boolean` | no | Toggles the error visual + `aria-invalid='true'`. Inside a `<RadioGroup>`, the group's `invalid` becomes the default for this radio — explicit per-child `invalid` (including `false`) still wins. |
|
|
20
|
+
| `onChange` | `((value: string, event: ChangeEvent<HTMLInputElement, Element>) => void)` | no | Fires when the radio is selected. Receives the radio's `value` AND the native event. Inside a group, this runs FIRST, then the group's `onChange` fires. |
|
|
21
|
+
| …native | | | plus native `<input>` attributes |
|
|
22
|
+
|
|
23
|
+
<!-- props:end -->
|
|
24
|
+
|
|
25
|
+
- Native `<input type='radio'>` is visually hidden but stays in tab order + AT tree — browser arrow-key navigation between same-`name` radios works for free.
|
|
26
|
+
- **Prefer `<RadioGroup>`** for proper fieldset/legend a11y and centralized state. Standalone `<Radio>` is for embedding a single radio next to other controls.
|
|
27
|
+
|
|
28
|
+
Inside a group (preferred): `<RadioGroup name="size" defaultValue="md" label="T-shirt size"><Radio value="sm" label="Small" />…</RadioGroup>`.
|
|
29
|
+
|
|
30
|
+
**When NOT to use:** 10+ options — `<Select>`; multi-select — a set of `<Checkbox>`es; a single binary on/off — `<Switch>`.
|
|
31
|
+
|
|
32
|
+
**Anti-patterns**
|
|
33
|
+
|
|
34
|
+
- ❌ Standalone radios without a wrapping `<fieldset>` — fails AT grouping. Use `<RadioGroup>`.
|
|
35
|
+
- ❌ Setting `checked` on a Radio inside a `<RadioGroup>` — the group's `value` already controls each child's checked state.
|
|
36
|
+
- ❌ Omitting both `label` and `aria-label` — the radio is unlabelled to AT.
|
|
37
|
+
- Inside a group, a Radio's `onChange` runs first, then the group's `onChange`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# `<RadioGroup>` — fieldset wrapper for related radios
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<RadioGroup name="size" defaultValue="md" label="T-shirt size">
|
|
5
|
+
<Radio value="sm" label="Small" />
|
|
6
|
+
<Radio value="md" label="Medium" />
|
|
7
|
+
<Radio value="lg" label="Large" />
|
|
8
|
+
</RadioGroup>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
<!-- props:start -->
|
|
12
|
+
|
|
13
|
+
## Props
|
|
14
|
+
|
|
15
|
+
<!-- prettier-ignore -->
|
|
16
|
+
| Prop | Type | Required | Description |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| `name` | `string` | yes | Form `name` shared by all radio children. Required. |
|
|
19
|
+
| `value` | `string` | no | Controlled selected value. Pair with `onChange`. |
|
|
20
|
+
| `defaultValue` | `string` | no | Initial selected value for uncontrolled use. |
|
|
21
|
+
| `onChange` | `((value: string, event: ChangeEvent<HTMLInputElement, Element>) => void)` | no | Fires when the user selects a different radio. Receives the new value AND the native event so consumers can read modifier keys, etc. |
|
|
22
|
+
| `label` | `ReactNode` | no | Optional group label, rendered as `<legend>`. |
|
|
23
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | no | Default size for child radios. Per-child explicit `size` wins. Defaults to `'md'`. |
|
|
24
|
+
| `orientation` | `'vertical' \| 'horizontal'` | no | Layout direction. `'vertical'` (default) / `'horizontal'`. |
|
|
25
|
+
| `disabled` | `boolean` | no | Disable every child. Per-child explicit `disabled` wins. |
|
|
26
|
+
| `invalid` | `boolean` | no | Apply the invalid visual to every child + set `aria-invalid` on the fieldset. |
|
|
27
|
+
| `required` | `boolean` | no | Mark every child as required (HTML form validation). |
|
|
28
|
+
| `children` | `ReactNode` | yes | |
|
|
29
|
+
| …native | | | plus native `<fieldset>` attributes |
|
|
30
|
+
|
|
31
|
+
<!-- props:end -->
|
|
32
|
+
|
|
33
|
+
- Renders `<fieldset>` + optional `<legend>` for AT grouping. Children should be `<Radio>`s.
|
|
34
|
+
- `FormData.get(name)` returns the selected value on native `<form>` submit.
|
|
35
|
+
- The group's `value` drives each child's `checked` — don't set `checked` per-child inside a group (the group already does it).
|
|
36
|
+
- Per-child `onChange` fires BEFORE the group's `onChange` (both run on every selection — `preventDefault` does NOT gate the group's state update). Use per-child handlers for side-effects scoped to one option; the group's handler is the single source of truth for the selected value.
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Controlled
|
|
40
|
+
const [plan, setPlan] = useState('free');
|
|
41
|
+
<RadioGroup name="plan" value={plan} onChange={setPlan} label="Plan">
|
|
42
|
+
<Radio value="free" label="Free" />
|
|
43
|
+
<Radio value="pro" label="Pro" />
|
|
44
|
+
</RadioGroup>
|
|
45
|
+
|
|
46
|
+
// Horizontal layout
|
|
47
|
+
<RadioGroup name="orientation" defaultValue="left" orientation="horizontal">
|
|
48
|
+
<Radio value="left" label="Left" />
|
|
49
|
+
<Radio value="center" label="Center" />
|
|
50
|
+
</RadioGroup>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**When NOT to use:** 10+ options — `<Select>`; multi-select — a list of `<Checkbox>`es (no group component yet).
|
|
54
|
+
|
|
55
|
+
- ❌ Setting `checked` on the child `<Radio>`s — the group handles that.
|
|
56
|
+
- ❌ Setting a per-radio `name` inside a group — overridden by the group's `name`.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# `<Rail>` — collapsible left-side navigation
|
|
2
|
+
|
|
3
|
+
Vertical nav anchored to one side of a page. Switches between a wide labelled mode (240px) and a narrow icon-only mode (56px) via a CSS width transition. Sections of items, parent groups with subitems, and a hover-popover that surfaces a collapsed group's subitems without expanding the rail.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Rail } from '@eocrm/design-system';
|
|
7
|
+
import { NavLink } from 'react-router-dom';
|
|
8
|
+
import { Home, Users, Settings } from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
<Rail defaultCollapsed={false} aria-label="Main navigation">
|
|
11
|
+
<Rail.Header>
|
|
12
|
+
<BrandLogo />
|
|
13
|
+
</Rail.Header>
|
|
14
|
+
|
|
15
|
+
<Rail.Section title="Main">
|
|
16
|
+
<Rail.Item icon={<Home />} as={NavLink} to="/" end>
|
|
17
|
+
Dashboard
|
|
18
|
+
</Rail.Item>
|
|
19
|
+
<Rail.Item icon={<Users />} as={NavLink} to="/contacts" badge="12">
|
|
20
|
+
Contacts
|
|
21
|
+
</Rail.Item>
|
|
22
|
+
</Rail.Section>
|
|
23
|
+
|
|
24
|
+
<Rail.Section title="Operations">
|
|
25
|
+
<Rail.Group icon={<Settings />} label="Settings">
|
|
26
|
+
<Rail.Item as={NavLink} to="/settings/general">
|
|
27
|
+
General
|
|
28
|
+
</Rail.Item>
|
|
29
|
+
<Rail.Item as={NavLink} to="/settings/security">
|
|
30
|
+
Security
|
|
31
|
+
</Rail.Item>
|
|
32
|
+
</Rail.Group>
|
|
33
|
+
</Rail.Section>
|
|
34
|
+
|
|
35
|
+
<Rail.Footer>
|
|
36
|
+
<Rail.CollapseToggle />
|
|
37
|
+
</Rail.Footer>
|
|
38
|
+
</Rail>;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
<!-- props:start -->
|
|
42
|
+
|
|
43
|
+
## Props
|
|
44
|
+
|
|
45
|
+
### `RailProps`
|
|
46
|
+
|
|
47
|
+
<!-- prettier-ignore -->
|
|
48
|
+
| Prop | Type | Required | Description |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `collapsed` | `boolean` | no | Controlled collapsed flag. Provide alongside `onCollapsedChange` to drive the rail's narrow/wide state externally (e.g. persisted to localStorage or URL). Omit both `collapsed` and `defaultCollapsed` to let the rail stay expanded. |
|
|
51
|
+
| `defaultCollapsed` | `boolean` | no | Initial collapsed state for the uncontrolled case. Has no effect when `collapsed` is provided. Defaults to `false` — the rail mounts expanded. |
|
|
52
|
+
| `onCollapsedChange` | `((collapsed: boolean) => void)` | no | Fires whenever the rail toggles, both controlled and uncontrolled. Useful for persisting the state to localStorage or the URL. |
|
|
53
|
+
| `collapseBelow` | `'sm' \| 'md' \| 'lg'` | no | Force the icon-only collapsed mode while the **viewport** is at or below a width threshold: `'sm'` 480px / `'md'` 640px / `'lg'` 768px. Omit for no responsive behavior (the default) — `collapsed` alone governs. Below the threshold the rail renders collapsed regardless of `collapsed` / `defaultCollapsed`; above it, the consumer's value governs again, untouched. It is a **presentation override, not a user choice**, so `onCollapsedChange` does NOT fire when the viewport crosses the threshold in either direction — a narrow window can't silently rewrite a persisted preference. `<Rail.CollapseToggle>` hides itself while the override is active, since it could not change anything. Unlike `<Grid collapseBelow>` / `<Split collapseBelow>`, which measure their own width with a container query, this measures the viewport: the rail's own width is exactly what collapsing changes, so a container query would be circular. Same token scale, different basis. ⚠️ **Your shell's column track must follow.** The override shrinks the rail but is invisible to the layout around it — a shell that sizes its rail track from its OWN `collapsed` state will keep a 240px track around a 56px rail and leave a dead gap on every screen. Key the track off the viewport or off the rail instead, both of which work precisely because the basis here is the viewport rather than a container: (example below) |
|
|
54
|
+
| `aria-label` | `string` | no | Accessible label for the wrapping `<nav>` landmark. Defaults to `t('rail.navigation')` when omitted OR empty — an empty string is not an explicit name — so screen readers always announce a name for the region. Override when the page has multiple navigation rails. |
|
|
55
|
+
| `children` | `ReactNode` | yes | Rail children — typically a sequence of `<Rail.Header>`, `<Rail.Section>`, `<Rail.Group>`, `<Rail.Spacer>`, `<Rail.Footer>`. Order matters: a leading `<Rail.Header>` is pinned above the scroll box, and the first `<Rail.Footer>` is the split point — everything before it goes in the scroll box, the Footer and anything after it stay outside and pinned to the bottom. A `<Rail.Spacer>` is not needed for that; use it to push a trailing *section* (settings, help) down inside the scrolling area. |
|
|
56
|
+
| …native | | | plus native HTML attributes |
|
|
57
|
+
|
|
58
|
+
**`collapseBelow`** example:
|
|
59
|
+
|
|
60
|
+
<!-- prettier-ignore -->
|
|
61
|
+
```scss
|
|
62
|
+
// Either mirror the breakpoint: a (max-width: 768px) media query setting
|
|
63
|
+
// .shell { grid-template-columns: 56px 1fr; }…
|
|
64
|
+
// …or follow the rail's own state attribute, no breakpoint duplication:
|
|
65
|
+
.shell:has(nav[data-collapsed]) { grid-template-columns: 56px 1fr; }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `RailCollapseToggleProps`
|
|
69
|
+
|
|
70
|
+
<!-- prettier-ignore -->
|
|
71
|
+
| Prop | Type | Required | Description |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| `aria-label` | `string` | no | Override the auto-generated aria-label (defaults to `t('rail.expand')` / `t('rail.collapse')` depending on the current state, when omitted OR empty — an empty string is not an explicit name). |
|
|
74
|
+
| …native | | | plus native `<button>` attributes |
|
|
75
|
+
|
|
76
|
+
### `RailFooterProps`
|
|
77
|
+
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
|
+
| Prop | Type | Required | Description |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| …native | | | plus native `<div>` attributes |
|
|
82
|
+
|
|
83
|
+
### `RailGroupProps`
|
|
84
|
+
|
|
85
|
+
<!-- prettier-ignore -->
|
|
86
|
+
| Prop | Type | Required | Description |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `icon` | `ReactNode` | yes | Required leading icon — the only thing visible when the rail is collapsed. Groups always have an icon; subitems don't need one. |
|
|
89
|
+
| `label` | `string` | yes | Group label — visible next to the icon when the rail is expanded, and rendered as the flyout's header when collapsed. |
|
|
90
|
+
| `open` | `boolean` | no | Controlled inline-expand state (only meaningful when the rail itself is expanded — collapsed-mode uses hover-driven popover state instead). Provide alongside `onOpenChange` to drive open state externally. |
|
|
91
|
+
| `defaultOpen` | `boolean` | no | Initial open state for the uncontrolled case. Default `false`. |
|
|
92
|
+
| `onOpenChange` | `((open: boolean) => void)` | no | Fires whenever the inline-expand toggles. |
|
|
93
|
+
| `children` | `ReactNode` | yes | Subitems — typically a list of `<Rail.Item>` without icons. |
|
|
94
|
+
| `as` | `ElementType` | no | |
|
|
95
|
+
| …native | | | plus native attributes of the `as` element (default `<div>`) |
|
|
96
|
+
|
|
97
|
+
### `RailHeaderProps`
|
|
98
|
+
|
|
99
|
+
<!-- prettier-ignore -->
|
|
100
|
+
| Prop | Type | Required | Description |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| …native | | | plus native `<div>` attributes |
|
|
103
|
+
|
|
104
|
+
### `RailItemProps`
|
|
105
|
+
|
|
106
|
+
<!-- prettier-ignore -->
|
|
107
|
+
| Prop | Type | Required | Description |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| `icon` | `ReactNode` | no | Leading icon — required for top-level items (the only thing visible when the rail is collapsed); optional for items nested inside a `<Rail.Group>` (Jira renders subitems without icons). |
|
|
110
|
+
| `badge` | `ReactNode` | no | Optional trailing badge — typically a `<Badge>` or a small numeric pill. Right-aligned when the rail is expanded; fades to invisible when collapsed (the badge content is announced by screen readers either way since it's still in the DOM). |
|
|
111
|
+
| `children` | `ReactNode` | no | The label text — visible when expanded, used as the tooltip when collapsed. Optional at the type level only to satisfy the polymorphic `forwardRef` generic; in practice every item has a label. |
|
|
112
|
+
| `as` | `ElementType` | no | |
|
|
113
|
+
| …native | | | plus native attributes of the `as` element (default `<a>`) |
|
|
114
|
+
|
|
115
|
+
### `RailSectionProps`
|
|
116
|
+
|
|
117
|
+
<!-- prettier-ignore -->
|
|
118
|
+
| Prop | Type | Required | Description |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `title` | `string` | no | Section heading rendered in small-caps muted style above the section's items. Hidden when the rail is collapsed (a small visual gap remains between sections via the rail's `gap`). When provided, doubles as `aria-label` for the section's `role="group"` so screen readers can navigate by group. |
|
|
121
|
+
| `children` | `ReactNode` | yes | Items and/or groups belonging to this section. |
|
|
122
|
+
| …native | | | plus native `<div>` attributes |
|
|
123
|
+
|
|
124
|
+
### `RailSpacerProps`
|
|
125
|
+
|
|
126
|
+
<!-- prettier-ignore -->
|
|
127
|
+
| Prop | Type | Required | Description |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| …native | | | plus native `<div>` attributes |
|
|
130
|
+
|
|
131
|
+
<!-- props:end -->
|
|
132
|
+
|
|
133
|
+
- **Compound API** — `Rail.Header` / `Rail.Section` / `Rail.Item` / `Rail.Group` / `Rail.Spacer` / `Rail.Footer` / `Rail.CollapseToggle`.
|
|
134
|
+
- **`Rail.Header` brand area (default)** — a brand block with a header→nav divider out of the box, sized to line up with `<TopBar>`: a **leading** `<Rail.Header>` is pinned above the scrolling item list and runs the **full rail width**, at least `--rail-header-height` tall (default `var(--topbar-height)`) with its content **vertically centred**, so in an `AppLayout` its divider continues the TopBar's bottom border as one line — expanded, collapsed, with `sidebarPinned`, and wherever `--topbar-height` is retuned (the defaults resolve on the header element, so a retune on a theme wrapper or the AppLayout carries through). Tokens: `--rail-header-height` (set `auto` for a content-height header), `--rail-header-padding` (default `var(--rail-header-divider-width) calc(var(--rail-padding-x) + var(--rail-item-padding-x)) 0` — the inline inset puts the brand mark on the same x as the item icons; the divider-width top inset centres the brand in the full bar height, window edge to the bottom of the divider, so a 32px mark and its wordmark land on whole pixels and stay level with each other) and `--rail-header-divider-width` (default `var(--border-width)`, color `--rail-border-color`). No prop and no raw CSS needed. For a bare header, override the padding/divider tokens to `0` and `--rail-header-height` to `auto` (otherwise it keeps the 56px floor). **Pinning needs `<Rail.Header>` as the rail's literal first child** (directly or inside a Fragment): Rail recognises it by element type, so a header placed later, or rendered from your own wrapper component (`<MyBrand/>` that returns a `<Rail.Header>`), stays inside the scroll box — it scrolls away, the divider stops short of the rail edges, and the brand sits 8px right of the icon column. Write `<Rail.Header><MyBrand/></Rail.Header>` instead. Children render inside an inner block box, so their own flow is unchanged: inline content (`<img/> Acme`) stays on one line, block children stack. When the rail is **collapsed** the header centers its content in the 56px track — render a mark-only brand there (read the state with `useRail()`). A brand too wide for the track is clipped; the centering is `safe`, so it aligns to the start edge in that case and you lose the trailing end rather than both sides. The header's **inline padding is zeroed** while collapsed — not just the default but whatever you set `--rail-header-padding` to (there is no collapsed-only token). _Migration:_ (a) the header no longer scrolls with the item list, and a tall header (org switcher, search) now permanently takes that height from the list; (b) every rail header is now at least `--topbar-height` (56px) tall, also without a TopBar (e.g. in AppLayout's overlay drawer) — set `--rail-header-height: auto` to get the old content height; (c) the rail's top/inline padding moved from the `<nav>` onto the scroll body and footer wrapper — `--rail-padding-x/y` still apply there, but no longer surround the header; (d) a custom `--rail-header-padding` written for the old in-padding header needs `--rail-padding-x` added to its inline value, and its block padding now shifts the centred brand rather than spacing it from the divider; (e) remove any app-side shim that sized the header to the top bar; (f) a header rendered through a wrapper component must become a literal `<Rail.Header>` (see above); (g) children now sit in an inner box, so styles that treated the header element itself as their layout container (a `className` with `display: flex; gap`, or a `.myHeader > img` selector) no longer reach them — put that layout on your own brand element inside the header.
|
|
135
|
+
- **Layout-owning primitive (Hard rule 4 exception)** — like `<Modal>`, `<Drawer>`, `<Page>`, the rail owns its own width and height because that IS its job. Place the rail inside whatever container shape your page needs (sticky aside, fixed sidebar, in-flow column).
|
|
136
|
+
- **Collapse state**: controlled (`collapsed` + `onCollapsedChange`) or uncontrolled (`defaultCollapsed`). The `<Rail.CollapseToggle>` button reads context and flips the state without prop drilling.
|
|
137
|
+
- **`useRail()`** reports the EFFECTIVE `collapsed` plus `collapsedByViewport` (true while `collapseBelow` forces the collapse) for custom chrome that needs to tell the two apart.
|
|
138
|
+
- ⚠️ **Make your shell's rail track follow the override.** It shrinks the rail but is invisible to the layout around it, so a shell sizing its rail column from its OWN `collapsed` state keeps a 240px track around a 56px rail — a dead gap on every screen. Key the track off the viewport (`@media (max-width: 768px) { .shell { grid-template-columns: 56px 1fr } }`) or off the rail itself (`.shell:has(nav[data-collapsed]) { grid-template-columns: 56px 1fr }`, no breakpoint duplication). Both work only because this basis is the viewport, not a container.
|
|
139
|
+
- **`Rail.Item` is polymorphic** — `as={NavLink}` (or any router primitive) sets `aria-current="page"` on the rendered anchor; Rail's CSS applies the active accent via `[aria-current="page"]` and `:has([aria-current="page"])` selectors. No router dependency in the library.
|
|
140
|
+
- **`Rail.Group`** — renders inline-expanding subitems when the rail is expanded, and a hover-popover (`right-start` placement, 80ms open delay, 200ms close grace) when collapsed — portaled to `document.body` at `--z-popover` and auto-elevating above Modal/Drawer like other floating surfaces. Auto-opens on mount when the group's own link or any subitem is the active route (one-shot, uncontrolled-only — a manual close sticks).
|
|
141
|
+
- **`Rail.Group` can be a link AND a toggle** — pass `as` (same polymorphic contract as `Rail.Item`) and the row splits into two hit-targets: icon + label navigate and carry `aria-current="page"` (so the row highlights like any item), while a separate chevron `<button>` owns `aria-expanded` + `aria-controls` with an `Expand <label>` / `Collapse <label>` accessible name. Keyboard order is link, then chevron. **Omitting `as` is byte-for-byte today's toggle-only shape** (one `<button>` spanning the row) — the feature is purely additive. Note the spread target moves with `as`: without it the remaining props land on the wrapping `<div>` (unchanged); with it they land on the rendered link, since that is what `to` / `end` / `replace` are for — **except `className` and `ref`, which stay on the wrapper `<div>` in both shapes**. Your `onPointerEnter` / `onPointerLeave` / `onFocus` / `onBlur` / `onClick` are _composed_ with the group's own handlers (yours runs first) rather than replacing them: those five open the collapsed-mode flyout, and overriding them would leave the subitems unreachable while the rail is collapsed. Pass **`end`** if you don't want the group counted as current on child routes — `to="/deals"` without it also matches `/deals/01K7…`, so opening one record auto-opens the saved-view list. **Collapsed, a linkable group has no chevron** — the single 56px target navigates on click and opens the flyout on hover/focus, and the flyout's header is a link to the same destination so the parent page stays reachable from inside the panel.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
<Rail.Group as={NavLink} to="/deals" icon={<Handshake />} label="Deals">
|
|
145
|
+
<Rail.Item as={NavLink} to="/deals?view=open">
|
|
146
|
+
My open USD
|
|
147
|
+
</Rail.Item>
|
|
148
|
+
</Rail.Group>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- **`Rail.Spacer`** — `flex-grow: 1` filler that pushes trailing _sections_ (Settings, Help) to the bottom of the scrolling body. Not needed to pin the footer.
|
|
152
|
+
- **Scrolling** — everything before the first `Rail.Footer` renders in one scroll box; the Footer is extracted out of it and stays pinned on its own, no Spacer required. The box scrolls vertically only — the X axis is clipped, since the rail is fixed-width and labels are meant to clip as it collapses. **Collapsed, the scrollbar is hidden entirely** (`scrollbar-width: none`): a gutter is a quarter of the 56px rail's inner width and shifts every item pill off-center. Wheel/trackpad and scroll-into-view on Tab still scroll it; the bar returns when the rail expands.
|
|
153
|
+
- **i18n**: `rail.expand` / `rail.collapse` (toggle aria-label), `rail.navigation` (default `<nav>` aria-label), `rail.expandGroup` / `rail.collapseGroup` (prefix for a linkable group's chevron, interpolated with the group label).
|
|
154
|
+
|
|
155
|
+
#### When NOT to use
|
|
156
|
+
|
|
157
|
+
- ❌ Top-bar / horizontal nav — use a `<Cluster>` + `<Link>` row.
|
|
158
|
+
- ❌ A value picker (status, country) — use `<Select>`.
|
|
159
|
+
- ❌ A focus-locked dialog navigation — use `<Modal>` / `<Drawer>`.
|
|
160
|
+
|
|
161
|
+
#### Anti-patterns
|
|
162
|
+
|
|
163
|
+
- ❌ Forking width via inline style — rebind the `--rail-width-*` tokens in a parent stylesheet.
|
|
164
|
+
- ❌ Hand-rolling active-state styling. Set `aria-current="page"` on the rendered element (NavLink does this for you) and the CSS handles it.
|
|
165
|
+
- ❌ Multi-level group nesting (groups inside groups). v1 supports one level only.
|
|
166
|
+
- ❌ Adding a synthetic "All deals" first subitem so the parent list stays reachable — that's what `Rail.Group`'s `as` prop is for. Make the group itself the link.
|
|
167
|
+
- ❌ Putting an icon-less top-level item directly inside a section — when the rail collapses there's nothing visible. Items without icons belong inside a `<Rail.Group>`.
|
|
168
|
+
|
|
169
|
+
- **`Rail.Item`** defaults to `<a href>`. Expanded it shows icon + label + optional `badge` on one row; collapsed (top-level) it shows the icon only, wrapped in a `<Tooltip>` so the label stays discoverable (only when the children are a string and the item is a direct rail child — group subitems show their labels in the flyout). A `badge` fades as the rail collapses.
|
|
170
|
+
- **`Rail.Section title="…"`** renders `<div role="group">` with the title as `aria-label`; visually the title is a small-caps muted line above the items, faded out (height 0, opacity 0) when collapsed.
|
|
171
|
+
- **`Rail.CollapseToggle`** is a ghost icon-only button whose `aria-label` flips between expand / collapse; its chevron rotates 180° when collapsed so it always points outward. Drop it in `<Rail.Footer>` or anywhere inside the rail. `Rail.Footer` typically holds a user chip, theme switcher or the toggle.
|
|
172
|
+
- **`Rail.Spacer`:** when the item list overflows, the body scrolls and the spacer collapses to nothing, so items after it scroll WITH the content; only `Rail.Footer` stays outside the scroll box. Use the Footer for anything that must stay visible on a tall scroll.
|
|
173
|
+
- **Group auto-open and timing:** the active-route auto-open is one-shot; pass a controlled `open` for exact sync. Re-entering the trigger or the flyout cancels the pending close.
|
|
174
|
+
|
|
175
|
+
Controlled, persisted to localStorage (add `collapseBelow="sm"` for a forced collapse on phone-width viewports):
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
const [collapsed, setCollapsed] = useState(() => localStorage.getItem('rail') === '1');
|
|
179
|
+
<Rail
|
|
180
|
+
collapsed={collapsed}
|
|
181
|
+
onCollapsedChange={(c) => {
|
|
182
|
+
setCollapsed(c);
|
|
183
|
+
localStorage.setItem('rail', c ? '1' : '0');
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
…
|
|
187
|
+
</Rail>;
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- ❌ Wrapping subcomponents in extra `<div>`s — the `[data-collapsed]` cascade is brittle to extra wrappers and the active-state CSS (`:has([aria-current="page"])`) won't reach where you expect.
|
|
191
|
+
- ❌ Putting non-`<Rail.Item>` children inside a `<Rail.Group>` — subitems should match the item shape so the active-state cascade reaches them.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# `<RelativeDateInput>` — editor for a relative date or date-range expression
|
|
2
|
+
|
|
3
|
+
Edits one expression **string** (`today+7d`, `last:30d`, `this:quarter:to_date`) that the eocrm server resolves to real dates at use time: list filters, saved views, dashboard periods, automation conditions, custom-field defaults. It is controlled: `value` in, `onChange` out.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { Button, Cluster, Field, RelativeDateInput } from '@eocrm/design-system';
|
|
8
|
+
|
|
9
|
+
// Point: anchor + signed offset, or an exact date ("Exact date" switch).
|
|
10
|
+
const [due, setDue] = useState<string | null>('today+7d');
|
|
11
|
+
<RelativeDateInput mode="point" value={due} onChange={setDue} aria-label="Due date" />;
|
|
12
|
+
|
|
13
|
+
// Point on a datetime field: adds the Now anchor, hour/minute offsets and a minute-precision exact datetime.
|
|
14
|
+
<RelativeDateInput
|
|
15
|
+
mode="point"
|
|
16
|
+
allowNow
|
|
17
|
+
value={due}
|
|
18
|
+
onChange={setDue}
|
|
19
|
+
aria-label="Created after"
|
|
20
|
+
/>;
|
|
21
|
+
|
|
22
|
+
// Range, with presets (toggle buttons; the one equal to `value` is pressed).
|
|
23
|
+
const [period, setPeriod] = useState<string | null>('last:7d');
|
|
24
|
+
<RelativeDateInput
|
|
25
|
+
mode="range"
|
|
26
|
+
value={period}
|
|
27
|
+
onChange={setPeriod}
|
|
28
|
+
presets={[
|
|
29
|
+
{ label: 'Last 30 days', value: 'last:30d' },
|
|
30
|
+
{ label: 'Last 7 days', value: 'last:7d' },
|
|
31
|
+
{ label: 'This month', value: 'this:month' },
|
|
32
|
+
{ label: 'Quarter to date', value: 'this:quarter:to_date' },
|
|
33
|
+
]}
|
|
34
|
+
aria-label="Period"
|
|
35
|
+
/>;
|
|
36
|
+
|
|
37
|
+
// Inside a Field with an error. asGroup: Field is the group, its label names it (no
|
|
38
|
+
// htmlFor onto a div) and the error describes it; Field injects `required`, and `invalid` only when it has an `error`.
|
|
39
|
+
// Pass no aria-label here: unnamed, the control renders no group role of its own.
|
|
40
|
+
<Field asGroup required label="Due date" error="Pick a due date">
|
|
41
|
+
<RelativeDateInput mode="point" value={due} onChange={setDue} />
|
|
42
|
+
</Field>;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
<!-- props:start -->
|
|
46
|
+
|
|
47
|
+
## Props
|
|
48
|
+
|
|
49
|
+
<!-- prettier-ignore -->
|
|
50
|
+
| Prop | Type | Required | Description |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| `mode` | `'range' \| 'point'` | yes | What the expression describes. - `'point'` — one date: an anchor (today, now, start/end of a period) plus a signed offset, or an exact date. - `'range'` — a date range: last/next N units, a whole this/last/next period, or a period to date. |
|
|
53
|
+
| `allowNow` | `boolean` | no | Datetime fields: offers the Now anchor with hour/minute offsets, and a minute-granular exact date (UTC `YYYY-MM-DDTHH:mm:ssZ`). Default `false`. |
|
|
54
|
+
| `value` | `string \| null` | yes | The controlled expression (see the grammar in the docs), or `null` for none. `null` shows the defaults (`today` / `this:month`) without emitting them. A value that doesn't parse shows the defaults plus an "Unrecognised value" note. |
|
|
55
|
+
| `onChange` | `(expr: string \| null) => void` | yes | Fires with the next canonical expression after a valid edit, and only when it differs from `value`. Invalid drafts (an out-of-range amount, an exact date not yet picked) never fire it. |
|
|
56
|
+
| `presets` | `readonly RelativeDatePreset[]` | no | Quick-pick toggle buttons rendered above the controls. The one whose `value` equals the canonical form of `value` is pressed (`last:07d` presses `last:7d`). |
|
|
57
|
+
| `disabled` | `boolean` | no | Disables every control, presets included. Default `false`. |
|
|
58
|
+
| `invalid` | `boolean` | no | Error chrome on the controls (`aria-invalid`), e.g. from `<Field error>`. Default `false`. The Exact-date Switch is never marked invalid. |
|
|
59
|
+
| `required` | `boolean` | no | Consumed for `<Field asGroup required>` composition, so Field can show its required marker. Adds no ARIA: `role="group"` does not support `aria-required` (ARIA 1.2), and the controls always show a value. Default `false`. |
|
|
60
|
+
| …native | | | plus native `<div>` attributes |
|
|
61
|
+
|
|
62
|
+
<!-- props:end -->
|
|
63
|
+
|
|
64
|
+
## How it behaves
|
|
65
|
+
|
|
66
|
+
- **Point mode:** an anchor (Today, Now with `allowNow`, Start/End of this/last/next week/month/quarter/year), a sign, an amount, a unit, and an **Exact date** switch that swaps the relative controls for a `DatePicker`.
|
|
67
|
+
- **Range mode:** a kind (**Past**, **Coming**, **Calendar period**, **Period to date**) followed by an amount + unit (Past / Coming) or a period (the rest). Whole periods read as composed labels such as "This week" and "Month to date".
|
|
68
|
+
- **Only valid expressions are emitted.** An out-of-range or non-integer amount shows an inline error and does not call `onChange` until it is valid. `onChange` also does not fire when the edit would produce the current `value`.
|
|
69
|
+
- **It never emits `null`.** `null` _in_ means "no value" and shows the defaults (`today` / `this:month`) without emitting them. To let users clear a filter, give them your own chip or reset button that sets your state to `null` (see the "Clearing" pattern below).
|
|
70
|
+
- **An unrecognised incoming `value`** (one that does not parse in this `mode` / `allowNow`) shows the defaults plus an "Unrecognised value: …" note and emits nothing until the user edits.
|
|
71
|
+
- **Presets** are toggle buttons (`aria-pressed`); choosing one emits its `value`. Their `label` is yours (translate it yourself). Clicking a pressed preset keeps it selected (radio-like; it never un-presses to `null`). A preset whose `value` does not parse in this `mode` / `allowNow` renders disabled, never emits, and logs a dev-mode `console.warn`.
|
|
72
|
+
- **`invalid`** marks the selects, amount and date picker invalid. The Exact-date `Switch` is intentionally not marked: it is a mode toggle, not the erroneous value.
|
|
73
|
+
- **Group role:** with `aria-label` or `aria-labelledby` the root is a named `role="group"`; without one (e.g. inside `<Field asGroup>`, which is the group) it has no role, so there is no nameless nested group. Outside a `Field`, always pass `aria-label` / `aria-labelledby`: an unnamed RelativeDateInput has no group semantics. The "Unrecognised value" note describes every control as well as a named root. `required` (injected by Field) is consumed for Field's marker and adds no ARIA, since `role="group"` does not support `aria-required`.
|
|
74
|
+
- **Leaving Now** for another anchor resets the offset (unit to days, amount to 0) and emits the bare anchor, since `now-3h` has no day equivalent.
|
|
75
|
+
- `allowNow` gates three things together: the `now` anchor, the `h` / `min` units (only on `now`), and a minute-precision exact datetime in UTC (`YYYY-MM-DDTHH:mm:ssZ`).
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// Clearing is the consumer's job: the control never emits null.
|
|
79
|
+
<Cluster align="center">
|
|
80
|
+
<RelativeDateInput mode="range" value={period} onChange={setPeriod} aria-label="Period" />
|
|
81
|
+
{period !== null && (
|
|
82
|
+
<Button variant="ghost" onClick={() => setPeriod(null)}>
|
|
83
|
+
Clear
|
|
84
|
+
</Button>
|
|
85
|
+
)}
|
|
86
|
+
</Cluster>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Grammar
|
|
90
|
+
|
|
91
|
+
The canonical strings the component reads and writes. Anything else does not parse.
|
|
92
|
+
|
|
93
|
+
<!-- prettier-ignore -->
|
|
94
|
+
| Mode | Expression | Meaning |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| point | `today` | Today |
|
|
97
|
+
| point | `now` | Now (`allowNow` only) |
|
|
98
|
+
| point | `start:{this\|last\|next}:{week\|month\|quarter\|year}` | Start of the period; `end:…` for the end |
|
|
99
|
+
| point | anchor `{+\|-}{N}{unit}` | Offset, N 0–1000: `today+7d`, `now-30min`, `end:next:quarter+1d` |
|
|
100
|
+
| point | `YYYY-MM-DD` | Exact date |
|
|
101
|
+
| point | `YYYY-MM-DDTHH:mm:ssZ` | Exact UTC datetime (`allowNow` only) |
|
|
102
|
+
| range | `last:{N}{d\|w\|m\|y}` / `next:{N}{d\|w\|m\|y}` | Rolling past / coming, N 1–1000: `last:30d` |
|
|
103
|
+
| range | `{this\|last\|next}:{week\|month\|quarter\|year}` | Whole calendar period: `this:week` |
|
|
104
|
+
| range | `this:{week\|month\|quarter\|year}:to_date` | Period to date: `this:month:to_date` |
|
|
105
|
+
|
|
106
|
+
Units are `d` (days), `w` (weeks), `m` (months), `y` (years), `h` (hours), `min` (minutes). `h` and `min` are valid only on the `now` anchor. **These spellings must match the eocrm server**; the component and the server parse the same strings. Absolute values are not part of the range grammar.
|
|
107
|
+
|
|
108
|
+
`parseRelativeDate(expr, { mode, allowNow })` returns a structured `RelativeDateValue`, or `null` if `expr` is not valid; `formatRelativeDate(value)` returns the canonical string. They round-trip every canonical expression.
|
|
109
|
+
|
|
110
|
+
## When NOT to use
|
|
111
|
+
|
|
112
|
+
- **Absolute-only dates.** If the user is picking a fixed calendar date or range and nothing relative, use `DatePicker` / `DateRangePicker` (or the inline variants).
|
|
113
|
+
- **Displaying a resolved date.** This edits the expression; it does not show what it resolves to (that is the server's job).
|
|
114
|
+
- **Custom anchors or business days.** Not supported.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# `<RichText>` — read-only rich-text renderer
|
|
2
|
+
|
|
3
|
+
Renders a `RichDoc` (the in-house rich-text model) read-only: paragraphs, H1–H3, bullet/ordered lists, blockquotes, code blocks, and inline marks (bold/italic/underline/strike/code/link). No editor libraries. Build docs with the exported engine constructors/transforms.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
const doc = {
|
|
7
|
+
blocks: [
|
|
8
|
+
createBlock('heading', 'Notes', { level: 2 }),
|
|
9
|
+
createBlock('paragraph', 'See the docs.'),
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
<RichText value={doc} />;
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
<!-- props:start -->
|
|
16
|
+
|
|
17
|
+
## Props
|
|
18
|
+
|
|
19
|
+
<!-- prettier-ignore -->
|
|
20
|
+
| Prop | Type | Required | Description |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `value` | `RichDoc` | yes | The document to render. Build one with `emptyDoc()` / `createBlock()` or the transforms. |
|
|
23
|
+
| `renderLink` | `RenderLink` | no | Substitute how a link renders. Called per link with `{ href, text }` and the default `<a>` node; return your own node (e.g. a task/member chip) or the `defaultNode` to keep the standard anchor. Render-time only — the document model is unchanged, so serialization still emits a plain link. Prose link styling (accent colour + underline) applies to BARE anchors only: an `<a>` you return with any `className` — including a DS `<Link>` — keeps its own decoration and colour entirely. |
|
|
24
|
+
| `renderMention` | `RenderMention` | no | Substitute how an `@`-mention renders. Called per mention with `{ id, label }` and the default mention span; return your own node (e.g. an interactive member chip / popover trigger) or the `defaultNode` to keep the standard non-interactive span. Render-time only — the document model is unchanged, so serialization still emits the mention mark. Composes with `renderLink`. |
|
|
25
|
+
| `imageMaxHeight` | `number` | no | Maximum DISPLAYED height of image attachments, in px. Default: the `--size-rich-text-image-max-height` token (480px). A taller image shrinks (width follows, aspect preserved) — render-time only; stored `width`/`height` are unchanged, so existing documents need no migration. There is no "off": pass a large value to effectively remove the cap. |
|
|
26
|
+
| …native | | | plus native `<div>` attributes |
|
|
27
|
+
|
|
28
|
+
<!-- props:end -->
|
|
29
|
+
|
|
30
|
+
**`renderLink`** (optional) — substitute how a link renders. The consumer checks "is this URL in my space?" and returns its own node (e.g. a task/member chip) or the supplied `fallback` (the standard `<a>`). It's render-time only — the model and `toHtml`/`toMarkdown` still emit a plain link, so serialization is unchanged. Don't do heavy synchronous work or block on the network inside it; return a component that handles its own lookup/cache. The same resolver works in `<RichText>` (viewer) and `<RichTextEditor>` (where a substituted link becomes an atomic chip). Typing against a link's trailing edge produces UNLINKED text — the link is inherited only with the caret strictly inside it, so text typed after a chip is visible instead of disappearing into the link's text under the unchanged href.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
const renderLink: RenderLink = ({ href }, fallback) => {
|
|
34
|
+
const m = /^https?:\/\/app\.eocrm\/task\/(\d+)/i.exec(href);
|
|
35
|
+
return m ? <Badge tone="purple">#{m[1]}</Badge> : fallback;
|
|
36
|
+
};
|
|
37
|
+
<RichText value={doc} renderLink={renderLink} />;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**`renderMention`** (optional) — `(mention: { id, label }, defaultNode) => ReactNode` — same contract as `renderLink` but for `@`-mention marks (render an interactive member chip/popover trigger), or return `defaultNode` for the standard non-interactive mention span. Composes with `renderLink`. Render-time only — `toHtml`/`toMarkdown` and the model still emit the mention mark. Works in both `<RichText>` and `<RichTextEditor>` (where a substituted mention becomes an atomic chip).
|
|
41
|
+
|
|
42
|
+
When NOT to use: plain text → `Text`. For editing → `<RichTextEditor>`. The model is immutable; render the doc returned by a transform, never mutate in place.
|
|
43
|
+
|
|
44
|
+
- ❌ Hand-writing HTML to display rich content — feed a `RichDoc` to `<RichText>`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# `<RichTextEditor>` — controlled rich-text editor (contentEditable)
|
|
2
|
+
|
|
3
|
+
Controlled WYSIWYG over the in-house engine. `value: RichDoc` + `onChange: (doc) => void` (feed it back into `value`). Type to edit; ⌘/Ctrl+B/I/U and ⌘/Ctrl+⇧X toggle marks over a selection; Enter splits, Backspace/Delete merge.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
const [doc, setDoc] = useState(emptyDoc());
|
|
7
|
+
// Plain editor (keyboard shortcuts only)
|
|
8
|
+
<RichTextEditor value={doc} onChange={setDoc} placeholder="Write a note…" />;
|
|
9
|
+
|
|
10
|
+
// With built-in formatting toolbar
|
|
11
|
+
<RichTextEditor value={doc} onChange={setDoc} toolbar placeholder="Write a note…" />;
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
<!-- props:start -->
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
<!-- prettier-ignore -->
|
|
19
|
+
| Prop | Type | Required | Description |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| `value` | `RichDoc` | yes | Controlled document. Render the doc returned by `onChange` back into `value` as-is, right away — a cloned/normalized copy reads as an outside replacement and resets undo history; one stored only after a later edit (debounced, or a deferred render the next keystroke beats) is treated as rejected. Defer expensive consumers (preview, save), not `value`. |
|
|
22
|
+
| `invalid` | `boolean` | no | `aria-invalid` on the textbox. Field / SettingRow inject it. Default: `false`. |
|
|
23
|
+
| `required` | `boolean` | no | `aria-required` on the textbox. Field / SettingRow inject it. Default: `false`. |
|
|
24
|
+
| `onChange` | `(doc: RichDoc) => void` | yes | Fires with the new document after every edit. |
|
|
25
|
+
| `readOnly` | `boolean` | no | Non-editable: renders the content read-only (prefer `<RichText>` for pure display). |
|
|
26
|
+
| `placeholder` | `string` | no | Shown when the document is empty. |
|
|
27
|
+
| `autoFocus` | `boolean` | no | Focus the editor on mount. |
|
|
28
|
+
| `toolbar` | `false \| true \| 'auto'` | no | Render the built-in formatting toolbar above the editor — Undo/Redo buttons, mark toggle buttons (bold/italic/underline/strike), a block-type dropdown (paragraph/headings/quote/code), and bullet/numbered list toggles. The toolbar dispatches through the same commit path the keyboard uses and reflects the active marks + current block of the live selection. Default `false` (keyboard-only). When `readOnly`, the toolbar renders disabled. `'auto'` shows the toolbar only when the editor is **focused or non-empty**, and keeps it shown while the editor's own overlays (the link editor / mention menu) are open — so opening the link editor on a focused-but-empty composer doesn't collapse the bar. The editable is NOT remounted as the bar appears or hides (the bar toggles in a stable shell), so there's no focus/selection loss. Use it for a compact, focus-gated composer (e.g. a comment box) instead of hand-rolling the show-on-focus logic and working around overlay focus theft. |
|
|
29
|
+
| `mentions` | `MentionsConfig` | no | Enable `@`-mention autocomplete. Supply `onQuery` to resolve candidates for the text typed after the trigger (default `@`); the editor renders a floating combobox and inserts a styled mention chip carrying the chosen item's `id`. Omit to disable mentions. See {@link MentionsConfig}. |
|
|
30
|
+
| `autolink` | `boolean` | no | Turn typed and pasted URLs into links automatically. While typing, a URL is linked when you type a space after it; on paste, plain-text URLs are linked (a single bare URL pasted over a selection links the selection). Default `true`. Set `false` to disable both the type rule and paste autolinking (the ⌘/Ctrl+K link tool still works). The href is sanitized — only safe schemes (http/https/`www.`) become links. |
|
|
31
|
+
| `renderLink` | `RenderLink` | no | Substitute how a link renders. Called per link with `{ href, text }` and the default `<a>`; return your own node (e.g. a task/member chip) or the `defaultNode` to keep the standard anchor. A custom node renders as a non-editable **atomic chip** — the caret steps over it and a single Backspace/ Delete removes the whole link. |
|
|
32
|
+
| `renderMention` | `RenderMention` | no | Substitute how an `@`-mention renders. Called per mention with `{ id, label }` and the default mention span; return your own node (e.g. an interactive member chip / popover trigger) or the `defaultNode` to keep the standard non-interactive span. A custom node renders as a non-editable **atomic chip** — the caret steps over it as a single unit. Composes with `renderLink` (both usable together). |
|
|
33
|
+
| `imageMaxHeight` | `number` | no | Maximum DISPLAYED height of image attachments, in px. Default: the `--size-rich-text-image-max-height` token (480px). A taller image shrinks (width follows, aspect preserved); the resize handle and width slider stop where the image hits the cap. Render-time only — stored `width`/`height` are unchanged. Pass the same value to the read-only `<RichText>` so both agree. |
|
|
34
|
+
| `blockControls` | `boolean` | no | Show Notion-style per-block controls: a left gutter that appears on the hovered/focused block with an insert button (`+`, adds an empty paragraph below) and a drag handle (`⠿`) that opens a block menu (Turn into ▸, Duplicate, Move up/down, Delete). Reordering is subtree-aware for nested lists. Keyboard: Shift+F10 / the ContextMenu key opens the focused block's menu; ⌘/Ctrl+⇧↑ / ⌘/Ctrl+⇧↓ move the block; ⌘/Ctrl+D duplicates. Default `false`. Ignored when `readOnly`. Independent of `toolbar`. |
|
|
35
|
+
| `upload` | `UploadConfig` | no | Enable file upload: a toolbar button (when `toolbar`) and clipboard-file paste. `onUpload(file)` resolves with where the file landed; images render inline as a preview, other files as a download chip. Reject `onUpload` to show a retry/remove error; reject with `new UploadError(message)` to also show that (user-facing, localized) reason on the block — any other rejection shows only "Upload failed". `onUploadingChange` fires while any upload is in flight — wire it to your submit button's disabled state. Validation (size/type) belongs in `onUpload` (`accept` is only a native-picker hint and is bypassed by paste). Omit to disable. Ignored when `readOnly`. When `blockControls` is also on, a ready image attachment can be configured (alt text, alignment, replace, open/download) via the block menu's "Configure" item. The width slider appears only for an image that renders as a preview (a safe, fetchable src — an embed); an uploaded image whose object-URL src renders as a download chip isn't resizable here. Alignment + width round-trip through HTML but not Markdown. |
|
|
36
|
+
| …native | | | plus native `<div>` attributes |
|
|
37
|
+
|
|
38
|
+
<!-- props:end -->
|
|
39
|
+
|
|
40
|
+
**`toolbar` prop** (`boolean | 'auto'`, default `false`): renders a formatting bar above the editor surface. `toolbar="auto"` shows the bar only when the editor is **focused or non-empty**, and keeps it shown while the editor's own overlays (link editor / mention menu) are open — so opening the link editor on a focused-but-empty composer doesn't collapse the bar. The editable is not remounted as the bar appears/hides (no focus/selection loss). Use `'auto'` for a compact, focus-gated composer (e.g. a comment box) instead of hand-rolling show-on-focus + overlay-focus handling. The toolbar contains:
|
|
41
|
+
|
|
42
|
+
- **Mark buttons** — Bold, Italic, Underline, Strikethrough. Reflect `aria-pressed` based on the current selection (or pending marks at a collapsed caret). Clicking with a selection toggles the mark over it; clicking at a collapsed caret sets a "pending" mark applied to the next typed characters (then clears).
|
|
43
|
+
- **Block-type dropdown** — Paragraph, Heading 1–3, Quote, Code block. Shows the current block type; mixed multi-block selections show "Mixed".
|
|
44
|
+
- **List toggles** — Bullet list, Numbered list. Toggle between the list type and paragraph.
|
|
45
|
+
- **Link button** — add/edit a link on the selection. Reflects `aria-pressed` when the caret is inside a link.
|
|
46
|
+
- **Emoji insert** — opens a searchable `EmojiPickerPopover`; selecting an emoji inserts it at the caret.
|
|
47
|
+
- **Color** — two separate pickers, **Text color** and **Highlight**, each its own toolbar button (and its own ⠿-menu submenu when `blockControls` is enabled). Each picker is a grid of small **named badges** (the Palette-demo chip look — subtle bg fill + strong fg text): the default brand colors first (gray + semantic red/green/amber/blue, backed by the `--color-fg-muted`/`--color-danger`/`--color-success`/`--color-warning`/`--color-accent` tokens), then the rest of the categorical palette (each palette extra uses its `-fg`/`-bg` token), led by a Default/clear badge. Applied to the selection from the toolbar, or to a whole block from the ⠿ menu. Colors round-trip through HTML and are dropped in Markdown.
|
|
48
|
+
- **Undo / Redo buttons** — undo/redo the last change; disabled at the ends of the history.
|
|
49
|
+
|
|
50
|
+
**List keys:**
|
|
51
|
+
|
|
52
|
+
- `Enter` on an empty list item exits the list (converts to paragraph).
|
|
53
|
+
- `Tab` / `Shift+Tab` indent and outdent list items (clamped at depth 0).
|
|
54
|
+
|
|
55
|
+
**Pending marks:** toggling a mark at a collapsed caret (via toolbar or ⌘B/I/U/⌘⇧X shortcut) queues it; the next inserted text gets that mark applied, then the queue clears. Moving the caret discards pending marks (with or without a toolbar).
|
|
56
|
+
|
|
57
|
+
**`blockControls` prop** (opt-in, default off): Notion-style per-block gutter — `+` insert below, drag the gutter (the whole strip) to reorder with an in-place reflow (subtree-aware for nested lists) + a block menu (turn into / duplicate / move up·down / delete). Reorder/duplicate/delete are subtree-aware for nested lists (a list item carries its nested children); **turn-into acts on the single anchor block only** (idempotent — choosing the current type is a no-op). Keyboard: Shift+F10 opens the menu, ⌘/Ctrl+⇧↑·↓ move, ⌘/Ctrl+D duplicate. Independent of `toolbar`; ignored when `readOnly`. All ops route through `value`/`onChange` and are undoable.
|
|
58
|
+
|
|
59
|
+
**`upload` prop** (opt-in): file upload via a toolbar button (when `toolbar`) + clipboard-file paste. `upload={{ onUpload, accept?, onUploadingChange? }}` — `onUpload(file)` resolves `{ url, name?, mime?, width?, height?, alt? }`; images render inline as a preview, other files as a download chip. Reject `onUpload` to show a retry/remove error; reject with `new UploadError(message)` (exported) to show that reason on the block — write it for end users, localized. Any other rejection shows only "Upload failed", so raw `Failed to fetch` / HTTP-client messages never leak. A failure is also announced to screen readers through a polite live region ("Upload failed: {file}. {reason}"), since it usually lands while the user is typing elsewhere; the read-only `<RichText>` shows a failed block's text without Retry/Remove. Wire `onUploadingChange(uploading)` to your submit button's disabled state. Validate size/type inside `onUpload` (the picker `accept` is a hint only; paste bypasses it). Uploaded files are void **attachment blocks** — `blockControls` can reorder/duplicate/delete them. With the caret on an attachment (e.g. after an image that ends the doc), typing or Enter continues in a new paragraph after it — even from a caret before the image, since the model has one position per attachment; Backspace from after it (or Delete from before it) removes it; Backspace from before it (or Delete from after it) only moves the caret into the neighboring block — unless that block is empty or another attachment, which is removed instead (the caret stays put). Stored doc JSON round-trips losslessly; HTML import maps `<img>` → an image attachment (Markdown image import is not supported). Ignored when `readOnly`. Ready image attachments are configurable via the block-menu's "Configure" item (requires `blockControls`): alt text, alignment, replace-in-place, open/download — plus a width slider **only** when the image renders as a preview (a safe, fetchable src — an embedded/imported image); an uploaded image whose object-URL src renders as a download chip isn't resizable here. A previewable image also gets a drag handle on its bottom-right corner (pointer only — keyboard/AT users use the Width slider); both the slider and the handle resize the image **live** and a whole drag is one undo step. Alignment + width persist in the doc and serialize to HTML; Markdown drops them. A newly uploaded/pasted image is laid out at its **perceived** size — natural pixels ÷ device pixel ratio, capped to the editor width — so a retina screenshot isn't inserted at 2× the size you saw; image **display height** is capped (`imageMaxHeight`, default 480px via `--size-rich-text-image-max-height`) — a tall photo shrinks with its aspect kept, the resize handle / width slider stop at the cap, and stored `width`/`height` are untouched (pass the same `imageMaxHeight` to the read-only `<RichText>`); return natural `width`/`height` from `onUpload` (or omit them and the editor measures the file). Persisted/imported image blocks (whose transient upload `status` is dropped on save) stay fully editable — the resize handle and the "Configure" popover treat "ready **or** status-absent" as settled, so editing an existing image comment works exactly like the brand-new composer.
|
|
60
|
+
|
|
61
|
+
**Links:** select text and press ⌘/Ctrl+K (or the toolbar link button) to add or edit a link; with the caret inside a link the URL is pre-filled and a Remove button appears; with no selection the URL is inserted as linked text. Esc / click-outside cancels. Stored hrefs are sanitized at render time (`safeHref` blocks `javascript:`/`data:`/protocol-relative).
|
|
62
|
+
|
|
63
|
+
**Autolink** (`autolink` prop, default `true`): typing a URL followed by a space, or pasting text containing a URL, turns it into a `link` mark automatically (`http(s)://…` or a bare `www.…` host; unsafe schemes are left as plain text). Set `autolink={false}` to disable both the type rule and paste autolinking.
|
|
64
|
+
|
|
65
|
+
**`renderLink`** (optional, same `RenderLink` as `<RichText>`): preview links inline — the consumer checks "is this URL in my space?" and returns a chip or the `fallback` `<a>`. In the editor a substituted link becomes an **atomic chip**: the caret sits before/after it (arrow keys step over it, never inside) and Backspace deletes the whole chip in one step. It's render-time only — `toHtml`/`toMarkdown` and the model still emit a plain link, so serialization is unchanged. Don't do heavy synchronous work inside it.
|
|
66
|
+
|
|
67
|
+
**`renderMention`** (optional, same `RenderMention` as `<RichText>`): `(mention: { id, label }, defaultNode) => ReactNode` — same contract as `renderLink` but for `@`-mention marks (render an interactive member chip/popover trigger); composes with `renderLink`. A substituted mention becomes an **atomic chip** the caret steps over as one unit. Render-time only — `toHtml`/`toMarkdown` and the model still emit the mention mark.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const renderLink: RenderLink = ({ href }, fallback) => {
|
|
71
|
+
const m = /^https?:\/\/app\.eocrm\/task\/(\d+)/i.exec(href);
|
|
72
|
+
return m ? <Badge tone="purple">#{m[1]}</Badge> : fallback;
|
|
73
|
+
};
|
|
74
|
+
// autolink is on by default; type/paste a URL to link it
|
|
75
|
+
<RichTextEditor value={doc} onChange={setDoc} toolbar renderLink={renderLink} />;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Import:** `fromHtml(html)` and `fromMarkdown(md)` parse a string into a `RichDoc` (e.g. to seed `value` from stored/legacy content). Pasting rich HTML into the editor imports it as formatted content (parsed + sanitized). Markdown import is via `fromMarkdown` only — pasted plain text (incl. Markdown source) inserts literally, except that bare URLs in pasted plain text autolink (see Autolink above). Both `from*` functions require a DOM environment (`DOMParser`); Markdown has no underline syntax and images/tables aren't modeled.
|
|
79
|
+
|
|
80
|
+
**Export:** `toHtml(doc)` and `toMarkdown(doc)` serialize a `RichDoc` back to a string (the inverse of `fromHtml`/`fromMarkdown`) — e.g. for storage, email bodies, or display outside the editor. `toHtml` is lossless (`fromHtml(toHtml(doc))` round-trips); `toMarkdown` drops underline (no Markdown syntax — use `toHtml` for full fidelity). Both escape output and run hrefs through `safeHref`.
|
|
81
|
+
|
|
82
|
+
**Mentions:** pass `mentions={{ onQuery }}` (optional `trigger`, default `@`) to
|
|
83
|
+
enable `@`-autocomplete. `onQuery(query)` returns `MentionItem[]` (`{ id, label,
|
|
84
|
+
description?, avatarUrl? }`), sync or async. Picking a candidate inserts a chip
|
|
85
|
+
carrying the `id`; chips survive `toHtml`/`fromHtml` round-trips but degrade to
|
|
86
|
+
plain `@label` text in `toMarkdown`. Chips are inert references, not links.
|
|
87
|
+
|
|
88
|
+
**Undo/redo:** built in — ⌘/Ctrl+Z undo, ⌘/Ctrl+Shift+Z (or ⌘/Ctrl+Y) redo, plus the toolbar Undo/Redo buttons. Typing coalesces into one step (short bursts), and replacing `value` from outside the editor clears the history.
|
|
89
|
+
|
|
90
|
+
**Input rules:** typing a Markdown marker + space at the start of a paragraph auto-converts the block — `# `/`## `/`### ` → headings, `- `/`* `/`+ ` → bullet list, `1. ` → ordered list, `> ` → blockquote, a triple-backtick fence → code block. One Undo reverts the conversion.
|
|
91
|
+
|
|
92
|
+
Read-only display of an existing document: `<RichTextEditor value={doc} onChange={() => {}} readOnly />` (or use `<RichText>`). `@`-mentions: `<RichTextEditor value={doc} onChange={setDoc} mentions={{ onQuery: (q) => searchUsers(q) }} />`.
|
|
93
|
+
|
|
94
|
+
**Anti-patterns**
|
|
95
|
+
|
|
96
|
+
- ❌ Treating it as uncontrolled — you MUST feed `onChange`'s doc back into `value`, or edits won't stick. Never mutate `value` in place.
|
|
97
|
+
- ❌ Storing `onChange`'s doc later (debounced / `setTimeout` / after an `await`), or deferring `value` itself (`startTransition` / `useDeferredValue` on the editor's doc) — every edit builds on the current `value`, so the next keystroke can beat the late update and drop text. Store it right away; defer the expensive consumers (preview, autosave) instead. An edit whose doc hasn't arrived as `value` by the next edit, undo or upload settle is treated as rejected: its undo step is dropped and its caret isn't restored.
|
|
98
|
+
- ❌ Cloning or normalizing the doc in `onChange` before storing it (`setDoc(structuredClone(d))`, a re-parse, an immutable-library copy) — the editor detects an outside `value` replacement by object identity, so a copy wipes undo history on every keystroke. Store the exact object `onChange` gave you; transform only on save.
|
|
99
|
+
- ❌ Implementing your own undo/redo stack — history is tracked internally (⌘/Ctrl+Z, ⌘/Ctrl+⇧Z or ⌘/Ctrl+Y, toolbar buttons).
|
|
100
|
+
- ❌ Hand-rolling a link UI or a toolbar by reaching into the DOM — press ⌘/Ctrl+K or pass `toolbar`; both route through the controlled `value`/`onChange` round-trip.
|
|
101
|
+
- ❌ Pre-stripping pasted HTML to plain text — paste rich HTML directly; the editor parses it (sanitized) into the model. Seed stored content with `fromHtml` / `fromMarkdown`.
|
|
102
|
+
- ❌ Hand-rolling HTML/Markdown from the model — use `toHtml` / `toMarkdown`.
|
|
103
|
+
- ❌ Using mentions or `renderMention` for plain links — that is the link tool (⌘K) / `renderLink`. A mention chip is an inert reference carrying an `id`, not a navigable anchor; the two compose.
|
|
104
|
+
- ❌ Returning thousands of unfiltered items from `onQuery` — filter server-side (or by `query`); the menu renders what you return.
|
|
105
|
+
- ❌ Relying on Markdown to preserve mentions — `toMarkdown` is lossy (plain `@label`); use `toHtml`/`fromHtml` to round-trip the id.
|
|
106
|
+
- ❌ Building a custom block drag/menu by reaching into the DOM — pass `blockControls`; insert/move/duplicate/delete route through `value`/`onChange` and are undoable.
|
|
107
|
+
- ❌ Expecting Backspace to delete a whole block — it edits text; use the block menu's Delete (or select the block's text and delete).
|
|
108
|
+
- ❌ Persisting or submitting the doc while an upload is in flight — gate your submit on `upload.onUploadingChange` (transient attachment blocks are skipped by `toHtml`/`toMarkdown`, but the model still carries them until they settle).
|
|
109
|
+
- ❌ Relying on the picker `accept` for validation — it is only a hint and paste bypasses it; enforce size/type inside `onUpload` and reject to show an error.
|
|
110
|
+
- ❌ Expecting image alignment/width to survive a Markdown round-trip — they serialize to HTML only; the stored RichDoc JSON is lossless.
|
|
111
|
+
|
|
112
|
+
When NOT to use: read-only display → `<RichText>`. It's controlled — render `onChange`'s doc back into `value`, never mutate in place.
|