Skip to content

Update Registry#4866

Open
zbeyens wants to merge 13 commits intomainfrom
registry
Open

Update Registry#4866
zbeyens wants to merge 13 commits intomainfrom
registry

Conversation

@zbeyens
Copy link
Member

@zbeyens zbeyens commented Mar 13, 2026

Update Registry.

@zbeyens zbeyens requested a review from a team March 13, 2026 08:51
@codesandbox
Copy link

codesandbox bot commented Mar 13, 2026

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
plate Ready Ready Preview, Comment Mar 13, 2026 9:00am

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Mar 13, 2026

⚠️ No Changeset found

Latest commit: 021c1c5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dosubot dosubot bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label Mar 13, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 021c1c5660

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{
"path": "src/registry/ui/table-node.tsx",
"content": "'use client';\n\nimport * as React from 'react';\n\nimport type * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\n\nimport { useDraggable, useDropLine } from '@platejs/dnd';\nimport {\n BlockSelectionPlugin,\n useBlockSelected,\n} from '@platejs/selection/react';\nimport { setCellBackground } from '@platejs/table';\nimport {\n TablePlugin,\n TableProvider,\n useTableBordersDropdownMenuContentState,\n useTableCellElement,\n useTableCellElementResizable,\n useTableElement,\n useTableMergeState,\n} from '@platejs/table/react';\nimport { PopoverAnchor } from '@radix-ui/react-popover';\nimport { cva } from 'class-variance-authority';\nimport {\n ArrowDown,\n ArrowLeft,\n ArrowRight,\n ArrowUp,\n CombineIcon,\n EraserIcon,\n Grid2X2Icon,\n GripVertical,\n PaintBucketIcon,\n SquareSplitHorizontalIcon,\n Trash2Icon,\n XIcon,\n} from 'lucide-react';\nimport {\n type TElement,\n type TTableCellElement,\n type TTableElement,\n type TTableRowElement,\n KEYS,\n PathApi,\n} from 'platejs';\nimport {\n type PlateElementProps,\n PlateElement,\n useComposedRef,\n useEditorPlugin,\n useEditorRef,\n useEditorSelector,\n useElement,\n useFocusedLast,\n usePluginOption,\n useReadOnly,\n useRemoveNodeButton,\n useSelected,\n withHOC,\n} from 'platejs/react';\nimport { useElementSelector } from 'platejs/react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuPortal,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { Popover, PopoverContent } from '@/components/ui/popover';\nimport { cn } from '@/lib/utils';\n\nimport { blockSelectionVariants } from './block-selection';\nimport {\n ColorDropdownMenuItems,\n DEFAULT_COLORS,\n} from './font-color-toolbar-button';\nimport { ResizeHandle } from './resize-handle';\nimport {\n BorderAllIcon,\n BorderBottomIcon,\n BorderLeftIcon,\n BorderNoneIcon,\n BorderRightIcon,\n BorderTopIcon,\n} from './table-icons';\nimport {\n Toolbar,\n ToolbarButton,\n ToolbarGroup,\n ToolbarMenuGroup,\n} from './toolbar';\nexport const TableElement = withHOC(\n TableProvider,\n function TableElement({\n children,\n ...props\n }: PlateElementProps<TTableElement>) {\n const readOnly = useReadOnly();\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n const hasControls = !readOnly && !isSelectionAreaVisible;\n const {\n isSelectingCell,\n marginLeft,\n props: tableProps,\n } = useTableElement();\n\n const isSelectingTable = useBlockSelected(props.element.id as string);\n\n const content = (\n <PlateElement\n {...props}\n className={cn(\n 'overflow-x-auto py-5',\n hasControls && '-ml-2 *:data-[slot=block-selection]:left-2'\n )}\n style={{ paddingLeft: marginLeft }}\n >\n <div className=\"group/table relative w-fit\">\n <table\n className={cn(\n 'mr-0 ml-px table h-px table-fixed border-collapse',\n isSelectingCell && 'selection:bg-transparent'\n )}\n {...tableProps}\n >\n <tbody className=\"min-w-full\">{children}</tbody>\n </table>\n\n {isSelectingTable && (\n <div className={blockSelectionVariants()} contentEditable={false} />\n )}\n </div>\n </PlateElement>\n );\n\n if (readOnly) {\n return content;\n }\n\n return <TableFloatingToolbar>{content}</TableFloatingToolbar>;\n }\n);\n\nfunction TableFloatingToolbar({\n children,\n ...props\n}: React.ComponentProps<typeof PopoverContent>) {\n const { tf } = useEditorPlugin(TablePlugin);\n const selected = useSelected();\n const element = useElement<TTableElement>();\n const { props: buttonProps } = useRemoveNodeButton({ element });\n const collapsedInside = useEditorSelector(\n (editor) => selected && editor.api.isCollapsed(),\n [selected]\n );\n const isFocusedLast = useFocusedLast();\n\n const { canMerge, canSplit } = useTableMergeState();\n\n return (\n <Popover\n open={isFocusedLast && (canMerge || canSplit || collapsedInside)}\n modal={false}\n >\n <PopoverAnchor asChild>{children}</PopoverAnchor>\n <PopoverContent\n asChild\n onOpenAutoFocus={(e) => e.preventDefault()}\n contentEditable={false}\n {...props}\n >\n <Toolbar\n className=\"scrollbar-hide flex w-auto max-w-[80vw] flex-row overflow-x-auto rounded-md border bg-popover p-1 shadow-md print:hidden\"\n contentEditable={false}\n >\n <ToolbarGroup>\n <ColorDropdownMenu tooltip=\"Background color\">\n <PaintBucketIcon />\n </ColorDropdownMenu>\n {canMerge && (\n <ToolbarButton\n onClick={() => tf.table.merge()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Merge cells\"\n >\n <CombineIcon />\n </ToolbarButton>\n )}\n {canSplit && (\n <ToolbarButton\n onClick={() => tf.table.split()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Split cell\"\n >\n <SquareSplitHorizontalIcon />\n </ToolbarButton>\n )}\n\n <DropdownMenu modal={false}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton tooltip=\"Cell borders\">\n <Grid2X2Icon />\n </ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuPortal>\n <TableBordersDropdownMenuContent />\n </DropdownMenuPortal>\n </DropdownMenu>\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton tooltip=\"Delete table\" {...buttonProps}>\n <Trash2Icon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n </ToolbarGroup>\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableRow({ before: true });\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert row before\"\n >\n <ArrowUp />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableRow();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert row after\"\n >\n <ArrowDown />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.remove.tableRow();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Delete row\"\n >\n <XIcon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableColumn({ before: true });\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert column before\"\n >\n <ArrowLeft />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableColumn();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert column after\"\n >\n <ArrowRight />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.remove.tableColumn();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Delete column\"\n >\n <XIcon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n </Toolbar>\n </PopoverContent>\n </Popover>\n );\n}\n\nfunction TableBordersDropdownMenuContent(\n props: React.ComponentProps<typeof DropdownMenuPrimitive.Content>\n) {\n const editor = useEditorRef();\n const {\n getOnSelectTableBorder,\n hasBottomBorder,\n hasLeftBorder,\n hasNoBorders,\n hasOuterBorders,\n hasRightBorder,\n hasTopBorder,\n } = useTableBordersDropdownMenuContentState();\n\n return (\n <DropdownMenuContent\n className=\"min-w-[220px]\"\n onCloseAutoFocus={(e) => {\n e.preventDefault();\n editor.tf.focus();\n }}\n align=\"start\"\n side=\"right\"\n sideOffset={0}\n {...props}\n >\n <DropdownMenuGroup>\n <DropdownMenuCheckboxItem\n checked={hasTopBorder}\n onCheckedChange={getOnSelectTableBorder('top')}\n >\n <BorderTopIcon />\n <div>Top Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasRightBorder}\n onCheckedChange={getOnSelectTableBorder('right')}\n >\n <BorderRightIcon />\n <div>Right Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasBottomBorder}\n onCheckedChange={getOnSelectTableBorder('bottom')}\n >\n <BorderBottomIcon />\n <div>Bottom Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasLeftBorder}\n onCheckedChange={getOnSelectTableBorder('left')}\n >\n <BorderLeftIcon />\n <div>Left Border</div>\n </DropdownMenuCheckboxItem>\n </DropdownMenuGroup>\n\n <DropdownMenuGroup>\n <DropdownMenuCheckboxItem\n checked={hasNoBorders}\n onCheckedChange={getOnSelectTableBorder('none')}\n >\n <BorderNoneIcon />\n <div>No Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasOuterBorders}\n onCheckedChange={getOnSelectTableBorder('outer')}\n >\n <BorderAllIcon />\n <div>Outside Borders</div>\n </DropdownMenuCheckboxItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n );\n}\n\nfunction ColorDropdownMenu({\n children,\n tooltip,\n}: {\n children: React.ReactNode;\n tooltip: string;\n}) {\n const [open, setOpen] = React.useState(false);\n\n const editor = useEditorRef();\n const selectedCells = usePluginOption(TablePlugin, 'selectedCells');\n\n const onUpdateColor = React.useCallback(\n (color: string) => {\n setOpen(false);\n setCellBackground(editor, { color, selectedCells: selectedCells ?? [] });\n },\n [selectedCells, editor]\n );\n\n const onClearColor = React.useCallback(() => {\n setOpen(false);\n setCellBackground(editor, {\n color: null,\n selectedCells: selectedCells ?? [],\n });\n }, [selectedCells, editor]);\n\n return (\n <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton tooltip={tooltip}>{children}</ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align=\"start\">\n <ToolbarMenuGroup label=\"Colors\">\n <ColorDropdownMenuItems\n className=\"px-2\"\n colors={DEFAULT_COLORS}\n updateColor={onUpdateColor}\n />\n </ToolbarMenuGroup>\n <DropdownMenuGroup>\n <DropdownMenuItem className=\"p-2\" onClick={onClearColor}>\n <EraserIcon />\n <span>Clear</span>\n </DropdownMenuItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\nexport function TableRowElement({\n children,\n ...props\n}: PlateElementProps<TTableRowElement>) {\n const { element } = props;\n const readOnly = useReadOnly();\n const selected = useSelected();\n const editor = useEditorRef();\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n const hasControls = !readOnly && !isSelectionAreaVisible;\n\n const { isDragging, nodeRef, previewRef, handleRef } = useDraggable({\n element,\n type: element.type,\n canDropNode: ({ dragEntry, dropEntry }) =>\n PathApi.equals(\n PathApi.parent(dragEntry[1]),\n PathApi.parent(dropEntry[1])\n ),\n onDropHandler: (_, { dragItem }) => {\n const dragElement = (dragItem as { element: TElement }).element;\n\n if (dragElement) {\n editor.tf.select(dragElement);\n }\n },\n });\n\n return (\n <PlateElement\n {...props}\n ref={useComposedRef(props.ref, previewRef, nodeRef)}\n as=\"tr\"\n className={cn('group/row', isDragging && 'opacity-50')}\n attributes={{\n ...props.attributes,\n 'data-selected': selected ? 'true' : undefined,\n }}\n >\n {hasControls && (\n <td className=\"w-2 select-none\" contentEditable={false}>\n <RowDragHandle dragRef={handleRef} />\n <RowDropLine />\n </td>\n )}\n\n {children}\n </PlateElement>\n );\n}\n\nfunction RowDragHandle({ dragRef }: { dragRef: React.Ref<any> }) {\n const editor = useEditorRef();\n const element = useElement();\n\n return (\n <Button\n ref={dragRef}\n variant=\"outline\"\n className={cn(\n '-translate-y-1/2 absolute top-1/2 left-0 z-51 h-6 w-4 p-0 focus-visible:ring-0 focus-visible:ring-offset-0',\n 'cursor-grab active:cursor-grabbing',\n 'opacity-0 transition-opacity duration-100 group-hover/row:opacity-100 group-has-data-[resizing=\"true\"]/row:opacity-0'\n )}\n onClick={() => {\n editor.tf.select(element);\n }}\n >\n <GripVertical className=\"text-muted-foreground\" />\n </Button>\n );\n}\n\nfunction RowDropLine() {\n const { dropLine } = useDropLine();\n\n if (!dropLine) return null;\n\n return (\n <div\n className={cn(\n 'absolute inset-x-0 left-2 z-50 h-0.5 bg-brand/50',\n dropLine === 'top' ? '-top-px' : '-bottom-px'\n )}\n />\n );\n}\n\nexport function TableCellElement({\n isHeader,\n ...props\n}: PlateElementProps<TTableCellElement> & {\n isHeader?: boolean;\n}) {\n const { api } = useEditorPlugin(TablePlugin);\n const readOnly = useReadOnly();\n const element = props.element;\n\n const tableId = useElementSelector(([node]) => node.id as string, [], {\n key: KEYS.table,\n });\n const rowId = useElementSelector(([node]) => node.id as string, [], {\n key: KEYS.tr,\n });\n const isSelectingTable = useBlockSelected(tableId);\n const isSelectingRow = useBlockSelected(rowId) || isSelectingTable;\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n\n const { borders, colIndex, colSpan, minHeight, rowIndex, selected, width } =\n useTableCellElement();\n\n const { bottomProps, hiddenLeft, leftProps, rightProps } =\n useTableCellElementResizable({\n colIndex,\n colSpan,\n rowIndex,\n });\n\n return (\n <PlateElement\n {...props}\n as={isHeader ? 'th' : 'td'}\n className={cn(\n 'h-full overflow-visible border-none bg-background p-0',\n element.background ? 'bg-(--cellBackground)' : 'bg-background',\n isHeader && 'text-left *:m-0',\n 'before:size-full',\n selected && 'before:z-10 before:bg-brand/5',\n \"before:absolute before:box-border before:select-none before:content-['']\",\n borders.bottom?.size && 'before:border-b before:border-b-border',\n borders.right?.size && 'before:border-r before:border-r-border',\n borders.left?.size && 'before:border-l before:border-l-border',\n borders.top?.size && 'before:border-t before:border-t-border'\n )}\n style={\n {\n '--cellBackground': element.background,\n maxWidth: width || 240,\n minWidth: width || 120,\n } as React.CSSProperties\n }\n attributes={{\n ...props.attributes,\n colSpan: api.table.getColSpan(element),\n rowSpan: api.table.getRowSpan(element),\n }}\n >\n <div\n className=\"relative z-20 box-border h-full px-3 py-2\"\n style={{ minHeight }}\n >\n {props.children}\n </div>\n\n {!isSelectionAreaVisible && (\n <div\n className=\"group absolute top-0 size-full select-none\"\n contentEditable={false}\n suppressContentEditableWarning={true}\n >\n {!readOnly && (\n <>\n <ResizeHandle\n {...rightProps}\n className=\"-top-2 -right-1 h-[calc(100%_+_8px)] w-2\"\n data-col={colIndex}\n />\n <ResizeHandle {...bottomProps} className=\"-bottom-1 h-2\" />\n {!hiddenLeft && (\n <ResizeHandle\n {...leftProps}\n className=\"-left-1 top-0 w-2\"\n data-resizer-left={colIndex === 0 ? 'true' : undefined}\n />\n )}\n\n <div\n className={cn(\n 'absolute top-0 z-30 hidden h-full w-1 bg-ring',\n 'right-[-1.5px]',\n columnResizeVariants({ colIndex: colIndex as any })\n )}\n />\n {colIndex === 0 && (\n <div\n className={cn(\n 'absolute top-0 z-30 h-full w-1 bg-ring',\n 'left-[-1.5px]',\n 'fade-in hidden animate-in group-has-[[data-resizer-left]:hover]/table:block group-has-[[data-resizer-left][data-resizing=\"true\"]]/table:block'\n )}\n />\n )}\n </>\n )}\n </div>\n )}\n\n {isSelectingRow && (\n <div className={blockSelectionVariants()} contentEditable={false} />\n )}\n </PlateElement>\n );\n}\n\nexport function TableCellHeaderElement(\n props: React.ComponentProps<typeof TableCellElement>\n) {\n return <TableCellElement {...props} isHeader />;\n}\n\nconst columnResizeVariants = cva('fade-in hidden animate-in', {\n variants: {\n colIndex: {\n 0: 'group-has-[[data-col=\"0\"]:hover]/table:block group-has-[[data-col=\"0\"][data-resizing=\"true\"]]/table:block',\n 1: 'group-has-[[data-col=\"1\"]:hover]/table:block group-has-[[data-col=\"1\"][data-resizing=\"true\"]]/table:block',\n 2: 'group-has-[[data-col=\"2\"]:hover]/table:block group-has-[[data-col=\"2\"][data-resizing=\"true\"]]/table:block',\n 3: 'group-has-[[data-col=\"3\"]:hover]/table:block group-has-[[data-col=\"3\"][data-resizing=\"true\"]]/table:block',\n 4: 'group-has-[[data-col=\"4\"]:hover]/table:block group-has-[[data-col=\"4\"][data-resizing=\"true\"]]/table:block',\n 5: 'group-has-[[data-col=\"5\"]:hover]/table:block group-has-[[data-col=\"5\"][data-resizing=\"true\"]]/table:block',\n 6: 'group-has-[[data-col=\"6\"]:hover]/table:block group-has-[[data-col=\"6\"][data-resizing=\"true\"]]/table:block',\n 7: 'group-has-[[data-col=\"7\"]:hover]/table:block group-has-[[data-col=\"7\"][data-resizing=\"true\"]]/table:block',\n 8: 'group-has-[[data-col=\"8\"]:hover]/table:block group-has-[[data-col=\"8\"][data-resizing=\"true\"]]/table:block',\n 9: 'group-has-[[data-col=\"9\"]:hover]/table:block group-has-[[data-col=\"9\"][data-resizing=\"true\"]]/table:block',\n 10: 'group-has-[[data-col=\"10\"]:hover]/table:block group-has-[[data-col=\"10\"][data-resizing=\"true\"]]/table:block',\n },\n },\n});\n",
"content": "'use client';\n\nimport * as React from 'react';\n\nimport { useDraggable, useDropLine } from '@platejs/dnd';\nimport {\n BlockSelectionPlugin,\n useBlockSelected,\n} from '@platejs/selection/react';\nimport { resizeLengthClampStatic } from '@platejs/resizable';\nimport {\n setCellBackground,\n setTableColSize,\n setTableMarginLeft,\n setTableRowSize,\n} from '@platejs/table';\nimport {\n TablePlugin,\n TableProvider,\n roundCellSizeToStep,\n useCellIndices,\n useIsCellSelected,\n useOverrideColSize,\n useOverrideMarginLeft,\n useOverrideRowSize,\n useTableCellBorders,\n useTableBordersDropdownMenuContentState,\n useTableColSizes,\n useTableElement,\n useTableMergeState,\n useTableValue,\n} from '@platejs/table/react';\nimport {\n ArrowDown,\n ArrowLeft,\n ArrowRight,\n ArrowUp,\n CombineIcon,\n EraserIcon,\n Grid2X2Icon,\n GripVertical,\n PaintBucketIcon,\n SquareSplitHorizontalIcon,\n Trash2Icon,\n XIcon,\n} from 'lucide-react';\nimport {\n type TElement,\n type TTableCellElement,\n type TTableElement,\n type TTableRowElement,\n KEYS,\n PathApi,\n} from 'platejs';\nimport {\n type PlateElementProps,\n PlateElement,\n useComposedRef,\n useEditorPlugin,\n useEditorRef,\n useEditorSelector,\n useElement,\n useFocusedLast,\n usePluginOption,\n useReadOnly,\n useRemoveNodeButton,\n useSelected,\n withHOC,\n} from 'platejs/react';\nimport { useElementSelector } from 'platejs/react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuPortal,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport {\n Popover,\n PopoverAnchor,\n PopoverContent,\n} from '@/components/ui/popover';\nimport { cn } from '@/lib/utils';\n\nimport { blockSelectionVariants } from './block-selection';\nimport {\n ColorDropdownMenuItems,\n DEFAULT_COLORS,\n} from './font-color-toolbar-button';\nimport {\n BorderAllIcon,\n BorderBottomIcon,\n BorderLeftIcon,\n BorderNoneIcon,\n BorderRightIcon,\n BorderTopIcon,\n} from './table-icons';\nimport {\n Toolbar,\n ToolbarButton,\n ToolbarGroup,\n ToolbarMenuGroup,\n} from './toolbar';\n\ntype TableResizeDirection = 'bottom' | 'left' | 'right';\n\ntype TableResizeStartOptions = {\n colIndex: number;\n direction: TableResizeDirection;\n handleKey: string;\n rowIndex: number;\n};\n\ntype TableResizeDragState = {\n colIndex: number;\n direction: TableResizeDirection;\n initialPosition: number;\n initialSize: number;\n marginLeft: number;\n rowIndex: number;\n};\n\ntype TableResizeContextValue = {\n disableMarginLeft: boolean;\n clearResizePreview: (handleKey: string) => void;\n setResizePreview: (\n event: React.PointerEvent<HTMLDivElement>,\n options: TableResizeStartOptions\n ) => void;\n startResize: (\n event: React.PointerEvent<HTMLDivElement>,\n options: TableResizeStartOptions\n ) => void;\n};\n\nconst TABLE_CONTROL_COLUMN_WIDTH = 8;\nconst TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT = 1200;\n\nconst TableResizeContext = React.createContext<TableResizeContextValue | null>(\n null\n);\n\nfunction useTableResizeContext() {\n const context = React.useContext(TableResizeContext);\n\n if (!context) {\n throw new Error('TableResizeContext is missing');\n }\n\n return context;\n}\n\nfunction useTableResizeController({\n deferColumnResize,\n dragIndicatorRef,\n hoverIndicatorRef,\n marginLeft,\n controlColumnWidth,\n tablePath,\n tableRef,\n wrapperRef,\n}: {\n deferColumnResize: boolean;\n dragIndicatorRef: React.RefObject<HTMLDivElement | null>;\n hoverIndicatorRef: React.RefObject<HTMLDivElement | null>;\n marginLeft: number;\n controlColumnWidth: number;\n tablePath: number[];\n tableRef: React.RefObject<HTMLTableElement | null>;\n wrapperRef: React.RefObject<HTMLDivElement | null>;\n}) {\n const { editor, getOptions } = useEditorPlugin(TablePlugin);\n const { disableMarginLeft = false, minColumnWidth = 0 } = getOptions();\n const colSizes = useTableColSizes({ disableOverrides: true });\n const colSizesRef = React.useRef(colSizes);\n const activeHandleKeyRef = React.useRef<string | null>(null);\n const activeRowElementRef = React.useRef<HTMLTableRowElement | null>(null);\n const cleanupListenersRef = React.useRef<(() => void) | null>(null);\n const marginLeftRef = React.useRef(marginLeft);\n const dragStateRef = React.useRef<TableResizeDragState | null>(null);\n const previewHandleKeyRef = React.useRef<string | null>(null);\n const overrideColSize = useOverrideColSize();\n const overrideMarginLeft = useOverrideMarginLeft();\n const overrideRowSize = useOverrideRowSize();\n\n React.useEffect(() => {\n colSizesRef.current = colSizes;\n }, [colSizes]);\n\n React.useEffect(() => {\n marginLeftRef.current = marginLeft;\n }, [marginLeft]);\n\n const hideDeferredResizeIndicator = React.useCallback(() => {\n const indicator = dragIndicatorRef.current;\n\n if (!indicator) return;\n\n indicator.style.display = 'none';\n indicator.style.removeProperty('left');\n }, [dragIndicatorRef]);\n\n const showDeferredResizeIndicator = React.useCallback(\n (offset: number) => {\n const indicator = dragIndicatorRef.current;\n\n if (!indicator) return;\n\n indicator.style.display = 'block';\n indicator.style.left = `${offset}px`;\n },\n [dragIndicatorRef]\n );\n\n const hideResizeIndicator = React.useCallback(() => {\n const indicator = hoverIndicatorRef.current;\n\n if (!indicator) return;\n\n indicator.style.display = 'none';\n indicator.style.removeProperty('left');\n }, [hoverIndicatorRef]);\n\n const showResizeIndicatorAtOffset = React.useCallback(\n (offset: number) => {\n const indicator = hoverIndicatorRef.current;\n\n if (!indicator) return;\n\n indicator.style.display = 'block';\n indicator.style.left = `${offset}px`;\n },\n [hoverIndicatorRef]\n );\n\n const showResizeIndicator = React.useCallback(\n ({\n event,\n direction,\n }: Pick<TableResizeStartOptions, 'direction'> & {\n event: React.PointerEvent<HTMLDivElement>;\n }) => {\n if (direction === 'bottom') return;\n\n const wrapper = wrapperRef.current;\n\n if (!wrapper) return;\n\n const handleRect = event.currentTarget.getBoundingClientRect();\n const wrapperRect = wrapper.getBoundingClientRect();\n const boundaryOffset =\n handleRect.left - wrapperRect.left + handleRect.width / 2;\n\n showResizeIndicatorAtOffset(boundaryOffset);\n },\n [showResizeIndicatorAtOffset, wrapperRef]\n );\n\n const setResizePreview = React.useCallback(\n (\n event: React.PointerEvent<HTMLDivElement>,\n options: TableResizeStartOptions\n ) => {\n if (activeHandleKeyRef.current) return;\n\n previewHandleKeyRef.current = options.handleKey;\n showResizeIndicator({ ...options, event });\n },\n [showResizeIndicator]\n );\n\n const clearResizePreview = React.useCallback(\n (handleKey: string) => {\n if (activeHandleKeyRef.current) return;\n if (previewHandleKeyRef.current !== handleKey) return;\n\n previewHandleKeyRef.current = null;\n hideResizeIndicator();\n },\n [hideResizeIndicator]\n );\n\n const commitColSize = React.useCallback(\n (colIndex: number, width: number) => {\n setTableColSize(editor, { colIndex, width }, { at: tablePath });\n setTimeout(() => overrideColSize(colIndex, null), 0);\n },\n [editor, overrideColSize, tablePath]\n );\n\n const commitRowSize = React.useCallback(\n (rowIndex: number, height: number) => {\n setTableRowSize(editor, { height, rowIndex }, { at: tablePath });\n setTimeout(() => overrideRowSize(rowIndex, null), 0);\n },\n [editor, overrideRowSize, tablePath]\n );\n\n const commitMarginLeft = React.useCallback(\n (nextMarginLeft: number) => {\n setTableMarginLeft(\n editor,\n { marginLeft: nextMarginLeft },\n { at: tablePath }\n );\n setTimeout(() => overrideMarginLeft(null), 0);\n },\n [editor, overrideMarginLeft, tablePath]\n );\n\n const getColumnBoundaryOffset = React.useCallback(\n (colIndex: number, currentWidth: number) =>\n controlColumnWidth +\n colSizesRef.current\n .slice(0, colIndex)\n .reduce((total, colSize) => total + colSize, 0) +\n currentWidth,\n [controlColumnWidth]\n );\n\n const applyResize = React.useCallback(\n (event: PointerEvent, finished: boolean) => {\n const dragState = dragStateRef.current;\n\n if (!dragState) return;\n\n const currentPosition =\n dragState.direction === 'bottom' ? event.clientY : event.clientX;\n const delta = currentPosition - dragState.initialPosition;\n\n if (dragState.direction === 'bottom') {\n const newHeight = roundCellSizeToStep(\n dragState.initialSize + delta,\n undefined\n );\n\n if (finished) {\n commitRowSize(dragState.rowIndex, newHeight);\n } else {\n overrideRowSize(dragState.rowIndex, newHeight);\n }\n\n return;\n }\n\n if (dragState.direction === 'left') {\n const initial =\n colSizesRef.current[dragState.colIndex] ?? dragState.initialSize;\n const complement = (width: number) =>\n initial + dragState.marginLeft - width;\n const nextMarginLeft = roundCellSizeToStep(\n resizeLengthClampStatic(dragState.marginLeft + delta, {\n max: complement(minColumnWidth),\n min: 0,\n }),\n undefined\n );\n const nextWidth = complement(nextMarginLeft);\n\n if (finished) {\n commitMarginLeft(nextMarginLeft);\n commitColSize(dragState.colIndex, nextWidth);\n } else if (deferColumnResize) {\n showDeferredResizeIndicator(\n controlColumnWidth + (nextMarginLeft - dragState.marginLeft)\n );\n } else {\n showResizeIndicatorAtOffset(\n controlColumnWidth + (nextMarginLeft - dragState.marginLeft)\n );\n overrideMarginLeft(nextMarginLeft);\n overrideColSize(dragState.colIndex, nextWidth);\n }\n\n return;\n }\n\n const currentInitial =\n colSizesRef.current[dragState.colIndex] ?? dragState.initialSize;\n const nextInitial = colSizesRef.current[dragState.colIndex + 1];\n const complement = (width: number) =>\n currentInitial + nextInitial - width;\n const currentWidth = roundCellSizeToStep(\n resizeLengthClampStatic(currentInitial + delta, {\n max: nextInitial ? complement(minColumnWidth) : undefined,\n min: minColumnWidth,\n }),\n undefined\n );\n const nextWidth = nextInitial ? complement(currentWidth) : undefined;\n\n if (finished) {\n commitColSize(dragState.colIndex, currentWidth);\n\n if (nextWidth !== undefined) {\n commitColSize(dragState.colIndex + 1, nextWidth);\n }\n } else if (deferColumnResize) {\n showDeferredResizeIndicator(\n getColumnBoundaryOffset(dragState.colIndex, currentWidth)\n );\n } else {\n showResizeIndicatorAtOffset(\n getColumnBoundaryOffset(dragState.colIndex, currentWidth)\n );\n overrideColSize(dragState.colIndex, currentWidth);\n\n if (nextWidth !== undefined) {\n overrideColSize(dragState.colIndex + 1, nextWidth);\n }\n }\n },\n [\n commitColSize,\n commitMarginLeft,\n commitRowSize,\n controlColumnWidth,\n deferColumnResize,\n getColumnBoundaryOffset,\n showDeferredResizeIndicator,\n showResizeIndicatorAtOffset,\n minColumnWidth,\n overrideColSize,\n overrideMarginLeft,\n overrideRowSize,\n ]\n );\n\n const stopResize = React.useCallback(() => {\n cleanupListenersRef.current?.();\n cleanupListenersRef.current = null;\n activeHandleKeyRef.current = null;\n previewHandleKeyRef.current = null;\n dragStateRef.current = null;\n\n if (activeRowElementRef.current) {\n delete activeRowElementRef.current.dataset.tableResizing;\n activeRowElementRef.current = null;\n }\n\n hideDeferredResizeIndicator();\n hideResizeIndicator();\n }, [hideDeferredResizeIndicator, hideResizeIndicator]);\n\n React.useEffect(() => stopResize, [stopResize]);\n\n const startResize = React.useCallback(\n (\n event: React.PointerEvent<HTMLDivElement>,\n { colIndex, direction, handleKey, rowIndex }: TableResizeStartOptions\n ) => {\n const rowHeight =\n tableRef.current?.rows.item(rowIndex)?.getBoundingClientRect().height ??\n 0;\n\n dragStateRef.current = {\n colIndex,\n direction,\n initialPosition: direction === 'bottom' ? event.clientY : event.clientX,\n initialSize:\n direction === 'bottom'\n ? rowHeight\n : (colSizesRef.current[colIndex] ?? 0),\n marginLeft: marginLeftRef.current,\n rowIndex,\n };\n activeHandleKeyRef.current = handleKey;\n previewHandleKeyRef.current = null;\n\n const rowElement = tableRef.current?.rows.item(rowIndex) ?? null;\n\n if (\n activeRowElementRef.current &&\n activeRowElementRef.current !== rowElement\n ) {\n delete activeRowElementRef.current.dataset.tableResizing;\n }\n\n activeRowElementRef.current = rowElement;\n\n if (rowElement) {\n rowElement.dataset.tableResizing = 'true';\n }\n\n cleanupListenersRef.current?.();\n\n const handlePointerMove = (pointerEvent: PointerEvent) => {\n applyResize(pointerEvent, false);\n };\n\n const handlePointerEnd = (pointerEvent: PointerEvent) => {\n applyResize(pointerEvent, true);\n stopResize();\n };\n\n window.addEventListener('pointermove', handlePointerMove);\n window.addEventListener('pointerup', handlePointerEnd);\n window.addEventListener('pointercancel', handlePointerEnd);\n\n cleanupListenersRef.current = () => {\n window.removeEventListener('pointermove', handlePointerMove);\n window.removeEventListener('pointerup', handlePointerEnd);\n window.removeEventListener('pointercancel', handlePointerEnd);\n };\n\n if (deferColumnResize && direction !== 'bottom') {\n hideResizeIndicator();\n showDeferredResizeIndicator(\n direction === 'left'\n ? controlColumnWidth\n : getColumnBoundaryOffset(\n colIndex,\n colSizesRef.current[colIndex] ?? 0\n )\n );\n } else {\n showResizeIndicator({ direction, event });\n }\n\n event.preventDefault();\n event.stopPropagation();\n },\n [\n controlColumnWidth,\n deferColumnResize,\n getColumnBoundaryOffset,\n hideResizeIndicator,\n showDeferredResizeIndicator,\n showResizeIndicator,\n stopResize,\n tableRef,\n applyResize,\n ]\n );\n\n return React.useMemo(\n () => ({\n clearResizePreview,\n disableMarginLeft,\n setResizePreview,\n startResize,\n }),\n [clearResizePreview, disableMarginLeft, setResizePreview, startResize]\n );\n}\n\nexport const TableElement = withHOC(\n TableProvider,\n function TableElement({\n children,\n ...props\n }: PlateElementProps<TTableElement>) {\n const readOnly = useReadOnly();\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n const hasControls = !readOnly && !isSelectionAreaVisible;\n const {\n isSelectingCell,\n marginLeft,\n props: tableProps,\n } = useTableElement();\n const colSizes = useTableColSizes();\n const controlColumnWidth = hasControls ? TABLE_CONTROL_COLUMN_WIDTH : 0;\n const dragIndicatorRef = React.useRef<HTMLDivElement>(null);\n const hoverIndicatorRef = React.useRef<HTMLDivElement>(null);\n const deferColumnResize =\n colSizes.length * props.element.children.length >\n TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT;\n const tablePath = useElementSelector(([, path]) => path, [], {\n key: KEYS.table,\n });\n const tableRef = React.useRef<HTMLTableElement>(null);\n const wrapperRef = React.useRef<HTMLDivElement>(null);\n const resizeController = useTableResizeController({\n controlColumnWidth,\n deferColumnResize,\n dragIndicatorRef,\n hoverIndicatorRef,\n marginLeft,\n tablePath,\n tableRef,\n wrapperRef,\n });\n const tableVariableStyle = React.useMemo(() => {\n if (colSizes.length === 0) {\n return;\n }\n\n return {\n ...Object.fromEntries(\n colSizes.map((colSize, index) => [\n `--table-col-${index}`,\n `${colSize}px`,\n ])\n ),\n } as React.CSSProperties;\n }, [colSizes]);\n const tableStyle = React.useMemo(\n () =>\n ({\n width: `${\n colSizes.reduce((total, colSize) => total + colSize, 0) +\n controlColumnWidth\n }px`,\n }) as React.CSSProperties,\n [colSizes, controlColumnWidth]\n );\n\n const isSelectingTable = useBlockSelected(props.element.id as string);\n\n const content = (\n <PlateElement\n {...props}\n className={cn(\n 'overflow-x-auto py-5',\n hasControls && '-ml-2 *:data-[slot=block-selection]:left-2'\n )}\n style={{ paddingLeft: marginLeft }}\n >\n <TableResizeContext.Provider value={resizeController}>\n <div\n ref={wrapperRef}\n className=\"group/table relative w-fit\"\n style={tableVariableStyle}\n >\n <div\n ref={dragIndicatorRef}\n className=\"-translate-x-[1.5px] pointer-events-none absolute inset-y-0 z-36 hidden w-[3px] bg-ring/70\"\n contentEditable={false}\n />\n <div\n ref={hoverIndicatorRef}\n className=\"-translate-x-[1.5px] pointer-events-none absolute inset-y-0 z-35 hidden w-[3px] bg-ring/80\"\n contentEditable={false}\n />\n <table\n ref={tableRef}\n className={cn(\n 'mr-0 ml-px table h-px table-fixed border-collapse',\n isSelectingCell && 'selection:bg-transparent'\n )}\n style={tableStyle}\n {...tableProps}\n >\n {colSizes.length > 0 && (\n <colgroup>\n {hasControls && (\n <col\n style={{\n maxWidth: TABLE_CONTROL_COLUMN_WIDTH,\n minWidth: TABLE_CONTROL_COLUMN_WIDTH,\n width: TABLE_CONTROL_COLUMN_WIDTH,\n }}\n />\n )}\n {colSizes.map((colSize, index) => (\n <col\n key={index}\n style={\n colSize\n ? {\n maxWidth: colSize,\n minWidth: colSize,\n width: colSize,\n }\n : undefined\n }\n />\n ))}\n </colgroup>\n )}\n <tbody className=\"min-w-full\">{children}</tbody>\n </table>\n\n {isSelectingTable && (\n <div\n className={blockSelectionVariants()}\n contentEditable={false}\n />\n )}\n </div>\n </TableResizeContext.Provider>\n </PlateElement>\n );\n\n if (readOnly) {\n return content;\n }\n\n return <TableFloatingToolbar>{content}</TableFloatingToolbar>;\n }\n);\n\nfunction TableFloatingToolbar({\n children,\n ...props\n}: React.ComponentProps<typeof PopoverContent>) {\n const { tf } = useEditorPlugin(TablePlugin);\n const selected = useSelected();\n const element = useElement<TTableElement>();\n const { props: buttonProps } = useRemoveNodeButton({ element });\n const collapsedInside = useEditorSelector(\n (editor) => selected && editor.api.isCollapsed(),\n [selected]\n );\n const isFocusedLast = useFocusedLast();\n\n const { canMerge, canSplit } = useTableMergeState();\n\n return (\n <Popover\n open={isFocusedLast && (canMerge || canSplit || collapsedInside)}\n modal={false}\n >\n <PopoverAnchor asChild>{children}</PopoverAnchor>\n <PopoverContent\n asChild\n onOpenAutoFocus={(e) => e.preventDefault()}\n contentEditable={false}\n {...props}\n >\n <Toolbar\n className=\"scrollbar-hide flex w-auto max-w-[80vw] flex-row overflow-x-auto rounded-md border bg-popover p-1 shadow-md print:hidden\"\n contentEditable={false}\n >\n <ToolbarGroup>\n <ColorDropdownMenu tooltip=\"Background color\">\n <PaintBucketIcon />\n </ColorDropdownMenu>\n {canMerge && (\n <ToolbarButton\n onClick={() => tf.table.merge()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Merge cells\"\n >\n <CombineIcon />\n </ToolbarButton>\n )}\n {canSplit && (\n <ToolbarButton\n onClick={() => tf.table.split()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Split cell\"\n >\n <SquareSplitHorizontalIcon />\n </ToolbarButton>\n )}\n\n <DropdownMenu modal={false}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton tooltip=\"Cell borders\">\n <Grid2X2Icon />\n </ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuPortal>\n <TableBordersDropdownMenuContent />\n </DropdownMenuPortal>\n </DropdownMenu>\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton tooltip=\"Delete table\" {...buttonProps}>\n <Trash2Icon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n </ToolbarGroup>\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableRow({ before: true });\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert row before\"\n >\n <ArrowUp />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableRow();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert row after\"\n >\n <ArrowDown />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.remove.tableRow();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Delete row\"\n >\n <XIcon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableColumn({ before: true });\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert column before\"\n >\n <ArrowLeft />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableColumn();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert column after\"\n >\n <ArrowRight />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.remove.tableColumn();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Delete column\"\n >\n <XIcon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n </Toolbar>\n </PopoverContent>\n </Popover>\n );\n}\n\nfunction TableBordersDropdownMenuContent(\n props: React.ComponentProps<typeof DropdownMenuContent>\n) {\n const editor = useEditorRef();\n const {\n getOnSelectTableBorder,\n hasBottomBorder,\n hasLeftBorder,\n hasNoBorders,\n hasOuterBorders,\n hasRightBorder,\n hasTopBorder,\n } = useTableBordersDropdownMenuContentState();\n\n return (\n <DropdownMenuContent\n className=\"min-w-[220px]\"\n onCloseAutoFocus={(e) => {\n e.preventDefault();\n editor.tf.focus();\n }}\n align=\"start\"\n side=\"right\"\n sideOffset={0}\n {...props}\n >\n <DropdownMenuGroup>\n <DropdownMenuCheckboxItem\n checked={hasTopBorder}\n onCheckedChange={getOnSelectTableBorder('top')}\n >\n <BorderTopIcon />\n <div>Top Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasRightBorder}\n onCheckedChange={getOnSelectTableBorder('right')}\n >\n <BorderRightIcon />\n <div>Right Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasBottomBorder}\n onCheckedChange={getOnSelectTableBorder('bottom')}\n >\n <BorderBottomIcon />\n <div>Bottom Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasLeftBorder}\n onCheckedChange={getOnSelectTableBorder('left')}\n >\n <BorderLeftIcon />\n <div>Left Border</div>\n </DropdownMenuCheckboxItem>\n </DropdownMenuGroup>\n\n <DropdownMenuGroup>\n <DropdownMenuCheckboxItem\n checked={hasNoBorders}\n onCheckedChange={getOnSelectTableBorder('none')}\n >\n <BorderNoneIcon />\n <div>No Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasOuterBorders}\n onCheckedChange={getOnSelectTableBorder('outer')}\n >\n <BorderAllIcon />\n <div>Outside Borders</div>\n </DropdownMenuCheckboxItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n );\n}\n\nfunction ColorDropdownMenu({\n children,\n tooltip,\n}: {\n children: React.ReactNode;\n tooltip: string;\n}) {\n const [open, setOpen] = React.useState(false);\n\n const editor = useEditorRef();\n const selectedCells = usePluginOption(TablePlugin, 'selectedCells') as\n | TElement[]\n | null;\n\n const onUpdateColor = React.useCallback(\n (color: string) => {\n setOpen(false);\n setCellBackground(editor, { color, selectedCells: selectedCells ?? [] });\n },\n [selectedCells, editor]\n );\n\n const onClearColor = React.useCallback(() => {\n setOpen(false);\n setCellBackground(editor, {\n color: null,\n selectedCells: selectedCells ?? [],\n });\n }, [selectedCells, editor]);\n\n return (\n <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton tooltip={tooltip}>{children}</ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align=\"start\">\n <ToolbarMenuGroup label=\"Colors\">\n <ColorDropdownMenuItems\n className=\"px-2\"\n colors={DEFAULT_COLORS}\n updateColor={onUpdateColor}\n />\n </ToolbarMenuGroup>\n <DropdownMenuGroup>\n <DropdownMenuItem className=\"p-2\" onClick={onClearColor}>\n <EraserIcon />\n <span>Clear</span>\n </DropdownMenuItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\nexport function TableRowElement({\n children,\n ...props\n}: PlateElementProps<TTableRowElement>) {\n const { element } = props;\n const readOnly = useReadOnly();\n const selected = useSelected();\n const editor = useEditorRef();\n const rowIndex = useElementSelector(([, path]) => path.at(-1) as number, [], {\n key: KEYS.tr,\n });\n const rowSize = useElementSelector(\n ([node]) => (node as TTableRowElement).size,\n [],\n {\n key: KEYS.tr,\n }\n );\n const rowSizeOverrides = useTableValue('rowSizeOverrides');\n const rowMinHeight = rowSizeOverrides.get?.(rowIndex) ?? rowSize;\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n const hasControls = !readOnly && !isSelectionAreaVisible;\n\n const { isDragging, nodeRef, previewRef, handleRef } = useDraggable({\n element,\n type: element.type,\n canDropNode: ({ dragEntry, dropEntry }) =>\n PathApi.equals(\n PathApi.parent(dragEntry[1]),\n PathApi.parent(dropEntry[1])\n ),\n onDropHandler: (_, { dragItem }) => {\n const dragElement = (dragItem as { element: TElement }).element;\n\n if (dragElement) {\n editor.tf.select(dragElement);\n }\n },\n });\n\n return (\n <PlateElement\n {...props}\n ref={useComposedRef(props.ref, previewRef, nodeRef)}\n as=\"tr\"\n className={cn('group/row', isDragging && 'opacity-50')}\n style={\n {\n ...props.style,\n '--tableRowMinHeight': rowMinHeight ? `${rowMinHeight}px` : undefined,\n } as React.CSSProperties\n }\n attributes={{\n ...props.attributes,\n 'data-selected': selected ? 'true' : undefined,\n }}\n >\n {hasControls && (\n <td\n className=\"w-2 min-w-2 max-w-2 select-none p-0\"\n contentEditable={false}\n >\n <RowDragHandle dragRef={handleRef} />\n <RowDropLine />\n </td>\n )}\n\n {children}\n </PlateElement>\n );\n}\n\nfunction useTableCellPresentation(element: TTableCellElement) {\n const { api, setOption } = useEditorPlugin(TablePlugin);\n const borders = useTableCellBorders({ element });\n const { col, row } = useCellIndices();\n const selected = useIsCellSelected(element);\n const selectedCells = usePluginOption(TablePlugin, 'selectedCells') as\n | TElement[]\n | null;\n\n React.useEffect(() => {\n if (\n selectedCells?.some((cell) => cell.id === element.id && cell !== element)\n ) {\n setOption(\n 'selectedCells',\n selectedCells.map((cell) => (cell.id === element.id ? element : cell))\n );\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [element]);\n\n const colSpan = api.table.getColSpan(element);\n const rowSpan = api.table.getRowSpan(element);\n const width = React.useMemo(() => {\n const terms = Array.from(\n { length: colSpan },\n (_, offset) => `var(--table-col-${col + offset}, 120px)`\n );\n\n return terms.length === 1 ? terms[0]! : `calc(${terms.join(' + ')})`;\n }, [col, colSpan]);\n\n return {\n borders,\n colIndex: col + colSpan - 1,\n colSpan,\n rowIndex: row + rowSpan - 1,\n rowSpan,\n selected,\n width,\n };\n}\n\nfunction RowDragHandle({ dragRef }: { dragRef: React.Ref<any> }) {\n const editor = useEditorRef();\n const element = useElement();\n\n return (\n <Button\n ref={dragRef}\n variant=\"outline\"\n className={cn(\n '-translate-y-1/2 absolute top-1/2 left-0 z-51 h-6 w-4 p-0 focus-visible:ring-0 focus-visible:ring-offset-0',\n 'cursor-grab active:cursor-grabbing',\n 'opacity-0 transition-opacity duration-100 group-hover/row:opacity-100 group-data-[table-resizing=true]/row:opacity-0'\n )}\n onClick={() => {\n editor.tf.select(element);\n }}\n >\n <GripVertical className=\"text-muted-foreground\" />\n </Button>\n );\n}\n\nfunction RowDropLine() {\n const { dropLine } = useDropLine();\n\n if (!dropLine) return null;\n\n return (\n <div\n className={cn(\n 'absolute inset-x-0 left-2 z-50 h-0.5 bg-brand/50',\n dropLine === 'top' ? '-top-px' : '-bottom-px'\n )}\n />\n );\n}\n\nexport function TableCellElement({\n isHeader,\n ...props\n}: PlateElementProps<TTableCellElement> & {\n isHeader?: boolean;\n}) {\n const readOnly = useReadOnly();\n const element = props.element;\n\n const tableId = useElementSelector(([node]) => node.id as string, [], {\n key: KEYS.table,\n });\n const rowId = useElementSelector(([node]) => node.id as string, [], {\n key: KEYS.tr,\n });\n const isSelectingTable = useBlockSelected(tableId);\n const isSelectingRow = useBlockSelected(rowId) || isSelectingTable;\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n\n const {\n borders,\n colIndex,\n colSpan,\n rowIndex,\n rowSpan,\n selected: cellSelected,\n width,\n } = useTableCellPresentation(element);\n\n return (\n <PlateElement\n {...props}\n as={isHeader ? 'th' : 'td'}\n className={cn(\n 'relative h-full overflow-visible border-none bg-background p-0',\n element.background ? 'bg-(--cellBackground)' : 'bg-background',\n isHeader && 'text-left *:m-0',\n 'before:size-full',\n cellSelected && 'before:z-10 before:bg-brand/5',\n \"before:absolute before:box-border before:select-none before:content-['']\",\n borders.bottom?.size && 'before:border-b before:border-b-border',\n borders.right?.size && 'before:border-r before:border-r-border',\n borders.left?.size && 'before:border-l before:border-l-border',\n borders.top?.size && 'before:border-t before:border-t-border'\n )}\n style={\n {\n '--cellBackground': element.background,\n maxWidth: width,\n minWidth: width,\n } as React.CSSProperties\n }\n attributes={{\n ...props.attributes,\n colSpan,\n rowSpan,\n }}\n >\n <div\n className=\"relative z-20 box-border h-full px-3 py-2\"\n style={\n rowSpan === 1\n ? { minHeight: 'var(--tableRowMinHeight, 0px)' }\n : undefined\n }\n >\n {props.children}\n </div>\n\n {!readOnly && !isSelectionAreaVisible && (\n <TableCellResizeControls colIndex={colIndex} rowIndex={rowIndex} />\n )}\n\n {isSelectingRow && (\n <div className={blockSelectionVariants()} contentEditable={false} />\n )}\n </PlateElement>\n );\n}\n\nexport function TableCellHeaderElement(\n props: React.ComponentProps<typeof TableCellElement>\n) {\n return <TableCellElement {...props} isHeader />;\n}\n\nconst TableCellResizeControls = React.memo(function TableCellResizeControls({\n colIndex,\n rowIndex,\n}: {\n colIndex: number;\n rowIndex: number;\n}) {\n const {\n clearResizePreview,\n disableMarginLeft,\n setResizePreview,\n startResize,\n } = useTableResizeContext();\n const rightHandleKey = `right:${rowIndex}:${colIndex}`;\n const bottomHandleKey = `bottom:${rowIndex}:${colIndex}`;\n const leftHandleKey = `left:${rowIndex}:${colIndex}`;\n const isLeftHandle = colIndex === 0 && !disableMarginLeft;\n\n return (\n <div\n className=\"group/resize pointer-events-none absolute inset-0 z-30 select-none\"\n contentEditable={false}\n suppressContentEditableWarning={true}\n >\n <div\n className=\"-top-2 -right-1 pointer-events-auto absolute z-40 h-[calc(100%_+_8px)] w-2 cursor-col-resize touch-none\"\n onPointerEnter={(event) => {\n setResizePreview(event, {\n colIndex,\n direction: 'right',\n handleKey: rightHandleKey,\n rowIndex,\n });\n }}\n onPointerLeave={() => {\n clearResizePreview(rightHandleKey);\n }}\n onPointerDown={(event) => {\n startResize(event, {\n colIndex,\n direction: 'right',\n handleKey: rightHandleKey,\n rowIndex,\n });\n }}\n />\n <div\n className=\"-bottom-1 pointer-events-auto absolute left-0 z-40 h-2 w-full cursor-row-resize touch-none\"\n onPointerEnter={(event) => {\n setResizePreview(event, {\n colIndex,\n direction: 'bottom',\n handleKey: bottomHandleKey,\n rowIndex,\n });\n }}\n onPointerLeave={() => {\n clearResizePreview(bottomHandleKey);\n }}\n onPointerDown={(event) => {\n startResize(event, {\n colIndex,\n direction: 'bottom',\n handleKey: bottomHandleKey,\n rowIndex,\n });\n }}\n />\n {isLeftHandle && (\n <div\n className=\"-left-1 pointer-events-auto absolute top-0 z-40 h-full w-2 cursor-col-resize touch-none\"\n onPointerEnter={(event) => {\n setResizePreview(event, {\n colIndex,\n direction: 'left',\n handleKey: leftHandleKey,\n rowIndex,\n });\n }}\n onPointerLeave={() => {\n clearResizePreview(leftHandleKey);\n }}\n onPointerDown={(event) => {\n startResize(event, {\n colIndex,\n direction: 'left',\n handleKey: leftHandleKey,\n rowIndex,\n });\n }}\n />\n )}\n </div>\n );\n});\n\nTableCellResizeControls.displayName = 'TableCellResizeControls';\n",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep non-zero default widths for tables without colSizes

This update makes table-node always derive CSS column widths from useTableColSizes() and sets <table style.width> to their sum, but tables created without explicit colSizes (e.g. default insert.table() output unless initialTableWidth is configured) produce zero-filled sizes, so the new code resolves each column to 0px and collapses the table to the control column. The previous implementation had a 120/240 fallback path for unset widths, so this is a rendering regression for valid table content that lacks persisted column sizes.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants