返回 presentation-ai
table-node.tsx
根目录 / src / components / plate / ui / table-node.tsx
1 "use client";
2
3 import { useDraggable, useDropLine } from "@platejs/dnd";
4 import {
5 BlockSelectionPlugin,
6 useBlockSelected,
7 } from "@platejs/selection/react";
8 import { isSelectingCell, setCellBackground } from "@platejs/table";
9 import {
10 TablePlugin,
11 TableProvider,
12 useTableBordersDropdownMenuContentState,
13 useTableCellElement,
14 useTableCellElementResizable,
15 useTableElement,
16 useTableMergeState,
17 } from "@platejs/table/react";
18 import type * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
19 import { PopoverAnchor } from "@radix-ui/react-popover";
20 import {
21 ArrowDown,
22 ArrowLeft,
23 ArrowRight,
24 ArrowUp,
25 CombineIcon,
26 EraserIcon,
27 Grid2X2Icon,
28 GripVertical,
29 PaintBucketIcon,
30 Plus,
31 SquareSplitHorizontalIcon,
32 Trash2Icon,
33 XIcon,
34 } from "lucide-react";
35 import {
36 ElementApi,
37 KEYS,
38 PathApi,
39 type Path,
40 type TTableCellElement,
41 type TTableElement,
42 type TTableRowElement,
43 } from "platejs";
44 import {
45 PlateElement,
46 useEditorPlugin,
47 useEditorRef,
48 useEditorSelector,
49 useElement,
50 useElementSelector,
51 usePluginOption,
52 useReadOnly,
53 useRemoveNodeButton,
54 useSelected,
55 withHOC,
56 type PlateElementProps,
57 } from "platejs/react";
58 import * as React from "react";
59
60 import { Button } from "@/components/plate/ui/button";
61 import {
62 DropdownMenu,
63 DropdownMenuCheckboxItem,
64 DropdownMenuContent,
65 DropdownMenuGroup,
66 DropdownMenuItem,
67 DropdownMenuPortal,
68 DropdownMenuTrigger,
69 } from "@/components/plate/ui/dropdown-menu";
70 import { Popover, PopoverContent } from "@/components/plate/ui/popover";
71 import { cn } from "@/lib/utils";
72 import { blockSelectionVariants } from "./block-selection";
73 import { ColorDropdownMenuItems, DEFAULT_COLORS } from "./font-color-toolbar-button";
74 import { ResizeHandle } from "./resize-handle";
75 import {
76 BorderAllIcon,
77 BorderBottomIcon,
78 BorderLeftIcon,
79 BorderNoneIcon,
80 BorderRightIcon,
81 BorderTopIcon,
82 } from "./table-icons";
83 import {
84 Toolbar,
85 ToolbarButton,
86 ToolbarGroup,
87 ToolbarMenuGroup,
88 } from "./toolbar";
89
90 function setRefValue<T>(ref: React.Ref<T> | undefined, value: T | null) {
91 if (typeof ref === "function") {
92 ref(value);
93 return;
94 }
95
96 if (ref) {
97 ref.current = value;
98 }
99 }
100
101 function getLastTableRowPath(
102 tableElement: TTableElement,
103 tablePath: Path,
104 ): Path | undefined {
105 if (tableElement.children.length === 0) return undefined;
106
107 return [...tablePath, tableElement.children.length - 1];
108 }
109
110 function getLastTableCellPath(
111 tableElement: TTableElement,
112 tablePath: Path,
113 ): Path | undefined {
114 for (
115 let rowIndex = tableElement.children.length - 1;
116 rowIndex >= 0;
117 rowIndex -= 1
118 ) {
119 const row = tableElement.children[rowIndex];
120
121 if (!ElementApi.isElement(row)) continue;
122
123 if (row.children.length > 0) {
124 return [...tablePath, rowIndex, row.children.length - 1];
125 }
126 }
127
128 return undefined;
129 }
130
131 export const TableElement = withHOC(
132 TableProvider,
133 function TableElement({
134 children,
135 ...props
136 }: PlateElementProps<TTableElement>) {
137 const editor = useEditorRef();
138 const { tf } = useEditorPlugin(TablePlugin);
139 const readOnly = useReadOnly();
140 const isSelectionAreaVisible = usePluginOption(
141 BlockSelectionPlugin,
142 "isSelectionAreaVisible",
143 );
144 const hasControls = !readOnly && !isSelectionAreaVisible;
145 const selected = useSelected();
146 const { marginLeft, props: tableProps } = useTableElement();
147 const isSelectingCells = isSelectingCell(props.editor);
148 const tableId = props.element.id;
149 const isTableBlockSelected = useBlockSelected(
150 typeof tableId === "string" ? tableId : undefined,
151 );
152 const isSelectionInCurrentTable = useEditorSelector(
153 (currentEditor) => {
154 const tableEntry = currentEditor.api.above({
155 match: { type: KEYS.table },
156 });
157 const [selectedTable] = tableEntry ?? [];
158
159 return typeof tableId === "string" && selectedTable?.id === tableId;
160 },
161 [tableId],
162 );
163 const showTableGrowthControls =
164 hasControls && (isTableBlockSelected || isSelectionInCurrentTable);
165
166 const colSizes = props.element.colSizes ?? [];
167
168 const insertColumnAfter = React.useCallback(() => {
169 const tablePath = editor.api.findPath(props.element);
170 if (!tablePath) return;
171
172 const fromCell = getLastTableCellPath(props.element, tablePath);
173 if (!fromCell) return;
174
175 tf.insert.tableColumn({ fromCell, select: true });
176 editor.tf.focus();
177 }, [editor, props.element, tf]);
178
179 const insertRowAfter = React.useCallback(() => {
180 const tablePath = editor.api.findPath(props.element);
181 if (!tablePath) return;
182
183 const fromRow = getLastTableRowPath(props.element, tablePath);
184 if (!fromRow) return;
185
186 tf.insert.tableRow({ fromRow, select: true });
187 editor.tf.focus();
188 }, [editor, props.element, tf]);
189
190 const content = (
191 <PlateElement
192 {...props}
193 className={cn(
194 "overflow-x-auto py-5",
195 hasControls && "-ml-2 data-[slot=block-selection]:*:left-2",
196 )}
197 style={{ paddingLeft: marginLeft }}
198 >
199 <div
200 className={cn(
201 "group/table relative w-full bg-transparent",
202 showTableGrowthControls &&
203 "grid grid-cols-[minmax(0,1fr)_1.25rem] grid-rows-[auto_1.25rem]",
204 )}
205 >
206 {isTableBlockSelected && (
207 <div className={blockSelectionVariants()} contentEditable={false} />
208 )}
209 <table
210 className={cn(
211 "mr-0 ml-px table h-px max-w-full table-fixed border-collapse bg-transparent text-(--presentation-text)",
212 isSelectingCells && "selection:bg-transparent",
213 colSizes.length > 0 && colSizes.every((size) => size !== 0)
214 ? "w-fit"
215 : "w-full",
216 )}
217 {...tableProps}
218 >
219 <tbody className="w-full">{children}</tbody>
220 </table>
221 {showTableGrowthControls && (
222 <>
223 <Button
224 aria-label="Add column"
225 className={cn(
226 "z-40 h-full min-h-10 w-5 rounded-md border-border/80 bg-muted/80 p-0 text-muted-foreground shadow-xs backdrop-blur-sm",
227 "transition-colors duration-100 hover:bg-background hover:text-foreground",
228 )}
229 contentEditable={false}
230 onClick={insertColumnAfter}
231 onMouseDown={(event) => event.preventDefault()}
232 size="icon"
233 title="Add column"
234 type="button"
235 variant="outline"
236 >
237 <Plus className="size-3.5" data-ppt-ignore="true" />
238 </Button>
239 <Button
240 aria-label="Add row"
241 className={cn(
242 "z-40 col-span-2 h-5 w-auto rounded-md border-border/80 bg-muted/80 p-0 text-muted-foreground shadow-xs backdrop-blur-sm",
243 "transition-colors duration-100 hover:bg-background hover:text-foreground",
244 )}
245 contentEditable={false}
246 onClick={insertRowAfter}
247 onMouseDown={(event) => event.preventDefault()}
248 size="icon"
249 title="Add row"
250 type="button"
251 variant="outline"
252 >
253 <Plus className="size-3.5" data-ppt-ignore="true" />
254 </Button>
255 </>
256 )}
257 </div>
258 </PlateElement>
259 );
260
261 if (readOnly || !selected) {
262 return content;
263 }
264
265 return <TableFloatingToolbar>{content}</TableFloatingToolbar>;
266 },
267 );
268
269 function TableFloatingToolbar({
270 children,
271 ...props
272 }: React.ComponentProps<typeof PopoverContent>) {
273 const { tf } = useEditorPlugin(TablePlugin);
274 const element = useElement<TTableElement>();
275 const { props: buttonProps } = useRemoveNodeButton({ element });
276 const collapsed = useEditorSelector((editor) => !editor.api.isExpanded(), []);
277
278 const { canMerge, canSplit } = useTableMergeState();
279
280 return (
281 <Popover open={canMerge || canSplit || collapsed} modal={false}>
282 <PopoverAnchor asChild>{children}</PopoverAnchor>
283 <PopoverContent
284 asChild
285 onOpenAutoFocus={(e) => e.preventDefault()}
286 contentEditable={false}
287 {...props}
288 >
289 <Toolbar
290 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"
291 contentEditable={false}
292 >
293 <ToolbarGroup>
294 <ColorDropdownMenu tooltip="Background color">
295 <PaintBucketIcon />
296 </ColorDropdownMenu>
297 {canMerge && (
298 <ToolbarButton
299 onClick={() => tf.table.merge()}
300 onMouseDown={(e) => e.preventDefault()}
301 tooltip="Merge cells"
302 >
303 <CombineIcon />
304 </ToolbarButton>
305 )}
306 {canSplit && (
307 <ToolbarButton
308 onClick={() => tf.table.split()}
309 onMouseDown={(e) => e.preventDefault()}
310 tooltip="Split cell"
311 >
312 <SquareSplitHorizontalIcon />
313 </ToolbarButton>
314 )}
315
316 <DropdownMenu modal={false}>
317 <DropdownMenuTrigger asChild>
318 <ToolbarButton tooltip="Cell borders">
319 <Grid2X2Icon />
320 </ToolbarButton>
321 </DropdownMenuTrigger>
322
323 <DropdownMenuPortal>
324 <TableBordersDropdownMenuContent />
325 </DropdownMenuPortal>
326 </DropdownMenu>
327
328 {collapsed && (
329 <ToolbarGroup>
330 <ToolbarButton tooltip="Delete table" {...buttonProps}>
331 <Trash2Icon />
332 </ToolbarButton>
333 </ToolbarGroup>
334 )}
335 </ToolbarGroup>
336
337 {collapsed && (
338 <ToolbarGroup>
339 <ToolbarButton
340 onClick={() => {
341 tf.insert.tableRow({ before: true });
342 }}
343 onMouseDown={(e) => e.preventDefault()}
344 tooltip="Insert row before"
345 >
346 <ArrowUp />
347 </ToolbarButton>
348 <ToolbarButton
349 onClick={() => {
350 tf.insert.tableRow();
351 }}
352 onMouseDown={(e) => e.preventDefault()}
353 tooltip="Insert row after"
354 >
355 <ArrowDown />
356 </ToolbarButton>
357 <ToolbarButton
358 onClick={() => {
359 tf.remove.tableRow();
360 }}
361 onMouseDown={(e) => e.preventDefault()}
362 tooltip="Delete row"
363 >
364 <XIcon />
365 </ToolbarButton>
366 </ToolbarGroup>
367 )}
368
369 {collapsed && (
370 <ToolbarGroup>
371 <ToolbarButton
372 onClick={() => {
373 tf.insert.tableColumn({ before: true });
374 }}
375 onMouseDown={(e) => e.preventDefault()}
376 tooltip="Insert column before"
377 >
378 <ArrowLeft />
379 </ToolbarButton>
380 <ToolbarButton
381 onClick={() => {
382 tf.insert.tableColumn();
383 }}
384 onMouseDown={(e) => e.preventDefault()}
385 tooltip="Insert column after"
386 >
387 <ArrowRight />
388 </ToolbarButton>
389 <ToolbarButton
390 onClick={() => {
391 tf.remove.tableColumn();
392 }}
393 onMouseDown={(e) => e.preventDefault()}
394 tooltip="Delete column"
395 >
396 <XIcon />
397 </ToolbarButton>
398 </ToolbarGroup>
399 )}
400 </Toolbar>
401 </PopoverContent>
402 </Popover>
403 );
404 }
405
406 function TableBordersDropdownMenuContent(
407 props: React.ComponentProps<typeof DropdownMenuPrimitive.Content>,
408 ) {
409 const editor = useEditorRef();
410 const {
411 getOnSelectTableBorder,
412 hasBottomBorder,
413 hasLeftBorder,
414 hasNoBorders,
415 hasOuterBorders,
416 hasRightBorder,
417 hasTopBorder,
418 } = useTableBordersDropdownMenuContentState();
419
420 return (
421 <DropdownMenuContent
422 className="min-w-55"
423 onCloseAutoFocus={(e) => {
424 e.preventDefault();
425 editor.tf.focus();
426 }}
427 align="start"
428 side="right"
429 sideOffset={0}
430 {...props}
431 >
432 <DropdownMenuGroup>
433 <DropdownMenuCheckboxItem
434 checked={hasTopBorder}
435 onCheckedChange={getOnSelectTableBorder("top")}
436 >
437 <BorderTopIcon />
438 <div>Top Border</div>
439 </DropdownMenuCheckboxItem>
440 <DropdownMenuCheckboxItem
441 checked={hasRightBorder}
442 onCheckedChange={getOnSelectTableBorder("right")}
443 >
444 <BorderRightIcon />
445 <div>Right Border</div>
446 </DropdownMenuCheckboxItem>
447 <DropdownMenuCheckboxItem
448 checked={hasBottomBorder}
449 onCheckedChange={getOnSelectTableBorder("bottom")}
450 >
451 <BorderBottomIcon />
452 <div>Bottom Border</div>
453 </DropdownMenuCheckboxItem>
454 <DropdownMenuCheckboxItem
455 checked={hasLeftBorder}
456 onCheckedChange={getOnSelectTableBorder("left")}
457 >
458 <BorderLeftIcon />
459 <div>Left Border</div>
460 </DropdownMenuCheckboxItem>
461 </DropdownMenuGroup>
462
463 <DropdownMenuGroup>
464 <DropdownMenuCheckboxItem
465 checked={hasNoBorders}
466 onCheckedChange={getOnSelectTableBorder("none")}
467 >
468 <BorderNoneIcon />
469 <div>No Border</div>
470 </DropdownMenuCheckboxItem>
471 <DropdownMenuCheckboxItem
472 checked={hasOuterBorders}
473 onCheckedChange={getOnSelectTableBorder("outer")}
474 >
475 <BorderAllIcon />
476 <div>Outside Borders</div>
477 </DropdownMenuCheckboxItem>
478 </DropdownMenuGroup>
479 </DropdownMenuContent>
480 );
481 }
482
483 function ColorDropdownMenu({
484 children,
485 tooltip,
486 }: {
487 children: React.ReactNode;
488 tooltip: string;
489 }) {
490 const [open, setOpen] = React.useState(false);
491
492 const editor = useEditorRef();
493 const selectedCells = usePluginOption(TablePlugin, "selectedCells");
494
495 const onUpdateColor = React.useCallback(
496 (color: string) => {
497 setOpen(false);
498 setCellBackground(editor, { color, selectedCells: selectedCells ?? [] });
499 },
500 [selectedCells, editor],
501 );
502
503 const onClearColor = React.useCallback(() => {
504 setOpen(false);
505 setCellBackground(editor, {
506 color: null,
507 selectedCells: selectedCells ?? [],
508 });
509 }, [selectedCells, editor]);
510
511 return (
512 <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
513 <DropdownMenuTrigger asChild>
514 <ToolbarButton tooltip={tooltip}>{children}</ToolbarButton>
515 </DropdownMenuTrigger>
516
517 <DropdownMenuContent align="start">
518 <ToolbarMenuGroup label="Colors">
519 <ColorDropdownMenuItems
520 className="px-2"
521 colors={DEFAULT_COLORS}
522 updateColor={onUpdateColor}
523 />
524 </ToolbarMenuGroup>
525 <DropdownMenuGroup>
526 <DropdownMenuItem className="p-2" onClick={onClearColor}>
527 <EraserIcon />
528 <span>Clear</span>
529 </DropdownMenuItem>
530 </DropdownMenuGroup>
531 </DropdownMenuContent>
532 </DropdownMenu>
533 );
534 }
535
536 export function TableRowElement(props: PlateElementProps<TTableRowElement>) {
537 const { element } = props;
538 const readOnly = useReadOnly();
539 const editor = useEditorRef();
540 const isSelectionAreaVisible = usePluginOption(
541 BlockSelectionPlugin,
542 "isSelectionAreaVisible",
543 );
544 const hasControls = !readOnly && !isSelectionAreaVisible;
545
546 const { isDragging, previewRef, handleRef } = useDraggable({
547 element,
548 type: element.type,
549 canDropNode: ({ dragEntry, dropEntry }) =>
550 PathApi.equals(
551 PathApi.parent(dragEntry[1]),
552 PathApi.parent(dropEntry[1]),
553 ),
554 onDropHandler: (_, { dragItem }) => {
555 const dragElement =
556 "element" in dragItem && ElementApi.isElement(dragItem.element)
557 ? dragItem.element
558 : undefined;
559
560 if (dragElement) {
561 editor.tf.select(dragElement);
562 }
563 },
564 });
565 const rowRef = React.useCallback(
566 (node: HTMLTableRowElement | null) => {
567 setRefValue<HTMLElement>(props.ref, node);
568 setRefValue<HTMLElement>(previewRef, node);
569 },
570 [previewRef, props.ref],
571 );
572
573 return (
574 <PlateElement
575 {...props}
576 ref={rowRef}
577 as="tr"
578 className={cn("group/row", isDragging && "opacity-50")}
579 >
580 {hasControls && (
581 <td className="w-2 select-none" contentEditable={false}>
582 <RowDragHandle dragRef={handleRef} />
583 <RowDropLine />
584 </td>
585 )}
586
587 {props.children}
588 </PlateElement>
589 );
590 }
591
592 function RowDragHandle({ dragRef }: { dragRef: React.Ref<HTMLButtonElement> }) {
593 const editor = useEditorRef();
594 const element = useElement();
595
596 return (
597 <button
598 ref={dragRef}
599 type="button"
600 className={cn(
601 "absolute top-1/2 z-51 w-4 -translate-y-1/2 p-0.5 focus-visible:ring-0 focus-visible:ring-offset-0",
602 "cursor-grab active:cursor-grabbing",
603 'flex items-center rounded bg-accent opacity-0 outline transition-opacity duration-100 group-hover/row:opacity-100 group-has-data-[resizing="true"]/row:opacity-0',
604 )}
605 onClick={() => {
606 editor.tf.select(element);
607 }}
608 >
609 <GripVertical
610 className="size-3.5 text-muted-foreground"
611 data-ppt-ignore="true"
612 />
613 </button>
614 );
615 }
616
617 function RowDropLine() {
618 const { dropLine } = useDropLine();
619
620 if (!dropLine) return null;
621
622 return (
623 <div
624 className={cn(
625 "absolute inset-x-0 left-2 z-50 h-0.5 rounded-full bg-brand/50",
626 dropLine === "top" ? "-top-px" : "-bottom-px",
627 )}
628 />
629 );
630 }
631
632 export function TableCellElement({
633 isHeader,
634 ...props
635 }: PlateElementProps<TTableCellElement> & {
636 isHeader?: boolean;
637 }) {
638 const { api } = useEditorPlugin(TablePlugin);
639 const readOnly = useReadOnly();
640 const element = props.element;
641
642 const rowId = useElementSelector(([node]) => node.id as string, [], {
643 key: KEYS.tr,
644 });
645 const isSelectingRow = useBlockSelected(rowId);
646 const isSelectionAreaVisible = usePluginOption(
647 BlockSelectionPlugin,
648 "isSelectionAreaVisible",
649 );
650 const selectedCellIds = usePluginOption(TablePlugin, "selectedCellIds");
651
652 const { borders, colIndex, colSpan, minHeight, rowIndex, selected, width } =
653 useTableCellElement();
654 const isCellSelected =
655 selected ||
656 (typeof element.id === "string" &&
657 (selectedCellIds?.includes(element.id) ?? false));
658
659 const { bottomProps, hiddenLeft, leftProps, rightProps } =
660 useTableCellElementResizable({
661 colIndex,
662 colSpan,
663 rowIndex,
664 });
665
666 return (
667 <PlateElement
668 {...props}
669 as={isHeader ? "th" : "td"}
670 className={cn(
671 "relative h-full overflow-visible border-none bg-transparent p-0",
672 isHeader && "text-left *:m-0",
673 "before:inset-0 before:z-10 before:size-full",
674 (isCellSelected || isSelectingRow) && "before:bg-brand/20",
675 "before:absolute before:box-border before:content-[''] before:select-none",
676 borders.bottom?.size && `before:border-b before:border-b-border`,
677 borders.right?.size && `before:border-r before:border-r-border`,
678 borders.left?.size && `before:border-l before:border-l-border`,
679 borders.top?.size && `before:border-t before:border-t-border`,
680 )}
681 style={
682 {
683 "--cellBackground": element.background,
684 maxWidth: width || 240,
685 width: width || undefined,
686 minWidth: width || 120,
687 backgroundColor:
688 element.background ??
689 (isHeader ? "var(--presentation-card-background)" : undefined),
690 } as React.CSSProperties
691 }
692 attributes={{
693 ...props.attributes,
694 colSpan: api.table.getColSpan(element),
695 rowSpan: api.table.getRowSpan(element),
696 }}
697 >
698 {(isCellSelected || isSelectingRow) && (
699 <div
700 className="pointer-events-none absolute inset-0 z-10 bg-brand/20"
701 contentEditable={false}
702 />
703 )}
704
705 <div
706 className={cn(
707 "relative z-20 box-border h-full rounded-md px-3 py-2",
708 isHeader ? "text-lg font-bold text-primary" : "presentation-text",
709 )}
710 style={{ minHeight }}
711 >
712 {props.children}
713 </div>
714
715 {!isSelectionAreaVisible && (
716 <div
717 className="group absolute top-0 size-full select-none"
718 contentEditable={false}
719 suppressContentEditableWarning={true}
720 >
721 {!readOnly && (
722 <>
723 <ResizeHandle
724 {...rightProps}
725 className="-top-2 -right-1 h-[calc(100%+8px)] w-2"
726 data-col={colIndex}
727 />
728 <ResizeHandle {...bottomProps} className="-bottom-1 h-2" />
729 {!hiddenLeft && (
730 <ResizeHandle
731 {...leftProps}
732 className="top-0 -left-1 w-2"
733 data-resizer-left={colIndex === 0 ? "true" : undefined}
734 />
735 )}
736
737 <div
738 className={cn(
739 "absolute top-0 z-30 hidden h-full w-1 bg-ring",
740 "right-[-1.5px]",
741 getColumnResizeClass(colIndex),
742 )}
743 />
744 {colIndex === 0 && (
745 <div
746 className={cn(
747 "absolute top-0 z-30 h-full w-1 bg-ring",
748 "left-[-1.5px]",
749 'hidden animate-in fade-in group-has-[[data-resizer-left]:hover]/table:block group-has-[[data-resizer-left][data-resizing="true"]]/table:block',
750 )}
751 />
752 )}
753 </>
754 )}
755 </div>
756 )}
757
758 {isSelectingRow && (
759 <div className={blockSelectionVariants()} contentEditable={false} />
760 )}
761 </PlateElement>
762 );
763 }
764
765 export function TableCellHeaderElement(
766 props: React.ComponentProps<typeof TableCellElement>,
767 ) {
768 return <TableCellElement {...props} isHeader />;
769 }
770
771 const COLUMN_RESIZE_CLASSES: Record<number, string> = {
772 0: 'group-has-[[data-col="0"]:hover]/table:block group-has-[[data-col="0"][data-resizing="true"]]/table:block',
773 1: 'group-has-[[data-col="1"]:hover]/table:block group-has-[[data-col="1"][data-resizing="true"]]/table:block',
774 2: 'group-has-[[data-col="2"]:hover]/table:block group-has-[[data-col="2"][data-resizing="true"]]/table:block',
775 3: 'group-has-[[data-col="3"]:hover]/table:block group-has-[[data-col="3"][data-resizing="true"]]/table:block',
776 4: 'group-has-[[data-col="4"]:hover]/table:block group-has-[[data-col="4"][data-resizing="true"]]/table:block',
777 5: 'group-has-[[data-col="5"]:hover]/table:block group-has-[[data-col="5"][data-resizing="true"]]/table:block',
778 6: 'group-has-[[data-col="6"]:hover]/table:block group-has-[[data-col="6"][data-resizing="true"]]/table:block',
779 7: 'group-has-[[data-col="7"]:hover]/table:block group-has-[[data-col="7"][data-resizing="true"]]/table:block',
780 8: 'group-has-[[data-col="8"]:hover]/table:block group-has-[[data-col="8"][data-resizing="true"]]/table:block',
781 9: 'group-has-[[data-col="9"]:hover]/table:block group-has-[[data-col="9"][data-resizing="true"]]/table:block',
782 10: 'group-has-[[data-col="10"]:hover]/table:block group-has-[[data-col="10"][data-resizing="true"]]/table:block',
783 };
784
785 function getColumnResizeClass(colIndex: number) {
786 return COLUMN_RESIZE_CLASSES[colIndex] ?? "";
787 }
788
788 lines Plain Text