From cea9be7926c2eaa7ed68928454d30492cd8c1972 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Thu, 14 May 2026 00:37:44 +0100 Subject: [PATCH] feat: table enhancement (#2191) --- apps/client/package.json | 126 +-- .../public/locales/en-US/translation.json | 18 +- .../fixed-toolbar/fixed-toolbar.module.css | 2 +- .../fixed-toolbar/fixed-toolbar.tsx | 1 + .../components/table/handle/cell-chevron.tsx | 126 +++ .../components/table/handle/column-handle.tsx | 127 +++ .../components/table/handle/handle.module.css | 108 ++ .../hooks/use-column-row-menu-lifecycle.ts | 40 + .../table/handle/hooks/use-table-clear.ts | 54 + .../handle/hooks/use-table-handle-drag.ts | 79 ++ .../handle/hooks/use-table-handle-state.ts | 23 + .../handle/hooks/use-table-move-row-column.ts | 50 + .../table/handle/hooks/use-table-sort.ts | 100 ++ .../table/handle/lib/select-row-column.ts | 34 + .../components/table/handle/lib/sort-cells.ts | 57 + .../table/handle/menus/alignment-submenu.tsx | 49 + .../table/handle/menus/cell-chevron-menu.tsx | 154 +++ .../table/handle/menus/column-handle-menu.tsx | 177 ++++ .../table/handle/menus/row-handle-menu.tsx | 138 +++ .../components/table/handle/row-handle.tsx | 122 +++ .../table/handle/table-handles-layer.tsx | 44 + .../table/table-background-color.tsx | 2 +- .../editor/components/table/table-menu.tsx | 4 +- .../components/table/table-text-alignment.tsx | 4 +- .../features/editor/extensions/drag-handle.ts | 118 ++- .../features/editor/extensions/extensions.ts | 9 + .../src/features/editor/page-editor.tsx | 3 +- .../src/features/editor/styles/core.css | 3 +- .../src/features/editor/styles/table.css | 143 ++- .../page/components/header/page-header.tsx | 2 +- .../lib/table/dnd/auto-scroll-controller.ts | 76 -- .../src/lib/table/dnd/dnd-extension.ts | 531 ++++++---- .../dnd/handle/drag-handle-controller.ts | 105 -- .../dnd/handle/empty-image-controller.ts | 21 - .../editor-ext/src/lib/table/dnd/index.ts | 8 +- .../dnd/preview/drop-indicator-controller.ts | 2 +- .../table/dnd/preview/preview-controller.ts | 22 +- .../editor-ext/src/lib/table/dnd/utils.ts | 34 +- .../src/lib/table/header-pin/controller.ts | 186 ++++ .../src/lib/table/header-pin/extension.ts | 78 ++ .../src/lib/table/header-pin/index.ts | 1 + .../src/lib/table/header-pin/offset.ts | 65 ++ packages/editor-ext/src/lib/table/index.ts | 12 +- .../src/lib/table/table-readonly-sort.ts | 233 +++++ .../editor-ext/src/lib/table/table-view.ts | 158 +++ packages/editor-ext/src/lib/table/table.ts | 29 + .../src/lib/table/utils/col-style.ts | 7 + pnpm-lock.yaml | 978 +++++++----------- 48 files changed, 3330 insertions(+), 1133 deletions(-) create mode 100644 apps/client/src/features/editor/components/table/handle/cell-chevron.tsx create mode 100644 apps/client/src/features/editor/components/table/handle/column-handle.tsx create mode 100644 apps/client/src/features/editor/components/table/handle/handle.module.css create mode 100644 apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts create mode 100644 apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts create mode 100644 apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-drag.ts create mode 100644 apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-state.ts create mode 100644 apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts create mode 100644 apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts create mode 100644 apps/client/src/features/editor/components/table/handle/lib/select-row-column.ts create mode 100644 apps/client/src/features/editor/components/table/handle/lib/sort-cells.ts create mode 100644 apps/client/src/features/editor/components/table/handle/menus/alignment-submenu.tsx create mode 100644 apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx create mode 100644 apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx create mode 100644 apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx create mode 100644 apps/client/src/features/editor/components/table/handle/row-handle.tsx create mode 100644 apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx delete mode 100644 packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts delete mode 100644 packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts delete mode 100644 packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts create mode 100644 packages/editor-ext/src/lib/table/header-pin/controller.ts create mode 100644 packages/editor-ext/src/lib/table/header-pin/extension.ts create mode 100644 packages/editor-ext/src/lib/table/header-pin/index.ts create mode 100644 packages/editor-ext/src/lib/table/header-pin/offset.ts create mode 100644 packages/editor-ext/src/lib/table/table-readonly-sort.ts create mode 100644 packages/editor-ext/src/lib/table/table-view.ts create mode 100644 packages/editor-ext/src/lib/table/utils/col-style.ts diff --git a/apps/client/package.json b/apps/client/package.json index 854c9f95d..c769f6090 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -12,84 +12,84 @@ "test:watch": "vitest" }, "dependencies": { - "@atlaskit/pragmatic-drag-and-drop": "^1.8.1", - "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0", - "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.15", - "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", - "@atlaskit/pragmatic-drag-and-drop-live-region": "^1.3.4", - "@casl/react": "^5.0.1", + "@atlaskit/pragmatic-drag-and-drop": "1.8.1", + "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.5", + "@atlaskit/pragmatic-drag-and-drop-flourish": "2.0.15", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0", + "@atlaskit/pragmatic-drag-and-drop-live-region": "1.3.4", + "@casl/react": "5.0.1", "@docmost/editor-ext": "workspace:*", "@excalidraw/excalidraw": "0.18.0-3a5ef40", - "@mantine/core": "^8.3.18", - "@mantine/dates": "^8.3.18", - "@mantine/form": "^8.3.18", - "@mantine/hooks": "^8.3.18", - "@mantine/modals": "^8.3.18", - "@mantine/notifications": "^8.3.18", - "@mantine/spotlight": "^8.3.18", - "@slidoapp/emoji-mart": "^5.8.7", - "@slidoapp/emoji-mart-data": "^1.2.4", - "@slidoapp/emoji-mart-react": "^1.1.5", - "@tabler/icons-react": "^3.40.0", + "@mantine/core": "8.3.18", + "@mantine/dates": "8.3.18", + "@mantine/form": "8.3.18", + "@mantine/hooks": "8.3.18", + "@mantine/modals": "8.3.18", + "@mantine/notifications": "8.3.18", + "@mantine/spotlight": "8.3.18", + "@slidoapp/emoji-mart": "5.8.7", + "@slidoapp/emoji-mart-data": "1.2.4", + "@slidoapp/emoji-mart-react": "1.1.5", + "@tabler/icons-react": "3.40.0", "@tanstack/react-query": "5.90.17", "@tanstack/react-virtual": "3.13.24", - "alfaaz": "^1.1.0", + "alfaaz": "1.1.0", "axios": "1.16.0", - "blueimp-load-image": "^5.16.0", - "clsx": "^2.1.1", - "file-saver": "^2.0.5", - "highlightjs-sap-abap": "^0.3.0", + "blueimp-load-image": "5.16.0", + "clsx": "2.1.1", + "file-saver": "2.0.5", + "highlightjs-sap-abap": "0.3.0", "i18next": "25.10.1", "i18next-http-backend": "3.0.6", - "jotai": "^2.18.1", - "jotai-optics": "^0.4.0", - "js-cookie": "^3.0.5", - "jwt-decode": "^4.0.0", + "jotai": "2.18.1", + "jotai-optics": "0.4.0", + "js-cookie": "3.0.5", + "jwt-decode": "4.0.0", "katex": "0.16.40", - "lowlight": "^3.3.0", - "mantine-form-zod-resolver": "^1.3.0", - "mermaid": "^11.13.0", - "mitt": "^3.0.1", + "lowlight": "3.3.0", + "mantine-form-zod-resolver": "1.3.0", + "mermaid": "11.15.0", + "mitt": "3.0.1", "posthog-js": "1.372.2", - "react": "^18.3.1", + "react": "18.3.1", "react-clear-modal": "^2.0.18", "react-dom": "^18.3.1", - "react-drawio": "^1.0.7", - "react-error-boundary": "^6.1.1", - "react-helmet-async": "^3.0.0", + "react-drawio": "1.0.7", + "react-error-boundary": "6.1.1", + "react-helmet-async": "3.0.0", "react-i18next": "16.5.8", - "react-router-dom": "^7.13.1", - "semver": "^7.7.4", - "socket.io-client": "^4.8.3", - "zod": "^4.3.6" + "react-router-dom": "7.13.1", + "semver": "7.7.4", + "socket.io-client": "4.8.3", + "zod": "4.3.6" }, "devDependencies": { - "@eslint/js": "^9.28.0", - "@tanstack/eslint-plugin-query": "^5.94.4", - "@testing-library/jest-dom": "^6.6.0", - "@testing-library/react": "^16.1.0", - "@types/blueimp-load-image": "^5.16.6", - "@types/file-saver": "^2.0.7", - "@types/js-cookie": "^3.0.6", - "@types/katex": "^0.16.8", + "@eslint/js": "9.28.0", + "@tanstack/eslint-plugin-query": "5.94.4", + "@testing-library/jest-dom": "6.6.0", + "@testing-library/react": "16.1.0", + "@types/blueimp-load-image": "5.16.6", + "@types/file-saver": "2.0.7", + "@types/js-cookie": "3.0.6", + "@types/katex": "0.16.8", "@types/node": "22.19.1", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^9.28.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^15.13.0", - "jsdom": "^25.0.0", - "optics-ts": "^2.4.1", - "postcss": "^8.5.12", - "postcss-preset-mantine": "^1.18.0", - "postcss-simple-vars": "^7.0.1", - "prettier": "^3.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.1", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "6.0.1", + "eslint": "9.28.0", + "eslint-plugin-react": "7.37.5", + "eslint-plugin-react-hooks": "7.0.1", + "eslint-plugin-react-refresh": "0.5.2", + "globals": "15.13.0", + "jsdom": "25.0.0", + "optics-ts": "2.4.1", + "postcss": "8.5.14", + "postcss-preset-mantine": "1.18.0", + "postcss-simple-vars": "7.0.1", + "prettier": "3.8.1", + "typescript": "5.9.3", + "typescript-eslint": "8.57.1", "vite": "8.0.5", - "vitest": "^4.1.6" + "vitest": "4.1.6" } } diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 3f357b258..268d696c8 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -286,6 +286,19 @@ "Add row above": "Add row above", "Add row below": "Add row below", "Delete table": "Delete table", + "Add column left": "Add column left", + "Add column right": "Add column right", + "Clear cell": "Clear cell", + "Clear cells": "Clear cells", + "Toggle header cell": "Toggle header cell", + "Toggle header column": "Toggle header column", + "Toggle header row": "Toggle header row", + "Move column left": "Move column left", + "Move column right": "Move column right", + "Move row down": "Move row down", + "Move row up": "Move row up", + "Sort A → Z": "Sort A → Z", + "Sort Z → A": "Sort Z → A", "Info": "Info", "Note": "Note", "Success": "Success", @@ -997,5 +1010,8 @@ "No pages with this label": "No pages with this label", "Pages tagged with this label will appear here.": "Pages tagged with this label will appear here.", "No pages match your search.": "No pages match your search.", - "Updated {{date}}": "Updated {{date}}" + "Updated {{date}}": "Updated {{date}}", + "Cell actions": "Cell actions", + "Column actions": "Column actions", + "Row actions": "Row actions" } diff --git a/apps/client/src/features/editor/components/fixed-toolbar/fixed-toolbar.module.css b/apps/client/src/features/editor/components/fixed-toolbar/fixed-toolbar.module.css index f5cf09cbb..ef5595ea1 100644 --- a/apps/client/src/features/editor/components/fixed-toolbar/fixed-toolbar.module.css +++ b/apps/client/src/features/editor/components/fixed-toolbar/fixed-toolbar.module.css @@ -3,7 +3,7 @@ top: calc(var(--app-shell-header-offset, 0rem) + 45px); inset-inline-start: var(--app-shell-navbar-offset, 0rem); inset-inline-end: var(--app-shell-aside-offset, 0rem); - z-index: 50; + z-index: 99; display: flex; align-items: center; background: var(--mantine-color-body); diff --git a/apps/client/src/features/editor/components/fixed-toolbar/fixed-toolbar.tsx b/apps/client/src/features/editor/components/fixed-toolbar/fixed-toolbar.tsx index 2a2135e8c..d72db0c7d 100644 --- a/apps/client/src/features/editor/components/fixed-toolbar/fixed-toolbar.tsx +++ b/apps/client/src/features/editor/components/fixed-toolbar/fixed-toolbar.tsx @@ -28,6 +28,7 @@ export const FixedToolbar: FC = () => { <>
e.preventDefault()} diff --git a/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx b/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx new file mode 100644 index 000000000..db79844e8 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx @@ -0,0 +1,126 @@ +import React, { useCallback, useEffect } from "react"; +import type { Editor } from "@tiptap/react"; +import { useEditorState } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { TextSelection } from "@tiptap/pm/state"; +import { columnResizingPluginKey } from "@tiptap/pm/tables"; +import { useFloating, offset, autoUpdate, hide } from "@floating-ui/react"; +import { Menu, UnstyledButton } from "@mantine/core"; +import { IconChevronDown } from "@tabler/icons-react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { isCellSelection } from "@docmost/editor-ext"; +import { CellChevronMenu } from "./menus/cell-chevron-menu"; +import classes from "./handle.module.css"; + +interface CellChevronProps { + editor: Editor; + cellPos: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const CellChevron = React.memo(function CellChevron({ + editor, + cellPos, + tableNode, + tablePos, +}: CellChevronProps) { + const { t } = useTranslation(); + const cellDom = editor.view.nodeDOM(cellPos) as HTMLElement | null; + + const { refs, floatingStyles, middlewareData } = useFloating({ + placement: "top-end", + // crossAxis pulls the chevron INWARD from the cell's right edge. We need + // enough inset that we don't overlap PM-tables' column-resize hot zone + // (~5px wide around the column boundary). Without this, hovering near the + // column edge picks up the chevron's `cursor: pointer` instead of + // `col-resize`, and a drag near the edge clicks the chevron. + middleware: [offset({ mainAxis: -22, crossAxis: -10 }), hide()], + whileElementsMounted: autoUpdate, + strategy: "absolute", + }); + const isReferenceHidden = !!middlewareData.hide?.referenceHidden; + + useEffect(() => { + refs.setReference(cellDom); + }, [cellDom, refs]); + + // Hide the chevron while the user is resizing a column. PM-tables sets + // `activeHandle > -1` whenever the mouse is near a column boundary OR + // actively dragging it. Either way we don't want the chevron in the way. + const isResizingColumn = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) return false; + const state = columnResizingPluginKey.getState(ctx.editor.state) as + | { activeHandle: number } + | undefined; + return !!state && state.activeHandle > -1; + }, + }); + + const onOpen = useCallback(() => { + const current = editor.state.selection; + + // Preserve an existing multi-cell CellSelection that already covers + // this cell so merge etc. operate on the user's whole range. + let preserveExisting = false; + if (isCellSelection(current)) { + current.forEachCell((_node, pos) => { + if (pos === cellPos) preserveExisting = true; + }); + } + + if (!preserveExisting) { + // Drop a collapsed cursor inside the cell rather than a single-cell + // CellSelection — PM-tables paints the latter as a text-range + // highlight on the cell content. + try { + const $inside = editor.state.doc.resolve(cellPos + 1); + const sel = TextSelection.near($inside, 1); + editor.view.dispatch(editor.state.tr.setSelection(sel)); + } catch {} + } + editor.commands.freezeHandles(); + }, [editor, cellPos]); + + const onClose = useCallback(() => { + editor.commands.unfreezeHandles(); + }, [editor]); + + if (!cellDom) return null; + if (isResizingColumn) return null; + + return ( + + + + + + + + + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/column-handle.tsx b/apps/client/src/features/editor/components/table/handle/column-handle.tsx new file mode 100644 index 000000000..ccc459740 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/column-handle.tsx @@ -0,0 +1,127 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { useFloating, offset, autoUpdate, hide } from "@floating-ui/react"; +import { Menu } from "@mantine/core"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { useTableHandleDrag } from "./hooks/use-table-handle-drag"; +import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle"; +import { ColumnHandleMenu } from "./menus/column-handle-menu"; +import classes from "./handle.module.css"; + +interface ColumnHandleProps { + editor: Editor; + index: number; + anchorPos: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const ColumnHandle = React.memo(function ColumnHandle({ + editor, + index, + anchorPos, + tableNode, + tablePos, +}: ColumnHandleProps) { + const { t } = useTranslation(); + // Hold the cell DOM in a ref-backed state so we never unmount the handle + // mid-drag. A remote edit can transiently flip `nodeDOM(anchorPos)` to null + // (the plugin re-emits `hoveringCell` with the mapped pos a tick later); + // unmounting the source element here would make pragmatic-dnd silently + // abort the active drag. + const lookupCellDom = editor.view.nodeDOM(anchorPos) as HTMLElement | null; + const [cellDom, setCellDom] = useState(lookupCellDom); + const lastCellDomRef = useRef(lookupCellDom); + useEffect(() => { + if (lookupCellDom && lookupCellDom !== lastCellDomRef.current) { + lastCellDomRef.current = lookupCellDom; + setCellDom(lookupCellDom); + } + }, [lookupCellDom]); + + const [handleEl, setHandleEl] = useState(null); + + const { refs, floatingStyles, middlewareData } = useFloating({ + placement: "top", + middleware: [offset(-4), hide()], + whileElementsMounted: autoUpdate, + }); + const isReferenceHidden = !!middlewareData.hide?.referenceHidden; + + useEffect(() => { + refs.setReference(cellDom); + }, [cellDom, refs]); + + // `cellDom` is inside the table, so `closest('.tableWrapper')` finds the + // wrapper for this drag's auto-scroll. The handle itself lives in a + // floating layer outside the editor DOM, so we can't walk up from it. + const wrapper = cellDom?.closest(".tableWrapper") ?? null; + + const [menuOpened, setMenuOpened] = useState(false); + const closeMenu = useCallback(() => setMenuOpened(false), []); + useTableHandleDrag(editor, "col", handleEl, wrapper, closeMenu); + + const { onOpen, onClose } = useColumnRowMenuLifecycle({ + editor, + orientation: "col", + index, + tableNode, + tablePos, + }); + + if (!cellDom) return null; + + return ( + + +
{ + refs.setFloating(node); + setHandleEl(node); + }} + style={{ + ...floatingStyles, + ...(isReferenceHidden ? { visibility: "hidden" as const } : {}), + }} + className={clsx(classes.handle, classes.columnHandle)} + role="button" + tabIndex={0} + aria-label={t("Column actions")} + > + + + +
+
+ + + +
+ ); +}); + +function GripIcon() { + return ( + + + + ); +} diff --git a/apps/client/src/features/editor/components/table/handle/handle.module.css b/apps/client/src/features/editor/components/table/handle/handle.module.css new file mode 100644 index 000000000..e7d9ac124 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/handle.module.css @@ -0,0 +1,108 @@ +.handle { + position: absolute; + z-index: 50; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + color: rgba(55, 53, 47, 0.45); + background: var(--mantine-color-body); + border: 1px solid rgba(55, 53, 47, 0.12); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + cursor: grab; + padding: 0; + transition: background-color 120ms ease, color 120ms ease; + + @mixin dark { + color: rgba(255, 255, 255, 0.55); + background: var(--mantine-color-dark-7); + border-color: rgba(255, 255, 255, 0.12); + } +} + +.handle:hover { + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); + color: light-dark( + var(--mantine-color-gray-7), + var(--mantine-color-dark-0) + ); +} + +.handle:active { + cursor: grabbing; +} + +.columnHandle { + width: 28px; + height: 16px; +} + +.columnHandle svg { + transform: rotate(90deg); +} + +.rowHandle { + width: 16px; + height: 28px; +} + +@media (max-width: 600px) { + .handle { + display: none; + } +} + +.cellChevron { + position: absolute; + z-index: 50; + width: 18px; + height: 18px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + color: light-dark( + var(--mantine-color-gray-7), + var(--mantine-color-dark-1) + ); + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); + border: 1px solid rgba(55, 53, 47, 0.12); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + cursor: pointer; + padding: 0; + transition: background-color 120ms ease, color 120ms ease; + + @mixin dark { + border-color: rgba(255, 255, 255, 0.12); + } +} + +.cellChevron:hover { + background: light-dark( + var(--mantine-color-gray-2), + var(--mantine-color-dark-4) + ); + color: light-dark( + var(--mantine-color-gray-8), + var(--mantine-color-dark-0) + ); +} + +@media (max-width: 600px) { + .cellChevron { + display: none; + } +} + +@media print { + .handle, + .cellChevron { + display: none !important; + } +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts new file mode 100644 index 000000000..a30595597 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts @@ -0,0 +1,40 @@ +import { useCallback } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { buildRowOrColumnSelection, Orientation } from "../lib/select-row-column"; + +interface Args { + editor: Editor; + orientation: Orientation; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export function useColumnRowMenuLifecycle({ + editor, + orientation, + index, + tableNode, + tablePos, +}: Args) { + const onOpen = useCallback(() => { + const selection = buildRowOrColumnSelection( + editor.state, + tableNode, + tablePos, + orientation, + index, + ); + const tr = editor.state.tr; + if (selection) tr.setSelection(selection); + editor.view.dispatch(tr); + editor.commands.freezeHandles(); + }, [editor, orientation, index, tableNode, tablePos]); + + const onClose = useCallback(() => { + editor.commands.unfreezeHandles(); + }, [editor]); + + return { onOpen, onClose }; +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts new file mode 100644 index 000000000..1bd4cb209 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts @@ -0,0 +1,54 @@ +import { useCallback } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { TableMap } from "@tiptap/pm/tables"; + +type Scope = + | { kind: "col"; index: number } + | { kind: "row"; index: number } + | { kind: "cell"; cellPos: number }; + +export function useTableClear( + editor: Editor, + tableNode: ProseMirrorNode, + tablePos: number, + scope: Scope, +) { + return useCallback(() => { + const tr = editor.state.tr; + const tableStart = tablePos + 1; + const map = TableMap.get(tableNode); + const paragraph = editor.schema.nodes.paragraph; + if (!paragraph) return; + + const cellOffsets: number[] = []; + + if (scope.kind === "col") { + for (let row = 0; row < map.height; row++) { + cellOffsets.push(map.map[row * map.width + scope.index]); + } + } else if (scope.kind === "row") { + for (let col = 0; col < map.width; col++) { + cellOffsets.push(map.map[scope.index * map.width + col]); + } + } + + const targets = + scope.kind === "cell" + ? [scope.cellPos] + : Array.from(new Set(cellOffsets)).map((o) => tableStart + o); + + // Process in reverse position order so earlier replacements don't shift later ones. + targets.sort((a, b) => b - a); + + for (const cellPos of targets) { + const node = tr.doc.nodeAt(cellPos); + if (!node) continue; + const start = cellPos + 1; + const end = cellPos + node.nodeSize - 1; + tr.replaceWith(start, end, paragraph.create()); + } + + if (tr.docChanged) editor.view.dispatch(tr); + }, [editor, tableNode, tablePos, scope]); +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-drag.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-drag.ts new file mode 100644 index 000000000..30b179689 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-drag.ts @@ -0,0 +1,79 @@ +import { useEffect } from "react"; +import type { Editor } from "@tiptap/react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { disableNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview"; +import { + autoScrollForElements, + autoScrollWindowForElements, +} from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; +import { getTableHandlePluginSpec } from "@docmost/editor-ext"; + +// Uses pragmatic-drag-and-drop instead of native HTML5 DnD because the native +// dragstart→dragover→drop lifecycle was being silently cancelled +export function useTableHandleDrag( + editor: Editor, + orientation: "col" | "row", + element: HTMLElement | null, + wrapper: HTMLElement | null, + onDragStart?: () => void, +) { + useEffect(() => { + if (!element) return; + + return combine( + draggable({ + element, + getInitialData: () => ({ type: `table-${orientation}` }), + onGenerateDragPreview: ({ nativeSetDragImage }) => { + // We render our own floating preview via PreviewController, so hide + // the native drag image entirely. + disableNativeDragPreview({ nativeSetDragImage }); + }, + onDragStart: ({ location }) => { + // The menu (if open from a prior click on the handle) won't dismiss + // on its own — pragmatic-dnd swallows the events Mantine listens for. + onDragStart?.(); + const spec = getTableHandlePluginSpec(editor); + if (!spec) return; + const { clientX, clientY } = location.initial.input; + spec.startDragFromHandle(orientation, clientX, clientY); + }, + onDrag: ({ location }) => { + const spec = getTableHandlePluginSpec(editor); + if (!spec) return; + const { clientX, clientY } = location.current.input; + spec.updateDragPosition(clientX, clientY); + }, + onDrop: ({ location }) => { + const spec = getTableHandlePluginSpec(editor); + if (!spec) return; + const { clientX, clientY } = location.current.input; + // Make sure the final position is recorded before committing the drop. + spec.updateDragPosition(clientX, clientY); + spec.commitDrop(); + spec.endDrag(); + }, + }), + // Wrapper owns horizontal auto-scroll (it has `overflow-x: auto`); + // window owns vertical. Locking each axis prevents the window's + // horizontal auto-scroll from running when the cursor approaches + // the viewport edge — without the cap, the preview's `left` follows + // the cursor past the viewport, the page widens to contain it, the + // plugin scrolls the now-wider page further, and the loop never + // ends. + // Only the column handle registers wrapper auto-scroll (rows can't + // scroll horizontally) — registering twice on the same wrapper + // triggers a dev-mode warning from pragmatic-dnd-auto-scroll. + orientation === "col" && + wrapper && + !wrapper.classList.contains("tableWrapperNoOverflow") + ? autoScrollForElements({ + element: wrapper, + getAllowedAxis: () => "horizontal", + }) + : () => {}, + autoScrollWindowForElements({ getAllowedAxis: () => "vertical" }), + ); + }, [editor, orientation, element, wrapper, onDragStart]); +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-state.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-state.ts new file mode 100644 index 000000000..ab8893566 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-state.ts @@ -0,0 +1,23 @@ +import type { Editor } from "@tiptap/react"; +import { useEditorState } from "@tiptap/react"; +import { TableDndKey, TableHandleState } from "@docmost/editor-ext"; + +const FALLBACK: TableHandleState = { + hoveringCell: null, + tableNode: null, + tablePos: null, + dragging: null, + frozen: false, +}; + +export function useTableHandleState(editor: Editor | null): TableHandleState { + const state = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) return null; + return TableDndKey.getState(ctx.editor.state) ?? null; + }, + }); + + return state ?? FALLBACK; +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts new file mode 100644 index 000000000..476c68f8d --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts @@ -0,0 +1,50 @@ +import { useCallback, useMemo } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { TableMap } from "@tiptap/pm/tables"; +import { moveColumn, moveRow } from "@docmost/editor-ext"; + +export type MoveDirection = "left" | "right" | "up" | "down"; + +export function useTableMoveRowColumn( + editor: Editor, + orientation: "col" | "row", + index: number, + direction: MoveDirection, + tableNode: ProseMirrorNode, + tablePos: number, +) { + const target = + direction === "left" || direction === "up" ? index - 1 : index + 1; + + const maxIndex = useMemo(() => { + const map = TableMap.get(tableNode); + return orientation === "col" ? map.width - 1 : map.height - 1; + }, [tableNode, orientation]); + + const canMove = target >= 0 && target <= maxIndex; + + const handleMove = useCallback(() => { + if (!canMove) return; + const tr = editor.state.tr; + const moved = + orientation === "col" + ? moveColumn({ + tr, + originIndex: index, + targetIndex: target, + select: true, + pos: tablePos + 1, + }) + : moveRow({ + tr, + originIndex: index, + targetIndex: target, + select: true, + pos: tablePos + 1, + }); + if (moved) editor.view.dispatch(tr); + }, [editor, orientation, index, target, tablePos, canMove]); + + return { canMove, handleMove }; +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts new file mode 100644 index 000000000..afc6a2774 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts @@ -0,0 +1,100 @@ +import { useCallback, useMemo } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { + convertArrayOfRowsToTableNode, + convertTableNodeToArrayOfRows, + transpose, +} from "@docmost/editor-ext"; +import { + getCellSortText, + isCellEmpty, + isHeaderCell, + type SortDirection, + type SortableItem, + sortItems, + weaveItems, +} from "../lib/sort-cells"; + +interface Args { + editor: Editor; + orientation: "col" | "row"; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; + direction: SortDirection; +} + +function tableHasMergedCells(tableNode: ProseMirrorNode): boolean { + for (let r = 0; r < tableNode.childCount; r++) { + const row = tableNode.child(r); + for (let c = 0; c < row.childCount; c++) { + const { colspan = 1, rowspan = 1 } = row.child(c).attrs; + if (colspan > 1 || rowspan > 1) return true; + } + } + return false; +} + +function isAllHeader(cells: (ProseMirrorNode | null)[]): boolean { + return cells.every((c) => c !== null && isHeaderCell(c)); +} + +export function useTableSort({ + editor, + orientation, + index, + tableNode, + tablePos, + direction, +}: Args) { + const canSort = useMemo(() => { + if (tableHasMergedCells(tableNode)) return false; + + const rows = convertTableNodeToArrayOfRows(tableNode); + const axes = orientation === "col" ? rows : transpose(rows); + if (axes.length < 2) return false; + + return axes.some((cells) => { + if (isAllHeader(cells)) return false; + const sortCell = cells[index]; + return !!sortCell && !isCellEmpty(sortCell); + }); + }, [tableNode, orientation, index]); + + const handleSort = useCallback(() => { + if (!canSort) return; + + const rows = convertTableNodeToArrayOfRows(tableNode); + const axes = orientation === "col" ? rows : transpose(rows); + + const items: SortableItem<(ProseMirrorNode | null)[]>[] = axes.map( + (cells, originalOrder) => { + const sortCell = cells[index]; + return { + payload: cells, + text: sortCell ? getCellSortText(sortCell) : "", + isHeader: isAllHeader(cells), + isEmpty: !sortCell || isCellEmpty(sortCell), + originalOrder, + }; + }, + ); + + const dataItems = items.filter((it) => !it.isHeader); + const sortedData = sortItems(dataItems, direction); + const woven = weaveItems(items, sortedData); + + const newAxes = woven.map((it) => it.payload); + const newRows = orientation === "col" ? newAxes : transpose(newAxes); + + const newTable = convertArrayOfRowsToTableNode(tableNode, newRows); + + const tr = editor.state.tr; + tr.replaceWith(tablePos, tablePos + tableNode.nodeSize, newTable); + + if (tr.docChanged) editor.view.dispatch(tr); + }, [editor, tableNode, tablePos, orientation, index, direction, canSort]); + + return { canSort, handleSort }; +} diff --git a/apps/client/src/features/editor/components/table/handle/lib/select-row-column.ts b/apps/client/src/features/editor/components/table/handle/lib/select-row-column.ts new file mode 100644 index 000000000..5ef315cf1 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/lib/select-row-column.ts @@ -0,0 +1,34 @@ +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import type { EditorState } from "@tiptap/pm/state"; +import { CellSelection, TableMap } from "@tiptap/pm/tables"; + +export type Orientation = "col" | "row"; + +export function buildRowOrColumnSelection( + state: EditorState, + tableNode: ProseMirrorNode, + tablePos: number, + orientation: Orientation, + index: number, +): CellSelection | null { + const map = TableMap.get(tableNode); + const tableStart = tablePos + 1; + + if (orientation === "col") { + if (index < 0 || index >= map.width) return null; + const firstCellPos = tableStart + map.map[index]; + const lastCellPos = + tableStart + map.map[(map.height - 1) * map.width + index]; + const $first = state.doc.resolve(firstCellPos); + const $last = state.doc.resolve(lastCellPos); + return CellSelection.colSelection($first, $last); + } + + if (index < 0 || index >= map.height) return null; + const firstCellPos = tableStart + map.map[index * map.width]; + const lastCellPos = + tableStart + map.map[index * map.width + (map.width - 1)]; + const $first = state.doc.resolve(firstCellPos); + const $last = state.doc.resolve(lastCellPos); + return CellSelection.rowSelection($first, $last); +} diff --git a/apps/client/src/features/editor/components/table/handle/lib/sort-cells.ts b/apps/client/src/features/editor/components/table/handle/lib/sort-cells.ts new file mode 100644 index 000000000..ffd039c2b --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/lib/sort-cells.ts @@ -0,0 +1,57 @@ +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; + +export type SortDirection = "asc" | "desc"; + +export interface SortableItem { + payload: T; + text: string; + isHeader: boolean; + isEmpty: boolean; + originalOrder: number; +} + +const HEADER_TYPE_NAMES = new Set(["tableHeader", "table_header"]); + +export function isHeaderCell(node: ProseMirrorNode): boolean { + if (HEADER_TYPE_NAMES.has(node.type.name)) return true; + return node.attrs?.header === true; +} + +export function getCellSortText(node: ProseMirrorNode): string { + let text = ""; + node.descendants((child) => { + if (child.isText) text += child.text ?? ""; + return true; + }); + return text.trim().toLowerCase(); +} + +export function isCellEmpty(node: ProseMirrorNode): boolean { + return getCellSortText(node) === ""; +} + +export const collator = new Intl.Collator(undefined, { + sensitivity: "base", + numeric: true, +}); + +export function sortItems( + data: SortableItem[], + direction: SortDirection, +): SortableItem[] { + return [...data].sort((a, b) => { + if (a.isEmpty && !b.isEmpty) return 1; + if (!a.isEmpty && b.isEmpty) return -1; + if (a.isEmpty && b.isEmpty) return a.originalOrder - b.originalOrder; + const cmp = collator.compare(a.text, b.text); + return direction === "asc" ? cmp : -cmp; + }); +} + +export function weaveItems( + all: SortableItem[], + sortedData: SortableItem[], +): SortableItem[] { + const dataQueue = [...sortedData]; + return all.map((item) => (item.isHeader ? item : dataQueue.shift()!)); +} diff --git a/apps/client/src/features/editor/components/table/handle/menus/alignment-submenu.tsx b/apps/client/src/features/editor/components/table/handle/menus/alignment-submenu.tsx new file mode 100644 index 000000000..c58f5a967 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/alignment-submenu.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import { Menu } from "@mantine/core"; +import { + IconAlignCenter, + IconAlignLeft, + IconAlignRight, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; + +interface AlignmentSubmenuProps { + editor: Editor; +} + +export const AlignmentSubmenu = React.memo(function AlignmentSubmenu({ + editor, +}: AlignmentSubmenuProps) { + const { t } = useTranslation(); + + return ( + + + }> + {t("Text alignment")} + + + + } + onClick={() => editor.chain().focus().setTextAlign("left").run()} + > + {t("Align left")} + + } + onClick={() => editor.chain().focus().setTextAlign("center").run()} + > + {t("Align center")} + + } + onClick={() => editor.chain().focus().setTextAlign("right").run()} + > + {t("Align right")} + + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx new file mode 100644 index 000000000..84f904ca7 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx @@ -0,0 +1,154 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { ColorSwatch, Menu } from "@mantine/core"; +import { + IconBoxMargin, + IconColumnInsertRight, + IconColumnRemove, + IconEraser, + IconPalette, + IconRowInsertBottom, + IconRowRemove, + IconSquareToggle, + IconTableRow, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useTableClear } from "../hooks/use-table-clear"; +import { TABLE_COLORS } from "../../table-background-color"; +import { AlignmentSubmenu } from "./alignment-submenu"; + +interface CellChevronMenuProps { + editor: Editor; + cellPos: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const CellChevronMenu = React.memo(function CellChevronMenu({ + editor, + cellPos, + tableNode, + tablePos, +}: CellChevronMenuProps) { + const { t } = useTranslation(); + + const clearCell = useTableClear(editor, tableNode, tablePos, { + kind: "cell", + cellPos, + }); + + const setBackground = (color: string, name: string) => { + editor + .chain() + .focus() + .updateAttributes("tableCell", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .updateAttributes("tableHeader", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .run(); + }; + + return ( + <> + + + }> + {t("Background color")} + + + +
+ {TABLE_COLORS.map((c) => ( + + ))} +
+
+
+ + + + } + onClick={() => editor.chain().focus().mergeCells().run()} + disabled={!editor.can().mergeCells()} + > + {t("Merge cells")} + + } + onClick={() => editor.chain().focus().splitCell().run()} + disabled={!editor.can().splitCell()} + > + {t("Split cell")} + + } + onClick={() => editor.chain().focus().toggleHeaderCell().run()} + > + {t("Toggle header cell")} + + + + + } + onClick={() => editor.chain().focus().addColumnAfter().run()} + > + {t("Add column right")} + + } + onClick={() => editor.chain().focus().addRowAfter().run()} + > + {t("Add row below")} + + + } onClick={clearCell}> + {t("Clear cell")} + + } + onClick={() => editor.chain().focus().deleteColumn().run()} + > + {t("Delete column")} + + } + onClick={() => editor.chain().focus().deleteRow().run()} + > + {t("Delete row")} + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx new file mode 100644 index 000000000..8dbe9d326 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx @@ -0,0 +1,177 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { ColorSwatch, Menu } from "@mantine/core"; +import { TABLE_COLORS } from "../../table-background-color"; +import { + IconArrowLeft, + IconArrowRight, + IconColumnInsertLeft, + IconColumnInsertRight, + IconColumnRemove, + IconEraser, + IconPalette, + IconSortAscendingLetters, + IconSortDescendingLetters, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useTableMoveRowColumn } from "../hooks/use-table-move-row-column"; +import { useTableClear } from "../hooks/use-table-clear"; +import { useTableSort } from "../hooks/use-table-sort"; +import { AlignmentSubmenu } from "./alignment-submenu"; + +interface ColumnHandleMenuProps { + editor: Editor; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const ColumnHandleMenu = React.memo(function ColumnHandleMenu({ + editor, + index, + tableNode, + tablePos, +}: ColumnHandleMenuProps) { + const { t } = useTranslation(); + + const moveLeft = useTableMoveRowColumn(editor, "col", index, "left", tableNode, tablePos); + const moveRight = useTableMoveRowColumn(editor, "col", index, "right", tableNode, tablePos); + const clearCol = useTableClear(editor, tableNode, tablePos, { + kind: "col", + index, + }); + + const setBackground = (color: string, name: string) => { + editor + .chain() + .focus() + .updateAttributes("tableCell", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .updateAttributes("tableHeader", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .run(); + }; + + const sortAsc = useTableSort({ + editor, + orientation: "col", + index, + tableNode, + tablePos, + direction: "asc", + }); + const sortDesc = useTableSort({ + editor, + orientation: "col", + index, + tableNode, + tablePos, + direction: "desc", + }); + + return ( + <> + } + onClick={sortAsc.handleSort} + disabled={!sortAsc.canSort} + > + {t("Sort A → Z")} + + } + onClick={sortDesc.handleSort} + disabled={!sortDesc.canSort} + > + {t("Sort Z → A")} + + + + + + }> + {t("Background color")} + + + +
+ {TABLE_COLORS.map((c) => ( + + ))} +
+
+
+ + + + + + } + onClick={() => editor.chain().focus().addColumnBefore().run()} + > + {t("Add column left")} + + } + onClick={() => editor.chain().focus().addColumnAfter().run()} + > + {t("Add column right")} + + + + + } + onClick={clearCol} + > + {t("Clear cells")} + + } + onClick={() => editor.chain().focus().deleteColumn().run()} + > + {t("Delete column")} + + + + + } + onClick={moveLeft.handleMove} + disabled={!moveLeft.canMove} + > + {t("Move column left")} + + } + onClick={moveRight.handleMove} + disabled={!moveRight.canMove} + > + {t("Move column right")} + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx new file mode 100644 index 000000000..13b968b76 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { ColorSwatch, Menu } from "@mantine/core"; +import { TABLE_COLORS } from "../../table-background-color"; +import { + IconArrowDown, + IconArrowUp, + IconEraser, + IconPalette, + IconRowInsertBottom, + IconRowInsertTop, + IconRowRemove, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useTableMoveRowColumn } from "../hooks/use-table-move-row-column"; +import { useTableClear } from "../hooks/use-table-clear"; +import { AlignmentSubmenu } from "./alignment-submenu"; + +interface RowHandleMenuProps { + editor: Editor; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const RowHandleMenu = React.memo(function RowHandleMenu({ + editor, + index, + tableNode, + tablePos, +}: RowHandleMenuProps) { + const { t } = useTranslation(); + + const setBackground = (color: string, name: string) => { + editor + .chain() + .focus() + .updateAttributes("tableCell", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .updateAttributes("tableHeader", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .run(); + }; + + const moveUp = useTableMoveRowColumn(editor, "row", index, "up", tableNode, tablePos); + const moveDown = useTableMoveRowColumn(editor, "row", index, "down", tableNode, tablePos); + const clearRow = useTableClear(editor, tableNode, tablePos, { + kind: "row", + index, + }); + + return ( + <> + + + }> + {t("Background color")} + + + +
+ {TABLE_COLORS.map((c) => ( + + ))} +
+
+
+ + + + + + } + onClick={() => editor.chain().focus().addRowBefore().run()} + > + {t("Add row above")} + + } + onClick={() => editor.chain().focus().addRowAfter().run()} + > + {t("Add row below")} + + + + + } onClick={clearRow}> + {t("Clear cells")} + + } + onClick={() => editor.chain().focus().deleteRow().run()} + > + {t("Delete row")} + + + + + } + onClick={moveUp.handleMove} + disabled={!moveUp.canMove} + > + {t("Move row up")} + + } + onClick={moveDown.handleMove} + disabled={!moveDown.canMove} + > + {t("Move row down")} + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/row-handle.tsx b/apps/client/src/features/editor/components/table/handle/row-handle.tsx new file mode 100644 index 000000000..7a5483558 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/row-handle.tsx @@ -0,0 +1,122 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { useFloating, offset, autoUpdate, hide } from "@floating-ui/react"; +import { Menu } from "@mantine/core"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { useTableHandleDrag } from "./hooks/use-table-handle-drag"; +import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle"; +import { RowHandleMenu } from "./menus/row-handle-menu"; +import classes from "./handle.module.css"; + +interface RowHandleProps { + editor: Editor; + index: number; + anchorPos: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const RowHandle = React.memo(function RowHandle({ + editor, + index, + anchorPos, + tableNode, + tablePos, +}: RowHandleProps) { + const { t } = useTranslation(); + // See ColumnHandle for the rationale: keep the last valid cell DOM cached + // so the handle div stays mounted across stale-anchor renders, otherwise + // pragmatic-dnd silently aborts an in-flight drag. + const lookupCellDom = editor.view.nodeDOM(anchorPos) as HTMLElement | null; + const [cellDom, setCellDom] = useState(lookupCellDom); + const lastCellDomRef = useRef(lookupCellDom); + useEffect(() => { + if (lookupCellDom && lookupCellDom !== lastCellDomRef.current) { + lastCellDomRef.current = lookupCellDom; + setCellDom(lookupCellDom); + } + }, [lookupCellDom]); + + const [handleEl, setHandleEl] = useState(null); + + const { refs, floatingStyles, middlewareData } = useFloating({ + placement: "left", + middleware: [offset(-4), hide()], + whileElementsMounted: autoUpdate, + }); + const isReferenceHidden = !!middlewareData.hide?.referenceHidden; + + useEffect(() => { + refs.setReference(cellDom); + }, [cellDom, refs]); + + const wrapper = cellDom?.closest(".tableWrapper") ?? null; + + const [menuOpened, setMenuOpened] = useState(false); + const closeMenu = useCallback(() => setMenuOpened(false), []); + useTableHandleDrag(editor, "row", handleEl, wrapper, closeMenu); + + const { onOpen, onClose } = useColumnRowMenuLifecycle({ + editor, + orientation: "row", + index, + tableNode, + tablePos, + }); + + if (!cellDom) return null; + + return ( + + +
{ + refs.setFloating(node); + setHandleEl(node); + }} + style={{ + ...floatingStyles, + ...(isReferenceHidden ? { visibility: "hidden" as const } : {}), + }} + className={clsx(classes.handle, classes.rowHandle)} + role="button" + tabIndex={0} + aria-label={t("Row actions")} + > + + + +
+
+ + + +
+ ); +}); + +function GripIcon() { + return ( + + + + ); +} diff --git a/apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx b/apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx new file mode 100644 index 000000000..e40c7baac --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import { useTableHandleState } from "./hooks/use-table-handle-state"; +import { ColumnHandle } from "./column-handle"; +import { RowHandle } from "./row-handle"; +import { CellChevron } from "./cell-chevron"; + +interface TableHandlesLayerProps { + editor: Editor | null; +} + +export const TableHandlesLayer = React.memo(function TableHandlesLayer({ + editor, +}: TableHandlesLayerProps) { + const state = useTableHandleState(editor); + + if (!editor || !editor.isEditable) return null; + if (!state.hoveringCell || !state.tableNode || state.tablePos == null) return null; + + return ( + <> + + + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/table-background-color.tsx b/apps/client/src/features/editor/components/table/table-background-color.tsx index 3e4ce6168..c0df52d81 100644 --- a/apps/client/src/features/editor/components/table/table-background-color.tsx +++ b/apps/client/src/features/editor/components/table/table-background-color.tsx @@ -22,7 +22,7 @@ interface TableBackgroundColorProps { editor: Editor | null; } -const TABLE_COLORS: TableColorItem[] = [ +export const TABLE_COLORS: TableColorItem[] = [ { name: "Default", color: "" }, { name: "Blue", color: "#b4d5ff" }, { name: "Green", color: "#acf5d2" }, diff --git a/apps/client/src/features/editor/components/table/table-menu.tsx b/apps/client/src/features/editor/components/table/table-menu.tsx index 4adafb206..3be7ec539 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -104,12 +104,12 @@ export const TableMenu = React.memo( element.style.zIndex = "99"; }} options={{ - placement: "top", + placement: "bottom", offset: { mainAxis: 15, }, flip: { - fallbackPlacements: ["top", "bottom"], + fallbackPlacements: ["bottom", "top"], padding: { top: 35 + 15, left: 8, right: 8, bottom: -Infinity }, boundary: editor.options.element as HTMLElement, }, diff --git a/apps/client/src/features/editor/components/table/table-text-alignment.tsx b/apps/client/src/features/editor/components/table/table-text-alignment.tsx index 4d4646cf5..17ef7c42e 100644 --- a/apps/client/src/features/editor/components/table/table-text-alignment.tsx +++ b/apps/client/src/features/editor/components/table/table-text-alignment.tsx @@ -86,11 +86,11 @@ export const TableTextAlignment: FC = ({ editor }) => { transitionProps={{ transition: "pop" }} > - + setOpened(!opened)} > diff --git a/apps/client/src/features/editor/extensions/drag-handle.ts b/apps/client/src/features/editor/extensions/drag-handle.ts index a4843ed67..6b10678a1 100644 --- a/apps/client/src/features/editor/extensions/drag-handle.ts +++ b/apps/client/src/features/editor/extensions/drag-handle.ts @@ -60,6 +60,23 @@ function nodeDOMAtCoords( options: GlobalDragHandleOptions, view: EditorView, ) { + // Custom nodes (transclusion, …) render via tiptap's React node-view + // renderer, which emits `class="react-renderer node-${name}"` on the + // live wrapper — the `data-type` attribute is for static HTML + // serialization only. Match both so we cover live and parsed DOM. + // Inside a custom node, also match plain `p` so the first paragraph + // (which doesn't match `:not(:first-child)`) still gets its own + // handle; only hovers on the custom node's padding/border fall + // through to the wrapper. + const customSelectors = options.customNodes.flatMap((node) => [ + `[data-type=${node}]`, + `.node-${node}`, + ]); + const customParagraphSelectors = options.customNodes.flatMap((node) => [ + `[data-type=${node}] p`, + `.node-${node} p`, + ]); + const selectors = [ "li", "p:not(:first-child)", @@ -71,7 +88,13 @@ function nodeDOMAtCoords( "h4", "h5", "h6", - ...options.customNodes.map((node) => `[data-type=${node}]`), + // Tables nested in another block (toggle, transclusion, …) have a + // wrapper that isn't a direct child of .ProseMirror, so the + // parent-check below skips it. Match the wrapper explicitly so the + // handle shows up even with empty cells. + ".tableWrapper", + ...customParagraphSelectors, + ...customSelectors, ].join(", "); return document .elementsFromPoint(coords.x, coords.y) @@ -99,6 +122,22 @@ function nodePosAtDOM( })?.inside; } +function isCustomNodeDOM( + elem: Element | null | undefined, + options: GlobalDragHandleOptions, +): boolean { + if (!elem) return false; + for (const name of options.customNodes) { + if ( + elem.getAttribute("data-type") === name || + elem.classList.contains(`node-${name}`) + ) { + return true; + } + } + return false; +} + function calcNodePos(pos: number, view: EditorView) { const $pos = view.state.doc.resolve(pos); if ($pos.depth > 1) return $pos.before($pos.depth); @@ -137,7 +176,6 @@ export function DragHandlePlugin( const nodePos = view.state.doc.resolve(fromSelectionPos); - // Check if nodePos points to the top level node if (nodePos.node().type.name === "doc") differentNodeSelected = true; else { const nodeSelection = NodeSelection.create( @@ -166,14 +204,46 @@ export function DragHandlePlugin( } else { selection = NodeSelection.create(view.state.doc, draggedNodePos); - // if inline node is selected, e.g mention -> go to the parent node to select the whole node - // if table row is selected, go to the parent node to select the whole node - if ( - (selection as NodeSelection).node.type.isInline || - (selection as NodeSelection).node.type.name === "tableRow" - ) { - let $pos = view.state.doc.resolve(selection.from); - selection = NodeSelection.create(view.state.doc, $pos.before()); + const $sel = view.state.doc.resolve(selection.from); + + if (isCustomNodeDOM(node, options)) { + // The drag landed on a custom-node container (transclusion etc.). + // Walk up to the matching node so the drag moves the whole + // container, not whatever inner element the click landed on. + const customTypes = new Set(options.customNodes); + for (let d = $sel.depth; d > 0; d--) { + if (customTypes.has($sel.node(d).type.name)) { + selection = NodeSelection.create( + view.state.doc, + $sel.before(d), + ); + break; + } + } + } else { + // If the selected node lives inside a table (at any nesting + // depth), promote to the whole table — the global drag handle is + // meant to move the table as a single block, not a row/cell. The + // earlier tableRow-only check only worked when the table sat at + // the doc root; once wrapped in another node (toggle, layout, + // etc.) the selection lands on a cell/paragraph and that check + // never fired. + let tableDepth = -1; + for (let d = $sel.depth; d > 0; d--) { + if ($sel.node(d).type.name === "table") { + tableDepth = d; + break; + } + } + if (tableDepth > 0) { + selection = NodeSelection.create( + view.state.doc, + $sel.before(tableDepth), + ); + } else if ((selection as NodeSelection).node.type.isInline) { + // Inline node (e.g. mention): walk up to the parent block. + selection = NodeSelection.create(view.state.doc, $sel.before()); + } } } view.dispatch(view.state.tr.setSelection(selection)); @@ -313,6 +383,27 @@ export function DragHandlePlugin( return; } + const isCustomNode = isCustomNodeDOM(node, options); + + // Custom nodes pin the handle to the inner NodeViewWrapper's top-left: + // the natural anchor sits in transient/empty space outside the visible block. + if (isCustomNode) { + // tiptap React node-views emit an outer `.react-renderer` whose first + // child is the visible NodeViewWrapper; walk to that outer first since + // `node` may be either the outer or an inner element with data-type. + const rendererOuter = + (node.closest(".react-renderer") as HTMLElement | null) ?? node; + const inner = + (rendererOuter.firstElementChild as HTMLElement | null) ?? + rendererOuter; + const innerRect = absoluteRect(inner); + if (!dragHandleElement) return; + dragHandleElement.style.left = `${innerRect.left + 4}px`; + dragHandleElement.style.top = `${innerRect.top + 4}px`; + showDragHandle(); + return; + } + const compStyle = window.getComputedStyle(node); const parsedLineHeight = parseInt(compStyle.lineHeight, 10); const lineHeight = isNaN(parsedLineHeight) @@ -328,6 +419,13 @@ export function DragHandlePlugin( if (node.matches("ul:not([data-type=taskList]) li, ol li")) { rect.left -= options.dragHandleWidth; } + // Tables: clear the table's own row-drag handle so the two + // grips don't stack on each other. `nodeDOMAtCoords` returns + // the wrapper for top-level hovers (wrapper is direct child of + // .ProseMirror) and a descendant for deeper hovers — cover both. + if (node.closest(".tableWrapper")) { + rect.left -= options.dragHandleWidth; + } rect.width = options.dragHandleWidth; if (!dragHandleElement) return; diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 23be85aa1..1f09bef37 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -45,6 +45,9 @@ import { SearchAndReplace, Mention, TableDndExtension, + TableHandleCommandsExtension, + TableHeaderPin, + TableReadonlySort, Subpages, Heading, Highlight, @@ -56,6 +59,7 @@ import { Status, TransclusionSource, TransclusionReference, + TableView, } from "@docmost/editor-ext"; import { randomElement, @@ -259,11 +263,16 @@ export const mainExtensions = [ resizable: true, lastColumnResizable: true, allowTableNodeSelection: true, + cellMinWidth: 49, + View: TableView, }), TableRow, TableCell, TableHeader, TableDndExtension, + TableHandleCommandsExtension, + TableHeaderPin, + TableReadonlySort, MathInline.configure({ view: MathInlineView, }), diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index 57aab5bb0..4e2fcccf6 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -44,6 +44,7 @@ import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubbl import { ReadonlyBubbleMenu } from "@/features/editor/components/bubble-menu/readonly-bubble-menu"; import TableCellMenu from "@/features/editor/components/table/table-cell-menu.tsx"; import TableMenu from "@/features/editor/components/table/table-menu.tsx"; +import { TableHandlesLayer } from "@/features/editor/components/table/handle/table-handles-layer"; import ImageMenu from "@/features/editor/components/image/image-menu.tsx"; import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx"; import VideoMenu from "@/features/editor/components/video/video-menu.tsx"; @@ -424,7 +425,7 @@ export default function PageEditor({ - + diff --git a/apps/client/src/features/editor/styles/core.css b/apps/client/src/features/editor/styles/core.css index 34ddaca3c..077570fb5 100644 --- a/apps/client/src/features/editor/styles/core.css +++ b/apps/client/src/features/editor/styles/core.css @@ -203,7 +203,8 @@ } } - .resize-cursor { + &.resize-cursor, + &.resize-cursor * { cursor: ew-resize; cursor: col-resize; } diff --git a/apps/client/src/features/editor/styles/table.css b/apps/client/src/features/editor/styles/table.css index 9926d0bc0..5d802e4ab 100644 --- a/apps/client/src/features/editor/styles/table.css +++ b/apps/client/src/features/editor/styles/table.css @@ -15,7 +15,8 @@ } .table-dnd-drop-indicator { - background-color: #adf; + background-color: var(--mantine-color-blue-5); + z-index: 3; } .ProseMirror { @@ -57,13 +58,14 @@ } .column-resize-handle { - background-color: #adf; + background-color: var(--mantine-color-blue-5); bottom: -1px; position: absolute; - right: -2px; + right: -1px; pointer-events: none; top: 0; - width: 4px; + width: 2px; + z-index: 3; } .selectedCell:after { @@ -129,6 +131,139 @@ } } + +/* Header-row pinning. Two CSS paths, picked by `header-pin/controller.ts`: + - native sticky (preferred): wrapper drops its overflow constraint so + `position: sticky` on the row can resolve against the document scroll. + - transform fallback: wrapper keeps `overflow-x: auto` for horizontal + scrolling; the row is positioned imperatively per scroll frame. + + `--editor-pin-offset` is published to :root by `pinOffsetWatcher` in + `header-pin/offset.ts`, measured against the lowest fixed surface above + the editor (app shell header, page header, fixed toolbar). */ + +.tableWrapper.tableWrapperNoOverflow, +.tableWrapper.tableWrapperNoOverflow table { + overflow: visible; +} + +.tableWrapper.tableHeaderPinned table tr:first-child { + z-index: 2; +} + +.tableWrapper.tableWrapperNoOverflow.tableHeaderPinned table tr:first-child { + position: sticky; + top: var(--editor-pin-offset, 90px); +} + +.tableWrapper.tableHeaderPinned:not(.tableWrapperNoOverflow) table tr:first-child { + position: relative; + transform: translateY(var(--table-pin-offset, 0px)); +} + +@media print { + .tableWrapper.tableHeaderPinned table tr:first-child { + position: static; + transform: none; + } +} + +.tableReadonlySortChevron { + /* Anchor to the cell's right edge, vertically centered with the cell + content. The cell content (a

) is block-level so an inline chevron + would wrap to a new line; absolute positioning takes it out of flow. */ + position: absolute; + top: 50%; + right: 6px; + transform: translateY(-50%); + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 4px; + background: light-dark( + rgba(55, 53, 47, 0.08), + rgba(255, 255, 255, 0.08) + ); + color: light-dark( + rgba(55, 53, 47, 0.55), + rgba(255, 255, 255, 0.55) + ); + user-select: none; + cursor: pointer; + z-index: 1; + /* Hidden by default; revealed on header-cell hover or when this column is + the active sort (see selectors below). */ + opacity: 0; + transition: opacity 120ms ease, background-color 120ms ease, color 120ms ease; +} + +.ProseMirror table th:hover .tableReadonlySortChevron, +.tableReadonlySortChevron[data-sort] { + opacity: 1; +} + +.ProseMirror table th:has(.tableReadonlySortChevron) { + padding-right: 30px; +} + +.tableReadonlySortChevron:hover { + background: light-dark( + rgba(55, 53, 47, 0.16), + rgba(255, 255, 255, 0.16) + ); +} + +/* Immediate tooltip on the chevron — same style language as the rest of the + app (small, dark, rounded), unlike the native `title` tooltip which only + appears after a long delay. */ +.tableReadonlySortChevron::after { + content: attr(data-tooltip); + position: absolute; + /* Below the chevron — placing it above the cell hits the table's + overflow clipping (the wrapper has `overflow-x: auto` which forces + `overflow-y: auto` per spec). */ + top: calc(100% + 6px); + right: 0; + padding: 4px 8px; + border-radius: 4px; + background: var(--mantine-color-dark-7); + color: var(--mantine-color-white); + font-size: 12px; + font-weight: 400; + line-height: 1.4; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 120ms ease; + z-index: 10; +} + +.tableReadonlySortChevron:hover::after { + opacity: 1; +} + +.tableReadonlySortChevron svg { + display: block; +} + +.tableReadonlySortChevron[data-sort="asc"], +.tableReadonlySortChevron[data-sort="desc"] { + background: light-dark( + var(--mantine-color-blue-1), + var(--mantine-color-blue-9) + ); + color: light-dark( + var(--mantine-color-blue-7), + var(--mantine-color-blue-2) + ); +} + +.tableReadonlySortChevron[data-sort="asc"] svg { + transform: rotate(180deg); +} + .editor-container:has(.table-dnd-drop-indicator[data-dragging="true"]) { .prosemirror-dropcursor-block { display: none; diff --git a/apps/client/src/features/page/components/header/page-header.tsx b/apps/client/src/features/page/components/header/page-header.tsx index 12f131b8d..0614cf0bd 100644 --- a/apps/client/src/features/page/components/header/page-header.tsx +++ b/apps/client/src/features/page/components/header/page-header.tsx @@ -8,7 +8,7 @@ interface Props { } export default function PageHeader({ readOnly }: Props) { return ( -

+
diff --git a/packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts b/packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts deleted file mode 100644 index 9b8304d54..000000000 --- a/packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { DraggingDOMs } from "./utils"; - -const EDGE_THRESHOLD = 100; -const SCROLL_SPEED = 10; - -export class AutoScrollController { - private _autoScrollInterval?: number; - - checkYAutoScroll = (clientY: number) => { - const scrollContainer = document.documentElement; - - if (clientY < 0 + EDGE_THRESHOLD) { - this._startYAutoScroll(scrollContainer!, -1 * SCROLL_SPEED); - } else if (clientY > window.innerHeight - EDGE_THRESHOLD) { - this._startYAutoScroll(scrollContainer!, SCROLL_SPEED); - } else { - this._stopYAutoScroll(); - } - } - - checkXAutoScroll = (clientX: number, draggingDOMs: DraggingDOMs) => { - const table = draggingDOMs?.table; - if (!table) return; - - const scrollContainer = table.closest('.tableWrapper'); - const editorRect = scrollContainer.getBoundingClientRect(); - if (!scrollContainer) return; - - if (clientX < editorRect.left + EDGE_THRESHOLD) { - this._startXAutoScroll(scrollContainer!, -1 * SCROLL_SPEED); - } else if (clientX > editorRect.right - EDGE_THRESHOLD) { - this._startXAutoScroll(scrollContainer!, SCROLL_SPEED); - } else { - this._stopXAutoScroll(); - } - } - - stop = () => { - this._stopXAutoScroll(); - this._stopYAutoScroll(); - } - - private _startXAutoScroll = (scrollContainer: HTMLElement, speed: number) => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - } - - this._autoScrollInterval = window.setInterval(() => { - scrollContainer.scrollLeft += speed; - }, 16); - } - - private _stopXAutoScroll = () => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - this._autoScrollInterval = undefined; - } - } - - private _startYAutoScroll = (scrollContainer: HTMLElement, speed: number) => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - } - - this._autoScrollInterval = window.setInterval(() => { - scrollContainer.scrollTop += speed; - }, 16); - } - - private _stopYAutoScroll = () => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - this._autoScrollInterval = undefined; - } - } -} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts index 1ad57ec1f..b4ca516ae 100644 --- a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts +++ b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts @@ -1,316 +1,393 @@ import { Editor, Extension } from "@tiptap/core"; -import { PluginKey, Plugin, PluginSpec } from "@tiptap/pm/state"; +import { PluginKey, Plugin, PluginSpec, TextSelection, Transaction } from "@tiptap/pm/state"; +import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { EditorProps, EditorView } from "@tiptap/pm/view"; +import { columnResizingPluginKey } from "@tiptap/pm/tables"; +import { cellAround } from "@tiptap/pm/tables"; import { + cellInfoFromResolvedCell, DraggingDOMs, getDndRelatedDOMs, getHoveringCell, HoveringCellInfo, } from "./utils"; import { getDragOverColumn, getDragOverRow } from "./calc-drag-over"; +import { findTable } from "../utils/query"; import { moveColumn, moveRow } from "../utils"; import { PreviewController } from "./preview/preview-controller"; import { DropIndicatorController } from "./preview/drop-indicator-controller"; -import { DragHandleController } from "./handle/drag-handle-controller"; -import { EmptyImageController } from "./handle/empty-image-controller"; -import { AutoScrollController } from "./auto-scroll-controller"; -export const TableDndKey = new PluginKey("table-drag-and-drop"); +export interface TableHandleState { + hoveringCell: HoveringCellInfo | null; + tableNode: ProseMirrorNode | null; + tablePos: number | null; + dragging: { orientation: "col" | "row"; index: number } | null; + frozen: boolean; +} -class TableDragHandlePluginSpec implements PluginSpec { +const INITIAL_STATE: TableHandleState = { + hoveringCell: null, + tableNode: null, + tablePos: null, + dragging: null, + frozen: false, +}; + +export const TableDndKey = new PluginKey("table-handles"); + +class TableHandlePluginSpec implements PluginSpec { key = TableDndKey; - props: EditorProps>; + props: EditorProps>; + + private _previewController: PreviewController; + private _dropIndicatorController: DropIndicatorController; - private _colDragHandle: HTMLElement; - private _rowDragHandle: HTMLElement; private _hoveringCell?: HoveringCellInfo; private _disposables: (() => void)[] = []; - private _draggingCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _dragging = false; private _draggingDirection: "col" | "row" = "col"; private _draggingIndex = -1; private _droppingIndex = -1; - private _draggingDOMs?: DraggingDOMs | undefined; - private _startCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _previewController: PreviewController; - private _dropIndicatorController: DropIndicatorController; - private _dragHandleController: DragHandleController; - private _emptyImageController: EmptyImageController; - private _autoScrollController: AutoScrollController; + private _draggingDOMs?: DraggingDOMs; + private _startCoords = { x: 0, y: 0 }; + private _dragging = false; + + state = { + init: (): TableHandleState => INITIAL_STATE, + apply: (tr: Transaction, prev: TableHandleState): TableHandleState => { + const meta = tr.getMeta(TableDndKey) as Partial | null; + if (!meta) return prev; + let changed = false; + for (const key in meta) { + if (!Object.is(prev[key as keyof TableHandleState], meta[key as keyof TableHandleState])) { + changed = true; + break; + } + } + return changed ? { ...prev, ...meta } : prev; + }, + }; constructor(public editor: Editor) { this.props = { handleDOMEvents: { - pointerover: this._pointerOver, + pointermove: this._pointerMove, + // Force-unfreeze on any pointerdown that lands on the editor. + // Mantine's `Menu.onClose` doesn't always fire on outside click + // (the dropdown vanishes visually but the callback is skipped), + // which would otherwise leave `frozen=true` permanently. + pointerdown: this._pointerDown, }, }; - this._dragHandleController = new DragHandleController(); - this._colDragHandle = this._dragHandleController.colDragHandle; - this._rowDragHandle = this._dragHandleController.rowDragHandle; - this._previewController = new PreviewController(); this._dropIndicatorController = new DropIndicatorController(); - this._emptyImageController = new EmptyImageController(); - - this._autoScrollController = new AutoScrollController(); - - this._bindDragEvents(); } view = () => { const wrapper = this.editor.options.element; - //@ts-ignore - wrapper.appendChild(this._colDragHandle); - //@ts-ignore - wrapper.appendChild(this._rowDragHandle); - //@ts-ignore + // @ts-ignore wrapper.appendChild(this._previewController.previewRoot); - //@ts-ignore + // @ts-ignore wrapper.appendChild(this._dropIndicatorController.dropIndicatorRoot); + // Track the cursor cell so handles follow keyboard nav and clicks too. + this.editor.on("selectionUpdate", this._onSelectionUpdate); + this._disposables.push(() => + this.editor.off("selectionUpdate", this._onSelectionUpdate), + ); + return { - update: this.update, destroy: this.destroy, }; }; - update = () => {}; - destroy = () => { - if (!this.editor.isDestroyed) return; - this._dragHandleController.destroy(); - this._emptyImageController.destroy(); this._previewController.destroy(); this._dropIndicatorController.destroy(); - this._autoScrollController.stop(); - - this._disposables.forEach((disposable) => disposable()); + this._disposables.forEach((d) => d()); }; - private _pointerOver = (view: EditorView, event: PointerEvent) => { - if (this._dragging) return; + private _pointerDown = (view: EditorView, _event: PointerEvent): boolean => { + const current = TableDndKey.getState(view.state); + if (current?.frozen) this.editor.commands.unfreezeHandles(); + return false; + }; + + private _pointerMove = (view: EditorView, event: PointerEvent) => { + const current = TableDndKey.getState(view.state); + if (current?.frozen || current?.dragging) return; + + const resizeState = columnResizingPluginKey.getState(view.state); + if (resizeState?.dragging) return; - // Don't show drag handles in readonly mode if (!this.editor.isEditable) { - this._dragHandleController.hide(); + if (current?.hoveringCell == null && current?.tableNode == null && current?.tablePos == null) return; + this._dispatchMeta({ hoveringCell: null, tableNode: null, tablePos: null }); return; } const hoveringCell = getHoveringCell(view, event); - this._hoveringCell = hoveringCell; - if (!hoveringCell) { - this._dragHandleController.hide(); - } else { - this._dragHandleController.show(this.editor, hoveringCell); + if (hoveringCell) { + if (current?.hoveringCell?.cellPos === hoveringCell.cellPos) return; + this._hoveringCell = hoveringCell; + const $cell = view.state.doc.resolve(hoveringCell.cellPos); + const tableInfo = findTable($cell); + this._dispatchMeta({ + hoveringCell, + tableNode: tableInfo?.node ?? null, + tablePos: tableInfo?.pos ?? null, + }); + return; } + + // Pointer isn't over a cell but may be transiting toward a handle that + // floats outside the cell — fall back to the selection's cell so the + // handles stay visible. + const $cellPos = cellAround(view.state.selection.$head); + if ($cellPos) { + const cellInfo = cellInfoFromResolvedCell($cellPos); + if (current?.hoveringCell?.cellPos === cellInfo.cellPos) return; + this._hoveringCell = cellInfo; + const tableInfo = findTable($cellPos); + this._dispatchMeta({ + hoveringCell: cellInfo, + tableNode: tableInfo?.node ?? null, + tablePos: tableInfo?.pos ?? null, + }); + return; + } + + this._hoveringCell = undefined; + if (current?.hoveringCell == null && current?.tableNode == null && current?.tablePos == null) return; + this._dispatchMeta({ hoveringCell: null, tableNode: null, tablePos: null }); }; - private _onDragColStart = (event: DragEvent) => { - this._onDragStart(event, "col"); + private _onSelectionUpdate = () => { + if (!this.editor.isEditable) return; + + const current = TableDndKey.getState(this.editor.state); + if (current?.frozen || current?.dragging) return; + + const $cellPos = cellAround(this.editor.state.selection.$head); + if (!$cellPos) return; + + const cellInfo = cellInfoFromResolvedCell($cellPos); + if (current?.hoveringCell?.cellPos === cellInfo.cellPos) return; + + this._hoveringCell = cellInfo; + const tableInfo = findTable($cellPos); + this._dispatchMeta({ + hoveringCell: cellInfo, + tableNode: tableInfo?.node ?? null, + tablePos: tableInfo?.pos ?? null, + }); }; - private _onDraggingCol = (event: DragEvent) => { + private _dispatchMeta = (patch: Partial) => { + const tr = this.editor.state.tr.setMeta(TableDndKey, patch); + tr.setMeta("addToHistory", false); + this.editor.view.dispatch(tr); + }; + + // ---- Public API for the React handle layer ---- + + // Returns true if the drag was set up successfully. + startDragFromHandle = ( + orientation: "col" | "row", + clientX: number, + clientY: number, + ): boolean => { + if (!this._hoveringCell) return false; + this._dragging = true; + this._draggingDirection = orientation; + this._startCoords = { x: clientX, y: clientY }; + + const draggingIndex = + (orientation === "col" + ? this._hoveringCell.colIndex + : this._hoveringCell.rowIndex) ?? 0; + this._draggingIndex = draggingIndex; + + const relatedDoms = getDndRelatedDOMs( + this.editor.view, + this._hoveringCell.cellPos, + draggingIndex, + orientation, + ); + if (!relatedDoms) { + this._dragging = false; + return false; + } + this._draggingDOMs = relatedDoms; + + this._previewController.onDragStart(relatedDoms, draggingIndex, orientation); + this._dropIndicatorController.onDragStart(relatedDoms, orientation); + + // Park the selection inside the dragged cell unless it's already in the + // same table. PM auto-maps `selection.from` through concurrent remote + // transactions, so commitDrop can resolve the table even if the doc + // shifted mid-drag — same trick the pre-pragmatic-dnd implementation + // relied on. + const state = this.editor.state; + const currentTable = findTable(state.selection.$from); + const hoverTable = (() => { + try { + return findTable(state.doc.resolve(this._hoveringCell.cellPos)); + } catch { + return undefined; + } + })(); + const tr = state.tr; + if ( + hoverTable && + (!currentTable || currentTable.pos !== hoverTable.pos) + ) { + try { + const $inside = state.doc.resolve(this._hoveringCell.cellPos + 1); + tr.setSelection(TextSelection.near($inside, 1)); + } catch {} + } + tr.setMeta(TableDndKey, { + dragging: { orientation, index: draggingIndex }, + }); + tr.setMeta("addToHistory", false); + this.editor.view.dispatch(tr); + return true; + }; + + updateDragPosition = (clientX: number, clientY: number) => { const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; + if (!draggingDOMs || !this._dragging) return; - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging( - draggingDOMs, - this._draggingCoords.x, - this._draggingCoords.y, - "col", - ); + if (this._draggingDirection === "col") { + this._previewController.onDragging( + draggingDOMs, + clientX, + clientY, + "col", + ); + const direction = this._startCoords.x > clientX ? "left" : "right"; + const dragOverColumn = getDragOverColumn(draggingDOMs.table, clientX); + if (!dragOverColumn) return; + const [col, index] = dragOverColumn; + this._droppingIndex = index; + this._dropIndicatorController.onDragging(col, direction, "col"); + return; + } - this._autoScrollController.checkXAutoScroll(event.clientX, draggingDOMs); - - const direction = - this._startCoords.x > this._draggingCoords.x ? "left" : "right"; - const dragOverColumn = getDragOverColumn( - draggingDOMs.table, - this._draggingCoords.x, - ); - if (!dragOverColumn) return; - - const [col, index] = dragOverColumn; - this._droppingIndex = index; - this._dropIndicatorController.onDragging(col, direction, "col"); - }; - - private _onDragRowStart = (event: DragEvent) => { - this._onDragStart(event, "row"); - }; - - private _onDraggingRow = (event: DragEvent) => { - const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; - - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging( - draggingDOMs, - this._draggingCoords.x, - this._draggingCoords.y, - "row", - ); - - this._autoScrollController.checkYAutoScroll(event.clientY); - - const direction = - this._startCoords.y > this._draggingCoords.y ? "up" : "down"; - const dragOverRow = getDragOverRow( - draggingDOMs.table, - this._draggingCoords.y, - ); + this._previewController.onDragging(draggingDOMs, clientX, clientY, "row"); + const direction = this._startCoords.y > clientY ? "up" : "down"; + const dragOverRow = getDragOverRow(draggingDOMs.table, clientY); if (!dragOverRow) return; - const [row, index] = dragOverRow; this._droppingIndex = index; this._dropIndicatorController.onDragging(row, direction, "row"); }; - private _onDragEnd = () => { - this._dragging = false; - this._draggingIndex = -1; - this._droppingIndex = -1; - this._startCoords = { x: 0, y: 0 }; - this._autoScrollController.stop(); - this._dropIndicatorController.onDragEnd(); - this._previewController.onDragEnd(); - }; - - private _bindDragEvents = () => { - this._colDragHandle.addEventListener("dragstart", this._onDragColStart); - this._disposables.push(() => { - this._colDragHandle.removeEventListener( - "dragstart", - this._onDragColStart, - ); - }); - - this._colDragHandle.addEventListener("dragend", this._onDragEnd); - this._disposables.push(() => { - this._colDragHandle.removeEventListener("dragend", this._onDragEnd); - }); - - this._rowDragHandle.addEventListener("dragstart", this._onDragRowStart); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener( - "dragstart", - this._onDragRowStart, - ); - }); - - this._rowDragHandle.addEventListener("dragend", this._onDragEnd); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener("dragend", this._onDragEnd); - }); - - const ownerDocument = this.editor.view.dom?.ownerDocument; - if (ownerDocument) { - // To make `drop` event work, we need to prevent the default behavior of the - // `dragover` event for drop zone. Here we set the whole document as the - // drop zone so that even the mouse moves outside the editor, the `drop` - // event will still be triggered. - ownerDocument.addEventListener("drop", this._onDrop); - ownerDocument.addEventListener("dragover", this._onDrag); - this._disposables.push(() => { - ownerDocument.removeEventListener("drop", this._onDrop); - ownerDocument.removeEventListener("dragover", this._onDrag); - }); - } - }; - - private _onDragStart = (event: DragEvent, type: "col" | "row") => { - const dataTransfer = event.dataTransfer; - if (dataTransfer) { - dataTransfer.effectAllowed = "move"; - this._emptyImageController.hideDragImage(dataTransfer); - } - this._dragging = true; - this._draggingDirection = type; - this._startCoords = { x: event.clientX, y: event.clientY }; - const draggingIndex = - (type === "col" - ? this._hoveringCell?.colIndex - : this._hoveringCell?.rowIndex) ?? 0; - - this._draggingIndex = draggingIndex; - - const relatedDoms = getDndRelatedDOMs( - this.editor.view, - this._hoveringCell?.cellPos, - draggingIndex, - type, - ); - this._draggingDOMs = relatedDoms; - - const index = - type === "col" - ? this._hoveringCell?.colIndex - : this._hoveringCell?.rowIndex; - - this._previewController.onDragStart(relatedDoms, index, type); - this._dropIndicatorController.onDragStart(relatedDoms, type); - }; - - private _onDrag = (event: DragEvent) => { - event.preventDefault(); - if (!this._dragging) return; - if (this._draggingDirection === "col") { - this._onDraggingCol(event); - } else { - this._onDraggingRow(event); - } - }; - - private _onDrop = () => { + commitDrop = () => { if (!this._dragging) return; const direction = this._draggingDirection; const from = this._draggingIndex; const to = this._droppingIndex; + + if (from < 0 || to < 0 || from === to) return; + + // Use the live (auto-mapped) selection as the table anchor — PM has + // already mapped it through any concurrent remote transactions, so + // it's safe to resolve even if the doc shifted mid-drag. const tr = this.editor.state.tr; const pos = this.editor.state.selection.from; if (direction === "col") { - const canMove = moveColumn({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }); - if (canMove) { + if (moveColumn({ tr, originIndex: from, targetIndex: to, select: true, pos })) { this.editor.view.dispatch(tr); } - return; } - - if (direction === "row") { - const canMove = moveRow({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }); - if (canMove) { - this.editor.view.dispatch(tr); - } - - return; + if (moveRow({ tr, originIndex: from, targetIndex: to, select: true, pos })) { + this.editor.view.dispatch(tr); } }; + + endDrag = () => { + this._dragging = false; + this._draggingIndex = -1; + this._droppingIndex = -1; + this._startCoords = { x: 0, y: 0 }; + this._draggingDOMs = undefined; + this._dropIndicatorController.onDragEnd(); + this._previewController.onDragEnd(); + this._dispatchMeta({ dragging: null }); + }; +} + +export type { TableHandlePluginSpec }; + +// Resolve via plugin key, not a module singleton — survives StrictMode / HMR. +export function getTableHandlePluginSpec( + editor: Editor, +): TableHandlePluginSpec | null { + const plugin = TableDndKey.get(editor.state); + if (!plugin) return null; + return plugin.spec as unknown as TableHandlePluginSpec; } export const TableDndExtension = Extension.create({ name: "table-drag-and-drop", addProseMirrorPlugins() { const editor = this.editor; - - const dragHandlePluginSpec = new TableDragHandlePluginSpec(editor); - const dragHandlePlugin = new Plugin(dragHandlePluginSpec); - - return [dragHandlePlugin]; + const spec = new TableHandlePluginSpec(editor); + return [new Plugin(spec)]; }, }); + +export const TableHandleCommandsExtension = Extension.create({ + name: "table-handle-commands", + addCommands() { + return { + freezeHandles: + () => + ({ tr, dispatch }) => { + if (dispatch) { + tr.setMeta(TableDndKey, { frozen: true }); + tr.setMeta("addToHistory", false); + } + return true; + }, + unfreezeHandles: + () => + ({ tr, state, dispatch }) => { + if (dispatch) { + // Re-sync `hoveringCell` to the cursor's cell as we unfreeze: + // `selectionUpdate` was gated while frozen, so the stored + // hoveringCell may be stale. + const patch: Partial = { frozen: false }; + const $cellPos = cellAround(state.selection.$head); + if ($cellPos) { + const cellInfo = cellInfoFromResolvedCell($cellPos); + const tableInfo = findTable($cellPos); + patch.hoveringCell = cellInfo; + patch.tableNode = tableInfo?.node ?? null; + patch.tablePos = tableInfo?.pos ?? null; + } else { + patch.hoveringCell = null; + patch.tableNode = null; + patch.tablePos = null; + } + tr.setMeta(TableDndKey, patch); + tr.setMeta("addToHistory", false); + } + return true; + }, + }; + }, +}); + +declare module "@tiptap/core" { + interface Commands { + tableHandleCommands: { + freezeHandles: () => ReturnType; + unfreezeHandles: () => ReturnType; + }; + } +} diff --git a/packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts b/packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts deleted file mode 100644 index 33137e91f..000000000 --- a/packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { HoveringCellInfo } from "../utils"; -import { computePosition, offset } from "@floating-ui/dom"; - -export class DragHandleController { - private _colDragHandle: HTMLElement; - private _rowDragHandle: HTMLElement; - - constructor() { - this._colDragHandle = this._createDragHandleDom('col'); - this._rowDragHandle = this._createDragHandleDom('row'); - } - - get colDragHandle() { - return this._colDragHandle; - } - - get rowDragHandle() { - return this._rowDragHandle; - } - - show = (editor: Editor, hoveringCell: HoveringCellInfo) => { - this._showColDragHandle(editor, hoveringCell); - this._showRowDragHandle(editor, hoveringCell); - } - - hide = () => { - Object.assign(this._colDragHandle.style, { - display: 'none', - left: '-999px', - top: '-999px', - }); - Object.assign(this._rowDragHandle.style, { - display: 'none', - left: '-999px', - top: '-999px', - }); - } - - destroy = () => { - this._colDragHandle.remove() - this._rowDragHandle.remove() - } - - private _createDragHandleDom = (type: 'col' | 'row') => { - const dragHandle = document.createElement('div') - dragHandle.classList.add('drag-handle') - dragHandle.setAttribute('draggable', 'true') - dragHandle.setAttribute('data-direction', type === 'col' ? 'horizontal' : 'vertical') - dragHandle.setAttribute('data-drag-handle', '') - Object.assign(dragHandle.style, { - position: 'absolute', - top: '-999px', - left: '-999px', - display: 'none', - }) - return dragHandle; - } - - private _showColDragHandle(editor: Editor, hoveringCell: HoveringCellInfo) { - const referenceCell = editor.view.nodeDOM(hoveringCell.colFirstCellPos); - if (!referenceCell) return; - - const yOffset = -1 * parseInt(getComputedStyle(this._colDragHandle).height) / 2; - - computePosition( - referenceCell as HTMLElement, - this._colDragHandle, - { - placement: 'top', - middleware: [offset(yOffset)] - } - ) - .then(({ x, y }) => { - Object.assign(this._colDragHandle.style, { - display: 'block', - top: `${y}px`, - left: `${x}px`, - }); - }) - } - - private _showRowDragHandle(editor: Editor, hoveringCell: HoveringCellInfo) { - const referenceCell = editor.view.nodeDOM(hoveringCell.rowFirstCellPos); - if (!referenceCell) return; - - const xOffset = -1 * parseInt(getComputedStyle(this._rowDragHandle).width) / 2; - - computePosition( - referenceCell as HTMLElement, - this._rowDragHandle, - { - middleware: [offset(xOffset)], - placement: 'left' - } - ) - .then(({ x, y}) => { - Object.assign(this._rowDragHandle.style, { - display: 'block', - top: `${y}px`, - left: `${x}px`, - }); - }) - } -} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts b/packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts deleted file mode 100644 index 8848a6b04..000000000 --- a/packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts +++ /dev/null @@ -1,21 +0,0 @@ -export class EmptyImageController { - private _emptyImage: HTMLImageElement; - - constructor() { - this._emptyImage = new Image(1, 1); - this._emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; - } - - get emptyImage() { - return this._emptyImage; - } - - hideDragImage = (dataTransfer: DataTransfer) => { - dataTransfer.effectAllowed = 'move'; - dataTransfer.setDragImage(this._emptyImage, 0, 0); - } - - destroy = () => { - this._emptyImage.remove(); - } -} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/dnd/index.ts b/packages/editor-ext/src/lib/table/dnd/index.ts index cb21bec14..eaeade987 100644 --- a/packages/editor-ext/src/lib/table/dnd/index.ts +++ b/packages/editor-ext/src/lib/table/dnd/index.ts @@ -1 +1,7 @@ -export * from './dnd-extension' \ No newline at end of file +export { + TableDndExtension, + TableHandleCommandsExtension, + TableDndKey, + getTableHandlePluginSpec, +} from "./dnd-extension"; +export type { TableHandleState, TableHandlePluginSpec } from "./dnd-extension"; diff --git a/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts b/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts index 0f0798282..a42c632f7 100644 --- a/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts +++ b/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts @@ -99,4 +99,4 @@ export class DropIndicatorController { }); } -} \ No newline at end of file +} diff --git a/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts b/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts index b7a0ea40d..9884f00f6 100644 --- a/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts +++ b/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts @@ -1,4 +1,4 @@ -import { computePosition, offset, ReferenceElement } from "@floating-ui/dom"; +import { computePosition, offset, shift, ReferenceElement } from "@floating-ui/dom"; import { DraggingDOMs } from "../utils"; import { clearPreviewDOM, createPreviewDOM } from "./render-preview"; @@ -23,7 +23,7 @@ export class PreviewController { onDragStart = (relatedDoms: DraggingDOMs, index: number | undefined, type: 'col' | 'row') => { this._initPreviewStyle(relatedDoms.table, relatedDoms.cell, type); createPreviewDOM(relatedDoms.table, this._preview, index, type) - this._initPreviewPosition(relatedDoms.cell, type); + this._initPreviewPosition(relatedDoms.table, relatedDoms.cell, type); } onDragEnd = () => { @@ -32,7 +32,7 @@ export class PreviewController { } onDragging = (relatedDoms: DraggingDOMs, x: number, y: number, type: 'col' | 'row') => { - this._updatePreviewPosition(x, y, relatedDoms.cell, type); + this._updatePreviewPosition(x, y, relatedDoms.table, relatedDoms.cell, type); } destroy = () => { @@ -60,7 +60,7 @@ export class PreviewController { } } - private _initPreviewPosition(cell: HTMLElement, type: 'col' | 'row') { + private _initPreviewPosition(table: HTMLElement, cell: HTMLElement, type: 'col' | 'row') { void computePosition(cell, this._preview, { placement: type === 'row' ? 'right' : 'bottom', middleware: [ @@ -70,6 +70,7 @@ export class PreviewController { } return -rects.reference.width }), + shift({ boundary: table, padding: 0 }), ], }).then(({ x, y }) => { Object.assign(this._preview.style, { @@ -79,11 +80,20 @@ export class PreviewController { }); } - private _updatePreviewPosition(x: number, y: number, cell: HTMLElement, type: 'col' | 'row') { + // Clamp the preview to within the table's bounds via `shift({ boundary })` + // so it can't track the cursor past the table edge. Without the clamp, + // dragging near the viewport edge pushes the preview's `left` (or `top`) + // beyond the document's natural width/height, the browser extends the + // page to contain it, and the auto-scroll plugin then has a wider area + // to keep scrolling into — a feedback loop that grows the page forever. + private _updatePreviewPosition(x: number, y: number, table: HTMLElement, cell: HTMLElement, type: 'col' | 'row') { computePosition( getVirtualElement(cell, x, y), this._preview, - { placement: type === 'row' ? 'right' : 'bottom' }, + { + placement: type === 'row' ? 'right' : 'bottom', + middleware: [shift({ boundary: table, padding: 0 })], + }, ).then(({ x, y }) => { if (type === 'row') { Object.assign(this._preview.style, { diff --git a/packages/editor-ext/src/lib/table/dnd/utils.ts b/packages/editor-ext/src/lib/table/dnd/utils.ts index d184368f4..9b00769d3 100644 --- a/packages/editor-ext/src/lib/table/dnd/utils.ts +++ b/packages/editor-ext/src/lib/table/dnd/utils.ts @@ -1,4 +1,5 @@ import { cellAround, TableMap } from "@tiptap/pm/tables" +import { ResolvedPos } from "@tiptap/pm/model" import { EditorView } from "@tiptap/pm/view" export function getHoveringCell( @@ -8,19 +9,30 @@ export function getHoveringCell( const domCell = domCellAround(event.target as HTMLElement | null) if (!domCell) return - const { left, top, width, height } = domCell.getBoundingClientRect() - const eventPos = view.posAtCoords({ - // Use the center coordinates of the cell to ensure we're within the - // selected cell. This prevents potential issues when the mouse is on the - // border of two cells. - left: left + width / 2, - top: top + height / 2, - }) - if (!eventPos) return - - const $cellPos = cellAround(view.state.doc.resolve(eventPos.pos)) + // Resolve directly from the cell DOM rather than via coords. The previous + // center-coords approach broke on tall merged cells — their visual center + // can land in empty space whose closest PM position resolves to an + // adjacent cell. `posAtDOM(td, 0)` is always inside this cell, regardless + // of rowspan/colspan. + let pos: number + try { + pos = view.posAtDOM(domCell, 0) + } catch { + return + } + const $cellPos = cellAround(view.state.doc.resolve(pos)) if (!$cellPos) return + return cellInfoFromResolvedCell($cellPos) +} + +/** + * Build HoveringCellInfo from a resolved position whose parent is a + * table cell (i.e. the result of `cellAround` on some inner position). + */ +export function cellInfoFromResolvedCell( + $cellPos: ResolvedPos, +): HoveringCellInfo { const map = TableMap.get($cellPos.node(-1)) const tableStart = $cellPos.start(-1) const cellRect = map.findCell($cellPos.pos - tableStart) diff --git a/packages/editor-ext/src/lib/table/header-pin/controller.ts b/packages/editor-ext/src/lib/table/header-pin/controller.ts new file mode 100644 index 000000000..318d4145d --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/controller.ts @@ -0,0 +1,186 @@ +// Per-table header-pin controller: native sticky when table fits its wrapper, transform fallback when it doesn't. + +import { computePinTop, pinOffsetWatcher } from './offset'; + +const WRAPPER_NO_OVERFLOW = 'tableWrapperNoOverflow'; +const HEADER_PINNED = 'tableHeaderPinned'; +const PIN_OFFSET_VAR = '--table-pin-offset'; + +type PinMode = 'off' | 'native' | 'fallback'; + +function firstRowIsAllHeaders(row: HTMLTableRowElement | null): boolean { + if (!row) return false; + const cells = Array.from(row.cells); + return cells.length > 0 && cells.every((c) => c.tagName === 'TH'); +} + +function isNestedTable(wrapper: HTMLElement): boolean { + return wrapper.closest('table .tableWrapper') !== null; +} + +function isLayoutInert(rect: DOMRectReadOnly): boolean { + return rect.width === 0 && rect.height === 0; +} + +const fallbackControllers = new Set(); +let fallbackScrollListener: (() => void) | null = null; +let fallbackRafPending = false; + +function ensureFallbackListener() { + if (fallbackScrollListener) return; + fallbackScrollListener = () => { + if (fallbackRafPending) return; + fallbackRafPending = true; + requestAnimationFrame(() => { + fallbackRafPending = false; + for (const ctrl of fallbackControllers) ctrl.updateFallbackOffset(); + }); + }; + document.addEventListener('scroll', fallbackScrollListener, { + passive: true, + capture: true, + }); +} + +function maybeTeardownFallbackListener() { + if (!fallbackScrollListener || fallbackControllers.size > 0) return; + document.removeEventListener('scroll', fallbackScrollListener, { + capture: true, + }); + fallbackScrollListener = null; + fallbackRafPending = false; +} + +export class TablePinController { + private wrapper: HTMLElement; + private table: HTMLTableElement; + private fitsObserver?: IntersectionObserver; + private mode: PinMode = 'off'; + private cachedHeaderRow: HTMLTableRowElement | null = null; + + constructor(wrapper: HTMLElement, table: HTMLTableElement) { + this.wrapper = wrapper; + this.table = table; + pinOffsetWatcher.acquire(); + this.fitsObserver = new IntersectionObserver( + (entries) => { + for (const entry of entries) this.evaluateFit(entry); + }, + { root: this.wrapper, threshold: 1 }, + ); + this.fitsObserver.observe(this.table); + } + + private getHeaderRow(): HTMLTableRowElement | null { + if (this.cachedHeaderRow && this.table.contains(this.cachedHeaderRow)) { + return this.cachedHeaderRow; + } + this.cachedHeaderRow = this.table.querySelector('tr'); + return this.cachedHeaderRow; + } + + private evaluateFit(entry: IntersectionObserverEntry) { + if (!this.isEligible()) { + this.apply('off'); + return; + } + if (isLayoutInert(entry.boundingClientRect)) return; + this.apply(entry.isIntersecting ? 'native' : 'fallback'); + } + + private isEligible(): boolean { + return ( + !isNestedTable(this.wrapper) && firstRowIsAllHeaders(this.getHeaderRow()) + ); + } + + private apply(next: PinMode) { + if (next === this.mode) return; + + if (this.mode === 'fallback' && next !== 'fallback') { + fallbackControllers.delete(this); + maybeTeardownFallbackListener(); + } + + this.mode = next; + const cls = this.wrapper.classList; + + if (next === 'off') { + cls.remove(HEADER_PINNED); + cls.remove(WRAPPER_NO_OVERFLOW); + this.wrapper.style.removeProperty(PIN_OFFSET_VAR); + } else if (next === 'native') { + cls.add(HEADER_PINNED); + cls.add(WRAPPER_NO_OVERFLOW); + // Native mode reads --editor-pin-offset from :root; clear stale per-wrapper var from fallback. + this.wrapper.style.removeProperty(PIN_OFFSET_VAR); + } else if (next === 'fallback') { + cls.add(HEADER_PINNED); + cls.remove(WRAPPER_NO_OVERFLOW); + fallbackControllers.add(this); + ensureFallbackListener(); + // Avoid one stale-frame paint under translateY. + this.updateFallbackOffset(); + } + } + + updateFallbackOffset() { + const pinTop = computePinTop(); + const tableRect = this.table.getBoundingClientRect(); + const headerRow = this.getHeaderRow(); + if (!headerRow) return; + const rowHeight = headerRow.getBoundingClientRect().height; + + const active = tableRect.top < pinTop && tableRect.bottom > pinTop + rowHeight; + + if (active) { + const offset = Math.min(pinTop - tableRect.top, tableRect.height - rowHeight); + this.wrapper.style.setProperty(PIN_OFFSET_VAR, `${offset}px`); + } else { + this.wrapper.style.removeProperty(PIN_OFFSET_VAR); + } + } + + refresh() { + // The header may have been replaced by a PM transaction; drop + // the cached reference before checking eligibility. + this.cachedHeaderRow = null; + if (!this.isEligible()) { + this.apply('off'); + return; + } + if (this.mode === 'off') { + // Eligibility just flipped back on; re-trigger the observer so it + // emits the current intersection state. + this.fitsObserver?.unobserve(this.table); + this.fitsObserver?.observe(this.table); + } + } + + destroy() { + this.fitsObserver?.disconnect(); + this.fitsObserver = undefined; + this.apply('off'); + pinOffsetWatcher.release(); + } +} + +const controllers = new WeakMap(); + +export function attach(wrapper: HTMLElement) { + if (controllers.has(wrapper)) return; + const table = wrapper.querySelector(':scope > table') as HTMLTableElement | null; + if (!table) return; + controllers.set(wrapper, new TablePinController(wrapper, table)); +} + +export function detach(wrapper: HTMLElement) { + const ctrl = controllers.get(wrapper); + if (!ctrl) return; + ctrl.destroy(); + controllers.delete(wrapper); +} + +export function getController(wrapper: HTMLElement): TablePinController | undefined { + return controllers.get(wrapper); +} diff --git a/packages/editor-ext/src/lib/table/header-pin/extension.ts b/packages/editor-ext/src/lib/table/header-pin/extension.ts new file mode 100644 index 000000000..8e5157ede --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/extension.ts @@ -0,0 +1,78 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +import { attach, detach, getController } from './controller'; + +const tableHeaderPinKey = new PluginKey('tableHeaderPin'); + +export const TableHeaderPin = Extension.create({ + name: 'tableHeaderPin', + + addProseMirrorPlugins() { + let editorRoot: HTMLElement | null = null; + let domObserver: MutationObserver | null = null; + const tracked = new Set(); + let rafHandle: number | null = null; + + const reconcile = () => { + rafHandle = null; + if (!editorRoot) return; + const current = new Set( + editorRoot.querySelectorAll('.tableWrapper'), + ); + for (const w of tracked) { + if (!current.has(w)) { + detach(w); + tracked.delete(w); + } + } + for (const w of current) { + if (!tracked.has(w)) { + attach(w); + tracked.add(w); + } + } + }; + + const schedule = () => { + if (rafHandle !== null) return; + rafHandle = requestAnimationFrame(reconcile); + }; + + return [ + new Plugin({ + key: tableHeaderPinKey, + + view(editorView) { + editorRoot = editorView.dom as HTMLElement; + + schedule(); + + domObserver = new MutationObserver(schedule); + domObserver.observe(editorRoot, { subtree: true, childList: true }); + + return { + update(view, prevState) { + if (!editorRoot) return; + if (view.state.doc === prevState.doc) return; + editorRoot + .querySelectorAll('.tableWrapper') + .forEach((w) => getController(w)?.refresh()); + }, + destroy() { + if (rafHandle !== null) { + cancelAnimationFrame(rafHandle); + rafHandle = null; + } + domObserver?.disconnect(); + domObserver = null; + for (const w of tracked) detach(w); + tracked.clear(); + editorRoot = null; + }, + }; + }, + }), + ]; + }, +}); diff --git a/packages/editor-ext/src/lib/table/header-pin/index.ts b/packages/editor-ext/src/lib/table/header-pin/index.ts new file mode 100644 index 000000000..b45e01aee --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/index.ts @@ -0,0 +1 @@ +export { TableHeaderPin } from './extension'; diff --git a/packages/editor-ext/src/lib/table/header-pin/offset.ts b/packages/editor-ext/src/lib/table/header-pin/offset.ts new file mode 100644 index 000000000..89cc6bf9e --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/offset.ts @@ -0,0 +1,65 @@ +// Pin-offset measurement and watcher used by the table header-pin controller. + +// Fallback app-bar height (px) when no fixed surface is mounted; matches global-app-shell.tsx. +const APP_BAR_FALLBACK_HEIGHT = 45; + +export const EDITOR_PIN_OFFSET_VAR = '--editor-pin-offset'; + +// Selectors for fixed surfaces between viewport top and editor content. Use data attributes — +// CSS module classes are build-time hashed and won't match. +const PIN_ANCHOR_SELECTORS = [ + '[data-page-header]', + '[data-fixed-toolbar]', +] as const; + +export function computePinTop(): number { + let bottom = APP_BAR_FALLBACK_HEIGHT; + for (const sel of PIN_ANCHOR_SELECTORS) { + const el = document.querySelector(sel) as HTMLElement | null; + if (!el) continue; + const rect = el.getBoundingClientRect(); + if (rect.height > 0 && rect.bottom > bottom) bottom = rect.bottom; + } + return bottom; +} + +// Reference-counted watcher that publishes the editor's top offset to a CSS custom property. +export const pinOffsetWatcher = { + refs: 0, + resizeObserver: null as ResizeObserver | null, + rafPending: false, + lastValue: -1, + + acquire() { + if (this.refs++ > 0) return; + this.publish(); + const schedule = () => { + if (this.rafPending) return; + this.rafPending = true; + requestAnimationFrame(() => { + this.rafPending = false; + this.publish(); + }); + }; + this.resizeObserver = new ResizeObserver(schedule); + this.resizeObserver.observe(document.body); + }, + + release() { + if (--this.refs > 0) return; + this.resizeObserver?.disconnect(); + this.resizeObserver = null; + document.documentElement.style.removeProperty(EDITOR_PIN_OFFSET_VAR); + this.lastValue = -1; + }, + + publish() { + const top = computePinTop(); + if (top === this.lastValue) return; + this.lastValue = top; + document.documentElement.style.setProperty( + EDITOR_PIN_OFFSET_VAR, + `${top}px`, + ); + }, +}; diff --git a/packages/editor-ext/src/lib/table/index.ts b/packages/editor-ext/src/lib/table/index.ts index 9e5a92651..ed06582e3 100644 --- a/packages/editor-ext/src/lib/table/index.ts +++ b/packages/editor-ext/src/lib/table/index.ts @@ -2,4 +2,14 @@ export * from "./row"; export * from "./cell"; export * from "./header"; export * from "./table"; -export * from "./dnd"; \ No newline at end of file +export * from "./dnd"; +export * from "./table-view"; +export * from "./header-pin"; +export * from "./table-readonly-sort"; +export { moveColumn } from "./utils/move-column"; +export type { MoveColumnParams } from "./utils/move-column"; +export { moveRow } from "./utils/move-row"; +export type { MoveRowParams } from "./utils/move-row"; +export { convertTableNodeToArrayOfRows } from "./utils/convert-table-node-to-array-of-rows"; +export { convertArrayOfRowsToTableNode } from "./utils/convert-array-of-rows-to-table-node"; +export { transpose } from "./utils/transpose"; diff --git a/packages/editor-ext/src/lib/table/table-readonly-sort.ts b/packages/editor-ext/src/lib/table/table-readonly-sort.ts new file mode 100644 index 000000000..3e246a411 --- /dev/null +++ b/packages/editor-ext/src/lib/table/table-readonly-sort.ts @@ -0,0 +1,233 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +type SortDirection = 'asc' | 'desc'; + +type SortState = { + col: number; + direction: SortDirection; +}; + +const CHEVRON_CLASS = 'tableReadonlySortChevron'; + +const tableReadonlySortKey = new PluginKey('tableReadonlySort'); + +const sortStates = new WeakMap(); +const originalOrders = new WeakMap(); + +const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }); + +function getColumnIndex(th: HTMLTableCellElement): number { + const row = th.parentElement as HTMLTableRowElement; + if (!row) return -1; + let col = 0; + for (let i = 0; i < row.cells.length; i++) { + if (row.cells[i] === th) return col; + col += row.cells[i].colSpan ?? 1; + } + return -1; +} + +function getHeaderTh(target: EventTarget | null): HTMLTableCellElement | null { + if (!(target instanceof Element)) return null; + const th = target.closest('th') as HTMLTableCellElement | null; + if (!th) return null; + const row = th.parentElement; + if (!row) return null; + const tbody = row.parentElement; + if (!tbody) return null; + const table = tbody.closest('table'); + if (!table) return null; + + // th must be in the first row of the table (could be in thead or tbody) + const firstRow = table.querySelector('tr'); + if (firstRow !== row) return null; + + return th; +} + +function getCellText(row: HTMLTableRowElement, colIndex: number): string { + let col = 0; + for (let i = 0; i < row.cells.length; i++) { + if (col === colIndex) return row.cells[i].textContent?.trim() ?? ''; + col += row.cells[i].colSpan ?? 1; + } + return ''; +} + +function getOrSaveOriginalOrder( + table: HTMLTableElement, + dataRows: HTMLTableRowElement[], +): HTMLTableRowElement[] { + if (!originalOrders.has(table)) { + originalOrders.set(table, [...dataRows]); + } + return originalOrders.get(table)!; +} + +function sortDataRows( + dataRows: HTMLTableRowElement[], + colIndex: number, + direction: SortDirection, +): HTMLTableRowElement[] { + return [...dataRows].sort((a, b) => { + const textA = getCellText(a, colIndex); + const textB = getCellText(b, colIndex); + const emptyA = textA === ''; + const emptyB = textB === ''; + if (emptyA && emptyB) return 0; + if (emptyA) return 1; + if (emptyB) return -1; + const cmp = collator.compare(textA, textB); + return direction === 'asc' ? cmp : -cmp; + }); +} + +function applySort(table: HTMLTableElement, colIndex: number): void { + const tbody = table.querySelector('tbody'); + if (!tbody) return; + + const allRows = Array.from(tbody.querySelectorAll(':scope > tr')); + if (allRows.length === 0) return; + + const headerRow = allRows[0]; + const dataRows = allRows.slice(1); + if (dataRows.length === 0) return; + + const current = sortStates.get(table) ?? null; + const saved = getOrSaveOriginalOrder(table, dataRows); + + let next: SortState | null; + if (!current || current.col !== colIndex) { + next = { col: colIndex, direction: 'asc' }; + } else if (current.direction === 'asc') { + next = { col: colIndex, direction: 'desc' }; + } else { + next = null; + } + + if (next === null) { + sortStates.delete(table); + tbody.append(headerRow, ...saved); + } else { + sortStates.set(table, next); + const sorted = sortDataRows(saved, next.col, next.direction); + tbody.append(headerRow, ...sorted); + } + + updateChevrons(table); +} + +const CHEVRON_SVG = + ''; + +function ensureChevron(th: HTMLTableCellElement): HTMLSpanElement { + let chevron = th.querySelector(`.${CHEVRON_CLASS}`); + if (!chevron) { + chevron = document.createElement('span'); + chevron.className = CHEVRON_CLASS; + chevron.setAttribute('aria-hidden', 'true'); + chevron.innerHTML = CHEVRON_SVG; + th.appendChild(chevron); + } + return chevron; +} + +function updateChevrons(table: HTMLTableElement): void { + const firstRow = table.querySelector('tr'); + if (!firstRow) return; + + const state = sortStates.get(table) ?? null; + let col = 0; + for (let i = 0; i < firstRow.cells.length; i++) { + const cell = firstRow.cells[i]; + if (cell.tagName !== 'TH') { + col += cell.colSpan ?? 1; + continue; + } + const chevron = ensureChevron(cell as HTMLTableCellElement); + let label: string; + if (state && state.col === col) { + chevron.setAttribute('data-sort', state.direction); + label = state.direction === 'asc' ? 'Sort descending' : 'Clear sort'; + } else { + chevron.removeAttribute('data-sort'); + label = 'Sort ascending'; + } + chevron.setAttribute('data-tooltip', label); + chevron.setAttribute('aria-label', label); + chevron.title = label; + col += cell.colSpan ?? 1; + } +} + +function addChevronsToAllTables(editorRoot: HTMLElement): void { + const tables = editorRoot.querySelectorAll('table'); + tables.forEach((table) => updateChevrons(table)); +} + +function removeAllChevrons(editorRoot: HTMLElement): void { + editorRoot + .querySelectorAll(`.${CHEVRON_CLASS}`) + .forEach((el) => el.remove()); +} + +export const TableReadonlySort = Extension.create({ + name: 'tableReadonlySort', + + addProseMirrorPlugins() { + const editor = this.editor; + let editorRoot: HTMLElement | null = null; + + const onClick = (event: MouseEvent) => { + if (editor.isEditable) return; + // Only react to clicks on the chevron, not anywhere else in the header + // cell. This lets the user click into a header to select text without + // accidentally triggering a sort. + if (!(event.target instanceof Element)) return; + const chevron = event.target.closest(`.${CHEVRON_CLASS}`); + if (!chevron) return; + const th = getHeaderTh(chevron); + if (!th) return; + const table = th.closest('table') as HTMLTableElement | null; + if (!table) return; + const colIndex = getColumnIndex(th); + if (colIndex < 0) return; + applySort(table, colIndex); + }; + + return [ + new Plugin({ + key: tableReadonlySortKey, + + view(editorView) { + editorRoot = editorView.dom as HTMLElement; + editorRoot.addEventListener('click', onClick); + + if (!editor.isEditable) { + addChevronsToAllTables(editorRoot); + } + + return { + update(view) { + const root = view.dom as HTMLElement; + if (!editor.isEditable) { + addChevronsToAllTables(root); + } else { + removeAllChevrons(root); + } + }, + destroy() { + if (editorRoot) { + editorRoot.removeEventListener('click', onClick); + removeAllChevrons(editorRoot); + } + }, + }; + }, + }), + ]; + }, +}); diff --git a/packages/editor-ext/src/lib/table/table-view.ts b/packages/editor-ext/src/lib/table/table-view.ts new file mode 100644 index 000000000..7e410918e --- /dev/null +++ b/packages/editor-ext/src/lib/table/table-view.ts @@ -0,0 +1,158 @@ +import type { Node as ProseMirrorNode } from '@tiptap/pm/model'; +import type { NodeView, ViewMutationRecord } from '@tiptap/pm/view'; +import { getColStyleDeclaration } from './utils/col-style'; + +export function updateColumns( + node: ProseMirrorNode, + colgroup: HTMLElement, + table: HTMLTableElement, + cellMinWidth: number, + overrideCol?: number, + overrideValue?: number, +) { + let totalWidth = 0; + let fixedWidth = true; + let nextDOM = colgroup.firstChild; + const row = node.firstChild; + + if (row !== null) { + for (let i = 0, col = 0; i < row.childCount; i += 1) { + const { colspan, colwidth } = row.child(i).attrs; + + for (let j = 0; j < colspan; j += 1, col += 1) { + const hasWidth = + overrideCol === col + ? overrideValue + : ((colwidth && colwidth[j]) as number | undefined); + const cssWidth = hasWidth ? `${hasWidth}px` : ''; + + totalWidth += hasWidth || cellMinWidth; + + if (!hasWidth) { + fixedWidth = false; + } + + if (!nextDOM) { + const colElement = document.createElement('col'); + + const [propertyKey, propertyValue] = getColStyleDeclaration( + cellMinWidth, + hasWidth, + ); + + colElement.style.setProperty(propertyKey, propertyValue); + + colgroup.appendChild(colElement); + } else { + if ((nextDOM as HTMLTableColElement).style.width !== cssWidth) { + const [propertyKey, propertyValue] = getColStyleDeclaration( + cellMinWidth, + hasWidth, + ); + + (nextDOM as HTMLTableColElement).style.setProperty( + propertyKey, + propertyValue, + ); + } + + nextDOM = nextDOM.nextSibling; + } + } + } + } + + while (nextDOM) { + const after = nextDOM.nextSibling; + + nextDOM.parentNode?.removeChild(nextDOM); + nextDOM = after; + } + + const hasUserWidth = + node.attrs.style && + typeof node.attrs.style === 'string' && + /\bwidth\s*:/i.test(node.attrs.style); + + if (fixedWidth && !hasUserWidth) { + table.style.width = `${totalWidth}px`; + table.style.minWidth = ''; + } else { + table.style.width = ''; + table.style.minWidth = `${totalWidth}px`; + } +} + +export class TableView implements NodeView { + node: ProseMirrorNode; + + cellMinWidth: number; + + dom: HTMLDivElement; + + table: HTMLTableElement; + + colgroup: HTMLTableColElement; + + contentDOM: HTMLTableSectionElement; + + constructor(node: ProseMirrorNode, cellMinWidth: number) { + this.node = node; + this.cellMinWidth = cellMinWidth; + this.dom = document.createElement('div'); + this.dom.className = 'tableWrapper'; + this.table = this.dom.appendChild(document.createElement('table')); + + if (node.attrs.style) { + this.table.style.cssText = node.attrs.style; + } + + this.colgroup = this.table.appendChild(document.createElement('colgroup')); + updateColumns(node, this.colgroup, this.table, cellMinWidth); + this.contentDOM = this.table.appendChild(document.createElement('tbody')); + } + + update(node: ProseMirrorNode) { + if (node.type !== this.node.type) return false; + + this.node = node; + updateColumns(node, this.colgroup, this.table, this.cellMinWidth); + + return true; + } + + ignoreMutation(mutation: ViewMutationRecord) { + const target = mutation.target as Node; + const isInsideWrapper = this.dom.contains(target); + const isInsideContent = this.contentDOM.contains(target); + + if (isInsideWrapper && !isInsideContent) { + if ( + mutation.type === 'attributes' || + mutation.type === 'childList' || + mutation.type === 'characterData' + ) { + return true; + } + } + + // Chevron span (.tableReadonlySortChevron) added/removed by sort plugin. + if (mutation.type === 'childList') { + const nodes = [ + ...Array.from(mutation.addedNodes), + ...Array.from(mutation.removedNodes), + ]; + if ( + nodes.some( + (n) => + n instanceof Element && + n.classList.contains('tableReadonlySortChevron'), + ) + ) { + return true; + } + } + + return false; + } +} diff --git a/packages/editor-ext/src/lib/table/table.ts b/packages/editor-ext/src/lib/table/table.ts index f1436c28d..e87048a46 100644 --- a/packages/editor-ext/src/lib/table/table.ts +++ b/packages/editor-ext/src/lib/table/table.ts @@ -1,6 +1,8 @@ import { Table } from "@tiptap/extension-table"; import { Editor } from "@tiptap/core"; import { DOMOutputSpec } from "@tiptap/pm/model"; +import { TextSelection } from "@tiptap/pm/state"; +import { cellAround } from "@tiptap/pm/tables"; const LIST_TYPES = ["bulletList", "orderedList", "taskList"]; @@ -32,9 +34,36 @@ function handleListOutdent(editor: Editor): boolean { } export const CustomTable = Table.extend({ + addKeyboardShortcuts() { return { ...this.parent?.(), + "Mod-a": () => { + const { state, view } = this.editor; + const { selection, doc } = state; + + const $cellPos = cellAround(selection.$anchor); + if (!$cellPos) return false; + + const cellNode = doc.nodeAt($cellPos.pos); + // Empty cells have nothing useful to scope to — let the default + // Mod-a fall through and select the whole doc. + if (!cellNode || !cellNode.textContent) return false; + + const from = $cellPos.pos + 1; + const to = $cellPos.pos + cellNode.nodeSize - 1; + if (from >= to) return true; + + const nextSel = TextSelection.between( + doc.resolve(from), + doc.resolve(to), + 1, + ); + if (!nextSel || selection.eq(nextSel)) return true; + + view.dispatch(state.tr.setSelection(nextSel)); + return true; + }, Tab: () => { // If we're in a list within a table, handle list indentation if (isInList(this.editor) && this.editor.isActive("table")) { diff --git a/packages/editor-ext/src/lib/table/utils/col-style.ts b/packages/editor-ext/src/lib/table/utils/col-style.ts new file mode 100644 index 000000000..8060962fd --- /dev/null +++ b/packages/editor-ext/src/lib/table/utils/col-style.ts @@ -0,0 +1,7 @@ +export function getColStyleDeclaration(minWidth: number, width: number | undefined): [string, string] { + if (width) { + return ['width', `${Math.max(width, minWidth)}px`] + } + + return ['min-width', `${minWidth}px`] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a6dd37f6..f4a628064 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,22 +248,22 @@ importers: apps/client: dependencies: '@atlaskit/pragmatic-drag-and-drop': - specifier: ^1.8.1 + specifier: 1.8.1 version: 1.8.1 '@atlaskit/pragmatic-drag-and-drop-auto-scroll': - specifier: ^2.1.0 + specifier: 2.1.5 version: 2.1.5 '@atlaskit/pragmatic-drag-and-drop-flourish': - specifier: ^2.0.15 + specifier: 2.0.15 version: 2.0.15(react@18.3.1) '@atlaskit/pragmatic-drag-and-drop-hitbox': - specifier: ^1.1.0 + specifier: 1.1.0 version: 1.1.0 '@atlaskit/pragmatic-drag-and-drop-live-region': - specifier: ^1.3.4 + specifier: 1.3.4 version: 1.3.4 '@casl/react': - specifier: ^5.0.1 + specifier: 5.0.1 version: 5.0.1(@casl/ability@6.8.0)(react@18.3.1) '@docmost/editor-ext': specifier: workspace:* @@ -272,37 +272,37 @@ importers: specifier: 0.18.0-3a5ef40 version: 0.18.0-3a5ef40(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/core': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/dates': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(dayjs@1.11.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/form': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(react@18.3.1) '@mantine/hooks': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(react@18.3.1) '@mantine/modals': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/notifications': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/spotlight': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@slidoapp/emoji-mart': - specifier: ^5.8.7 + specifier: 5.8.7 version: 5.8.7 '@slidoapp/emoji-mart-data': - specifier: ^1.2.4 + specifier: 1.2.4 version: 1.2.4 '@slidoapp/emoji-mart-react': - specifier: ^1.1.5 + specifier: 1.1.5 version: 1.1.5(@slidoapp/emoji-mart@5.8.7)(react@18.3.1) '@tabler/icons-react': - specifier: ^3.40.0 + specifier: 3.40.0 version: 3.40.0(react@18.3.1) '@tanstack/react-query': specifier: 5.90.17 @@ -311,22 +311,22 @@ importers: specifier: 3.13.24 version: 3.13.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) alfaaz: - specifier: ^1.1.0 + specifier: 1.1.0 version: 1.1.0 axios: specifier: 1.16.0 version: 1.16.0 blueimp-load-image: - specifier: ^5.16.0 + specifier: 5.16.0 version: 5.16.0 clsx: - specifier: ^2.1.1 + specifier: 2.1.1 version: 2.1.1 file-saver: - specifier: ^2.0.5 + specifier: 2.0.5 version: 2.0.5 highlightjs-sap-abap: - specifier: ^0.3.0 + specifier: 0.3.0 version: 0.3.0 i18next: specifier: 25.10.1 @@ -335,37 +335,37 @@ importers: specifier: 3.0.6 version: 3.0.6 jotai: - specifier: ^2.18.1 + specifier: 2.18.1 version: 2.18.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.12)(react@18.3.1) jotai-optics: - specifier: ^0.4.0 + specifier: 0.4.0 version: 0.4.0(jotai@2.18.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.12)(react@18.3.1))(optics-ts@2.4.1) js-cookie: - specifier: ^3.0.5 + specifier: 3.0.5 version: 3.0.5 jwt-decode: - specifier: ^4.0.0 + specifier: 4.0.0 version: 4.0.0 katex: specifier: 0.16.40 version: 0.16.40 lowlight: - specifier: ^3.3.0 + specifier: 3.3.0 version: 3.3.0 mantine-form-zod-resolver: - specifier: ^1.3.0 + specifier: 1.3.0 version: 1.3.0(@mantine/form@8.3.18(react@18.3.1))(zod@4.3.6) mermaid: specifier: 11.13.0 version: 11.13.0 mitt: - specifier: ^3.0.1 + specifier: 3.0.1 version: 3.0.1 posthog-js: specifier: 1.372.2 version: 1.372.2 react: - specifier: ^18.3.1 + specifier: 18.3.1 version: 18.3.1 react-clear-modal: specifier: ^2.0.18 @@ -374,111 +374,111 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-drawio: - specifier: ^1.0.7 + specifier: 1.0.7 version: 1.0.7(react@18.3.1) react-error-boundary: - specifier: ^6.1.1 + specifier: 6.1.1 version: 6.1.1(react@18.3.1) react-helmet-async: - specifier: ^3.0.0 + specifier: 3.0.0 version: 3.0.0(react@18.3.1) react-i18next: specifier: 16.5.8 version: 16.5.8(i18next@25.10.1(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) react-router-dom: - specifier: ^7.13.1 + specifier: 7.13.1 version: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: - specifier: ^7.7.4 + specifier: 7.7.4 version: 7.7.4 socket.io-client: - specifier: ^4.8.3 + specifier: 4.8.3 version: 4.8.3 zod: - specifier: ^4.3.6 + specifier: 4.3.6 version: 4.3.6 devDependencies: '@eslint/js': - specifier: ^9.28.0 - version: 9.39.4 + specifier: 9.28.0 + version: 9.28.0 '@tanstack/eslint-plugin-query': - specifier: ^5.94.4 - version: 5.94.4(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) + specifier: 5.94.4 + version: 5.94.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) '@testing-library/jest-dom': - specifier: ^6.6.0 - version: 6.9.1 + specifier: 6.6.0 + version: 6.6.0 '@testing-library/react': - specifier: ^16.1.0 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 16.1.0 + version: 16.1.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/blueimp-load-image': - specifier: ^5.16.6 + specifier: 5.16.6 version: 5.16.6 '@types/file-saver': - specifier: ^2.0.7 + specifier: 2.0.7 version: 2.0.7 '@types/js-cookie': - specifier: ^3.0.6 + specifier: 3.0.6 version: 3.0.6 '@types/katex': - specifier: ^0.16.8 + specifier: 0.16.8 version: 0.16.8 '@types/node': specifier: 22.19.1 version: 22.19.1 '@types/react': - specifier: ^18.3.12 + specifier: 18.3.12 version: 18.3.12 '@types/react-dom': - specifier: ^18.3.1 + specifier: 18.3.1 version: 18.3.1 '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 6.0.1 + version: 6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) eslint: - specifier: ^9.28.0 - version: 9.39.4(jiti@2.4.2) + specifier: 9.28.0 + version: 9.28.0(jiti@2.4.2) eslint-plugin-react: - specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.4(jiti@2.4.2)) + specifier: 7.37.5 + version: 7.37.5(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-react-hooks: - specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.4(jiti@2.4.2)) + specifier: 7.0.1 + version: 7.0.1(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-react-refresh: - specifier: ^0.5.2 - version: 0.5.2(eslint@9.39.4(jiti@2.4.2)) + specifier: 0.5.2 + version: 0.5.2(eslint@9.28.0(jiti@2.4.2)) globals: - specifier: ^15.13.0 + specifier: 15.13.0 version: 15.13.0 jsdom: - specifier: ^25.0.0 - version: 25.0.1 + specifier: 25.0.0 + version: 25.0.0 optics-ts: - specifier: ^2.4.1 + specifier: 2.4.1 version: 2.4.1 postcss: - specifier: ^8.5.12 - version: 8.5.12 + specifier: 8.5.14 + version: 8.5.14 postcss-preset-mantine: - specifier: ^1.18.0 - version: 1.18.0(postcss@8.5.12) + specifier: 1.18.0 + version: 1.18.0(postcss@8.5.14) postcss-simple-vars: - specifier: ^7.0.1 - version: 7.0.1(postcss@8.5.12) + specifier: 7.0.1 + version: 7.0.1(postcss@8.5.14) prettier: - specifier: ^3.8.1 + specifier: 3.8.1 version: 3.8.1 typescript: - specifier: ^5.9.3 + specifier: 5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.57.1 - version: 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) + specifier: 8.57.1 + version: 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) vite: specifier: 8.0.5 - version: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + version: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) vitest: - specifier: ^4.1.6 - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.6 + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) apps/server: dependencies: @@ -2237,14 +2237,30 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/config-array@0.20.1': + resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.21.2': resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.2.3': + resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.4.2': resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.14.0': + resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.17.0': resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2253,6 +2269,10 @@ packages: resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.28.0': + resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.39.4': resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2261,6 +2281,10 @@ packages: resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.4.1': resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4463,12 +4487,12 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + '@testing-library/jest-dom@6.6.0': + resolution: {integrity: sha512-Y76dmd7C85xekWqylJqRmO6lr83cdVprTs0muSvkXr6M73auYK5OvZMc3tKe1F7wMFdzfeBCwVbkoGrRKWb+fg==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + '@testing-library/react@16.1.0': + resolution: {integrity: sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -5540,10 +5564,6 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - array-buffer-byte-length@1.0.1: - resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} - engines: {node: '>= 0.4'} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -5571,10 +5591,6 @@ packages: resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} engines: {node: '>= 0.4'} - arraybuffer.prototype.slice@1.0.3: - resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} - engines: {node: '>= 0.4'} - arraybuffer.prototype.slice@1.0.4: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} @@ -5787,10 +5803,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.7: - resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} - engines: {node: '>= 0.4'} - call-bind@1.0.8: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} @@ -5825,6 +5837,10 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -6135,9 +6151,6 @@ packages: resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} engines: {node: '>=18'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -6301,26 +6314,14 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} - data-view-buffer@1.0.1: - resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} - engines: {node: '>= 0.4'} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} - data-view-byte-length@1.0.1: - resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} - engines: {node: '>= 0.4'} - data-view-byte-length@1.0.2: resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} engines: {node: '>= 0.4'} - data-view-byte-offset@1.0.0: - resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} - engines: {node: '>= 0.4'} - data-view-byte-offset@1.0.1: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} @@ -6606,10 +6607,6 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - es-abstract@1.23.5: - resolution: {integrity: sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==} - engines: {node: '>= 0.4'} - es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -6626,9 +6623,6 @@ packages: resolution: {integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==} engines: {node: '>= 0.4'} - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} @@ -6727,6 +6721,16 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.28.0: + resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6954,9 +6958,6 @@ packages: debug: optional: true - for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -7016,10 +7017,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.1.6: - resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} - engines: {node: '>= 0.4'} - function.prototype.name@1.1.8: resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} engines: {node: '>= 0.4'} @@ -7059,10 +7056,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-symbol-description@1.0.2: - resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} - engines: {node: '>= 0.4'} - get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -7137,10 +7130,6 @@ packages: has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - has-proto@1.0.3: - resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} - engines: {node: '>= 0.4'} - has-proto@1.2.0: resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} engines: {node: '>= 0.4'} @@ -7292,10 +7281,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - internal-slot@1.0.7: - resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} - engines: {node: '>= 0.4'} - internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -7326,10 +7311,6 @@ packages: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} - is-array-buffer@3.0.4: - resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} - engines: {node: '>= 0.4'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -7349,10 +7330,6 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-boolean-object@1.2.0: - resolution: {integrity: sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw==} - engines: {node: '>= 0.4'} - is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -7364,18 +7341,10 @@ packages: is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - is-data-view@1.0.1: - resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} - engines: {node: '>= 0.4'} - is-data-view@1.0.2: resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} engines: {node: '>= 0.4'} - is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - is-date-object@1.1.0: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} @@ -7421,10 +7390,6 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} - is-number-object@1.1.0: - resolution: {integrity: sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw==} - engines: {node: '>= 0.4'} - is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -7439,10 +7404,6 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.0: - resolution: {integrity: sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA==} - engines: {node: '>= 0.4'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -7451,10 +7412,6 @@ packages: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} - is-shared-array-buffer@1.0.3: - resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} - engines: {node: '>= 0.4'} - is-shared-array-buffer@1.0.4: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} @@ -7463,26 +7420,14 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-string@1.1.0: - resolution: {integrity: sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g==} - engines: {node: '>= 0.4'} - is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} - is-symbol@1.1.0: - resolution: {integrity: sha512-qS8KkNNXUZ/I+nX6QT8ZS1/Yx0A444yhzdTKxCzKkNjQ9sHErBxJnJAgh+f5YhusYECEcjo4XcyH87hn6+ks0A==} - engines: {node: '>= 0.4'} - is-symbol@1.1.1: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} - is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} - is-typed-array@1.1.15: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} @@ -7499,9 +7444,6 @@ packages: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} - is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - is-weakref@1.1.1: resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} engines: {node: '>= 0.4'} @@ -7785,8 +7727,8 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@25.0.1: - resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + jsdom@25.0.0: + resolution: {integrity: sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==} engines: {node: '>=18'} peerDependencies: canvas: ^2.11.2 @@ -8555,10 +8497,6 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - object.assign@4.1.5: - resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} - engines: {node: '>= 0.4'} - object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -8948,8 +8886,8 @@ packages: peerDependencies: postcss: ^8.2.1 - postcss@8.5.12: - resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -9095,6 +9033,9 @@ packages: prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -9124,6 +9065,9 @@ packages: query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -9324,10 +9268,6 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - reflect.getprototypeof@1.0.7: - resolution: {integrity: sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g==} - engines: {node: '>= 0.4'} - regenerate-unicode-properties@10.1.1: resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} engines: {node: '>=4'} @@ -9338,10 +9278,6 @@ packages: regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} - regexp.prototype.flags@1.5.3: - resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} - engines: {node: '>= 0.4'} - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -9365,6 +9301,9 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -9446,10 +9385,6 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-array-concat@1.1.2: - resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} - engines: {node: '>=0.4'} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -9464,10 +9399,6 @@ packages: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} - safe-regex-test@1.0.3: - resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} - engines: {node: '>= 0.4'} - safe-regex-test@1.1.0: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} @@ -9710,13 +9641,6 @@ packages: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} - string.prototype.trim@1.2.9: - resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.8: - resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} - string.prototype.trimend@1.0.9: resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} engines: {node: '>= 0.4'} @@ -9920,6 +9844,10 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -10046,26 +9974,14 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typed-array-buffer@1.0.2: - resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} - engines: {node: '>= 0.4'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} - typed-array-byte-length@1.0.1: - resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} - engines: {node: '>= 0.4'} - typed-array-byte-length@1.0.3: resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.3: - resolution: {integrity: sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==} - engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.4: resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} engines: {node: '>= 0.4'} @@ -10118,9 +10034,6 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -10154,6 +10067,10 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -10174,6 +10091,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -10445,18 +10365,10 @@ packages: when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} - which-boxed-primitive@1.1.0: - resolution: {integrity: sha512-Ei7Miu/AXe2JJ4iNF5j/UphAgRoma4trE6PtisM09bPygb3egMH3YLW/befsWb1A1AxvNSFidOFTB18XtnIIng==} - engines: {node: '>= 0.4'} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} - which-builtin-type@1.2.0: - resolution: {integrity: sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==} - engines: {node: '>= 0.4'} - which-builtin-type@1.2.1: resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} engines: {node: '>= 0.4'} @@ -10468,10 +10380,6 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which-typed-array@1.1.16: - resolution: {integrity: sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==} - engines: {node: '>= 0.4'} - which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -12430,6 +12338,11 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.28.0(jiti@2.4.2))': + dependencies: + eslint: 9.28.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.4.2))': dependencies: eslint: 9.39.4(jiti@2.4.2) @@ -12437,6 +12350,14 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} + '@eslint/config-array@0.20.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 @@ -12445,10 +12366,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/config-helpers@0.2.3': {} + '@eslint/config-helpers@0.4.2': dependencies: '@eslint/core': 0.17.0 + '@eslint/core@0.14.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 @@ -12467,10 +12398,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/js@9.28.0': {} + '@eslint/js@9.39.4': {} '@eslint/object-schema@2.1.7': {} + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + '@eslint/plugin-kit@0.4.1': dependencies: '@eslint/core': 0.17.0 @@ -13065,7 +13003,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': @@ -14985,10 +14923,10 @@ snapshots: '@tabler/icons@3.40.0': {} - '@tanstack/eslint-plugin-query@5.94.4(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.94.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) - eslint: 9.39.4(jiti@2.4.2) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + eslint: 9.28.0(jiti@2.4.2) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -15020,16 +14958,17 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.9.1': + '@testing-library/jest-dom@6.6.0': dependencies: '@adobe/css-tools': 4.4.3 aria-query: 5.3.2 + chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 - picocolors: 1.1.1 + lodash: 4.18.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.1.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.29.2 '@testing-library/dom': 10.4.1 @@ -15517,11 +15456,11 @@ snapshots: '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 8.56.10 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/eslint@8.56.10': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/estree@1.0.8': {} @@ -15686,7 +15625,7 @@ snapshots: '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.11 - csstype: 3.1.3 + csstype: 3.2.3 '@types/send@0.17.4': dependencies: @@ -15747,6 +15686,22 @@ snapshots: dependencies: '@types/node': 25.5.0 + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/type-utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 + eslint: 9.28.0(jiti@2.4.2) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3))(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -15763,6 +15718,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3 + eslint: 9.28.0(jiti@2.4.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.57.1 @@ -15793,6 +15760,18 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/type-utils@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.28.0(jiti@2.4.2) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.57.1 @@ -15822,6 +15801,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.28.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + eslint: 9.28.0(jiti@2.4.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.4.2)) @@ -15922,10 +15912,10 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/expect@4.1.6': dependencies: @@ -15936,13 +15926,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@4.1.6': dependencies: @@ -16187,11 +16177,6 @@ snapshots: aria-query@5.3.2: {} - array-buffer-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - is-array-buffer: 3.0.4 - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -16199,57 +16184,46 @@ snapshots: array-includes@3.1.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 - is-string: 1.1.0 + is-string: 1.1.1 array-timsort@1.0.3: {} array.prototype.findlast@1.2.5: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.0.2 array.prototype.flat@1.3.2: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-shim-unscopables: 1.0.2 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-shim-unscopables: 1.0.2 array.prototype.tosorted@1.1.4: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-errors: 1.3.0 es-shim-unscopables: 1.0.2 - arraybuffer.prototype.slice@1.0.3: - dependencies: - array-buffer-byte-length: 1.0.1 - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.4 - is-shared-array-buffer: 1.0.3 - arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 @@ -16531,14 +16505,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.7: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - call-bind@1.0.8: dependencies: call-bind-apply-helpers: 1.0.2 @@ -16565,6 +16531,11 @@ snapshots: chai@6.2.2: {} + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -16890,8 +16861,6 @@ snapshots: '@asamuzakjp/css-color': 2.8.3 rrweb-cssom: 0.8.0 - csstype@3.1.3: {} - csstype@3.2.3: {} cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): @@ -17083,36 +17052,18 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - data-view-buffer@1.0.1: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 - data-view-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - data-view-byte-length@1.0.2: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 - data-view-byte-offset@1.0.0: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - data-view-byte-offset@1.0.1: dependencies: call-bound: 1.0.4 @@ -17230,7 +17181,7 @@ snapshots: dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.29.2 - csstype: 3.1.3 + csstype: 3.2.3 dom-serializer@2.0.0: dependencies: @@ -17368,55 +17319,6 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-abstract@1.23.5: - dependencies: - array-buffer-byte-length: 1.0.1 - arraybuffer.prototype.slice: 1.0.3 - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - data-view-buffer: 1.0.1 - data-view-byte-length: 1.0.1 - data-view-byte-offset: 1.0.0 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.6 - get-intrinsic: 1.3.0 - get-symbol-description: 1.0.2 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.0.3 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.0.7 - is-array-buffer: 3.0.4 - is-callable: 1.2.7 - is-data-view: 1.0.1 - is-negative-zero: 2.0.3 - is-regex: 1.2.0 - is-shared-array-buffer: 1.0.3 - is-string: 1.1.0 - is-typed-array: 1.1.13 - is-weakref: 1.0.2 - object-inspect: 1.13.3 - object-keys: 1.1.1 - object.assign: 4.1.5 - regexp.prototype.flags: 1.5.3 - safe-array-concat: 1.1.2 - safe-regex-test: 1.0.3 - string.prototype.trim: 1.2.9 - string.prototype.trimend: 1.0.8 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.2 - typed-array-byte-length: 1.0.1 - typed-array-byte-offset: 1.0.3 - typed-array-length: 1.0.7 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.16 - es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 @@ -17498,8 +17400,6 @@ snapshots: math-intrinsics: 1.1.0 safe-array-concat: 1.1.3 - es-module-lexer@2.0.0: {} - es-module-lexer@2.1.0: {} es-object-atoms@1.1.1: @@ -17520,8 +17420,8 @@ snapshots: es-to-primitive@1.3.0: dependencies: is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.1.0 + is-date-object: 1.1.0 + is-symbol: 1.1.1 es6-promise-pool@2.5.0: {} @@ -17599,22 +17499,22 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.4.2) - eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.4.2)): + eslint-plugin-react-hooks@7.0.1(eslint@9.28.0(jiti@2.4.2)): dependencies: '@babel/core': 7.28.5 '@babel/parser': 7.28.5 - eslint: 9.39.4(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@9.39.4(jiti@2.4.2)): + eslint-plugin-react-refresh@0.5.2(eslint@9.28.0(jiti@2.4.2)): dependencies: - eslint: 9.39.4(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) - eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.4.2)): + eslint-plugin-react@7.37.5(eslint@9.28.0(jiti@2.4.2)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -17622,7 +17522,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.3.1 - eslint: 9.39.4(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -17652,6 +17552,48 @@ snapshots: eslint-visitor-keys@5.0.1: {} + eslint@9.28.0(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.20.1 + '@eslint/config-helpers': 0.2.3 + '@eslint/core': 0.14.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.28.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.3 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + eslint@9.39.4(jiti@2.4.2): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.4.2)) @@ -17955,10 +17897,6 @@ snapshots: follow-redirects@1.16.0: {} - for-each@0.3.3: - dependencies: - is-callable: 1.2.7 - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -18027,13 +17965,6 @@ snapshots: function-bind@1.1.2: {} - function.prototype.name@1.1.6: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - functions-have-names: 1.2.3 - function.prototype.name@1.1.8: dependencies: call-bind: 1.0.8 @@ -18075,12 +18006,6 @@ snapshots: get-stream@6.0.1: {} - get-symbol-description@1.0.2: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -18157,8 +18082,6 @@ snapshots: dependencies: es-define-property: 1.0.1 - has-proto@1.0.3: {} - has-proto@1.2.0: dependencies: dunder-proto: 1.0.1 @@ -18306,12 +18229,6 @@ snapshots: inherits@2.0.4: {} - internal-slot@1.0.7: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -18346,11 +18263,6 @@ snapshots: ipaddr.js@2.2.0: {} - is-array-buffer@3.0.4: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.3.0 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -18371,11 +18283,6 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-boolean-object@1.2.0: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -18387,20 +18294,12 @@ snapshots: dependencies: hasown: 2.0.2 - is-data-view@1.0.1: - dependencies: - is-typed-array: 1.1.13 - is-data-view@1.0.2: dependencies: call-bound: 1.0.4 get-intrinsic: 1.3.0 is-typed-array: 1.1.15 - is-date-object@1.0.5: - dependencies: - has-tostringtag: 1.0.2 - is-date-object@1.1.0: dependencies: call-bound: 1.0.4 @@ -18432,11 +18331,6 @@ snapshots: is-negative-zero@2.0.3: {} - is-number-object@1.1.0: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -18448,13 +18342,6 @@ snapshots: is-promise@4.0.0: {} - is-regex@1.2.0: - dependencies: - call-bind: 1.0.7 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -18464,42 +18351,23 @@ snapshots: is-set@2.0.3: {} - is-shared-array-buffer@1.0.3: - dependencies: - call-bind: 1.0.7 - is-shared-array-buffer@1.0.4: dependencies: call-bound: 1.0.4 is-stream@2.0.1: {} - is-string@1.1.0: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-string@1.1.1: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-symbol@1.1.0: - dependencies: - call-bind: 1.0.7 - has-symbols: 1.1.0 - safe-regex-test: 1.0.3 - is-symbol@1.1.1: dependencies: call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 - is-typed-array@1.1.13: - dependencies: - which-typed-array: 1.1.16 - is-typed-array@1.1.15: dependencies: which-typed-array: 1.1.20 @@ -18510,10 +18378,6 @@ snapshots: is-weakmap@2.0.2: {} - is-weakref@1.0.2: - dependencies: - call-bind: 1.0.7 - is-weakref@1.1.1: dependencies: call-bound: 1.0.4 @@ -18990,7 +18854,7 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@25.0.1: + jsdom@25.0.0: dependencies: cssstyle: 4.2.1 data-urls: 5.0.0 @@ -19005,7 +18869,7 @@ snapshots: rrweb-cssom: 0.7.1 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 5.1.2 + tough-cookie: 4.1.4 w3c-xmlserializer: 5.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 3.1.1 @@ -19098,7 +18962,7 @@ snapshots: dependencies: array-includes: 3.1.8 array.prototype.flat: 1.3.2 - object.assign: 4.1.5 + object.assign: 4.1.7 object.values: 1.2.1 jszip@3.10.1: @@ -19729,13 +19593,6 @@ snapshots: object-keys@1.1.1: {} - object.assign@4.1.5: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - object.assign@4.1.7: dependencies: call-bind: 1.0.8 @@ -19754,9 +19611,9 @@ snapshots: object.fromentries@2.0.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 object.values@1.2.1: @@ -20134,40 +19991,40 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-js@4.0.1(postcss@8.5.12): + postcss-js@4.0.1(postcss@8.5.14): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.12 + postcss: 8.5.14 - postcss-mixins@12.1.2(postcss@8.5.12): + postcss-mixins@12.1.2(postcss@8.5.14): dependencies: - postcss: 8.5.12 - postcss-js: 4.0.1(postcss@8.5.12) - postcss-simple-vars: 7.0.1(postcss@8.5.12) - sugarss: 5.0.1(postcss@8.5.12) + postcss: 8.5.14 + postcss-js: 4.0.1(postcss@8.5.14) + postcss-simple-vars: 7.0.1(postcss@8.5.14) + sugarss: 5.0.1(postcss@8.5.14) tinyglobby: 0.2.15 - postcss-nested@7.0.2(postcss@8.5.12): + postcss-nested@7.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.12 + postcss: 8.5.14 postcss-selector-parser: 7.1.1 - postcss-preset-mantine@1.18.0(postcss@8.5.12): + postcss-preset-mantine@1.18.0(postcss@8.5.14): dependencies: - postcss: 8.5.12 - postcss-mixins: 12.1.2(postcss@8.5.12) - postcss-nested: 7.0.2(postcss@8.5.12) + postcss: 8.5.14 + postcss-mixins: 12.1.2(postcss@8.5.14) + postcss-nested: 7.0.2(postcss@8.5.14) postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-simple-vars@7.0.1(postcss@8.5.12): + postcss-simple-vars@7.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.12 + postcss: 8.5.14 - postcss@8.5.12: + postcss@8.5.14: dependencies: nanoid: 3.3.8 picocolors: 1.1.1 @@ -20382,6 +20239,10 @@ snapshots: prr@1.0.1: optional: true + psl@1.15.0: + dependencies: + punycode: 2.3.1 + pump@3.0.3: dependencies: end-of-stream: 1.4.4 @@ -20407,6 +20268,8 @@ snapshots: query-selector-shadow-dom@1.0.1: {} + querystringify@2.2.0: {} + quick-format-unescaped@4.0.4: {} radix-ui@1.4.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -20677,16 +20540,6 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 - reflect.getprototypeof@1.0.7: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - which-builtin-type: 1.2.0 - regenerate-unicode-properties@10.1.1: dependencies: regenerate: 1.4.2 @@ -20697,13 +20550,6 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - regexp.prototype.flags@1.5.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - set-function-name: 2.0.2 - regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -20732,6 +20578,8 @@ snapshots: require-main-filename@2.0.0: {} + requires-port@1.0.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -20832,13 +20680,6 @@ snapshots: dependencies: tslib: 2.8.1 - safe-array-concat@1.1.2: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -20856,12 +20697,6 @@ snapshots: es-errors: 1.3.0 isarray: 2.0.5 - safe-regex-test@1.0.3: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-regex: 1.2.0 - safe-regex-test@1.1.0: dependencies: call-bound: 1.0.4 @@ -21149,14 +20984,14 @@ snapshots: gopd: 1.2.0 has-symbols: 1.1.0 internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.3 + regexp.prototype.flags: 1.5.4 set-function-name: 2.0.2 side-channel: 1.1.0 string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 string.prototype.trim@1.2.10: dependencies: @@ -21168,19 +21003,6 @@ snapshots: es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 - string.prototype.trim@1.2.9: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - es-object-atoms: 1.1.1 - - string.prototype.trimend@1.0.8: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - string.prototype.trimend@1.0.9: dependencies: call-bind: 1.0.8 @@ -21190,7 +21012,7 @@ snapshots: string.prototype.trimstart@1.0.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -21239,9 +21061,9 @@ snapshots: stylis@4.3.6: {} - sugarss@5.0.1(postcss@8.5.12): + sugarss@5.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.12 + postcss: 8.5.14 superagent@10.3.0: dependencies: @@ -21371,6 +21193,13 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tough-cookie@5.1.2: dependencies: tldts: 6.1.72 @@ -21497,49 +21326,25 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typed-array-buffer@1.0.2: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-typed-array: 1.1.13 - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 - typed-array-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.2.0 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - typed-array-byte-length@1.0.3: dependencies: call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 - typed-array-byte-offset@1.0.3: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.2.0 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - reflect.getprototypeof: 1.0.7 - typed-array-byte-offset@1.0.4: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 @@ -21547,12 +21352,23 @@ snapshots: typed-array-length@1.0.7: dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 + call-bind: 1.0.8 + for-each: 0.3.5 gopd: 1.2.0 - is-typed-array: 1.1.13 + is-typed-array: 1.1.15 possible-typed-array-names: 1.0.0 - reflect.getprototypeof: 1.0.7 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + eslint: 9.28.0(jiti@2.4.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color typescript-eslint@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3): dependencies: @@ -21593,13 +21409,6 @@ snapshots: uint8array-extras@1.5.0: {} - unbox-primitive@1.0.2: - dependencies: - call-bind: 1.0.7 - has-bigints: 1.0.2 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.0 - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -21626,6 +21435,8 @@ snapshots: unicode-property-aliases-ecmascript@2.1.0: {} + universalify@0.2.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -21664,6 +21475,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + use-callback-ref@1.3.3(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 @@ -21724,11 +21540,11 @@ snapshots: vary@1.1.2: {} - vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.12 + postcss: 8.5.14 rolldown: 1.0.0-rc.12 tinyglobby: 0.2.15 optionalDependencies: @@ -21737,15 +21553,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 less: 4.2.0 - sugarss: 5.0.1(postcss@8.5.12) + sugarss: 5.0.1(postcss@8.5.14) terser: 5.39.0 tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.6 '@vitest/runner': 4.1.6 '@vitest/snapshot': 4.1.6 @@ -21762,13 +21578,13 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.1 happy-dom: 20.8.9 - jsdom: 25.0.1 + jsdom: 25.0.0 transitivePeerDependencies: - msw @@ -21825,7 +21641,7 @@ snapshots: webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5)): dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 @@ -21835,7 +21651,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.3 enhanced-resolve: 5.20.1 - es-module-lexer: 2.0.0 + es-module-lexer: 2.1.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -21878,14 +21694,6 @@ snapshots: when-exit@2.1.5: {} - which-boxed-primitive@1.1.0: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.0 - is-number-object: 1.1.0 - is-string: 1.1.0 - is-symbol: 1.1.0 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -21894,22 +21702,6 @@ snapshots: is-string: 1.1.1 is-symbol: 1.1.1 - which-builtin-type@1.2.0: - dependencies: - call-bind: 1.0.7 - function.prototype.name: 1.1.6 - has-tostringtag: 1.0.2 - is-async-function: 2.0.0 - is-date-object: 1.0.5 - is-finalizationregistry: 1.1.0 - is-generator-function: 1.0.10 - is-regex: 1.2.0 - is-weakref: 1.0.2 - isarray: 2.0.5 - which-boxed-primitive: 1.1.0 - which-collection: 1.0.2 - which-typed-array: 1.1.16 - which-builtin-type@1.2.1: dependencies: call-bound: 1.0.4 @@ -21922,7 +21714,7 @@ snapshots: is-regex: 1.2.1 is-weakref: 1.1.1 isarray: 2.0.5 - which-boxed-primitive: 1.1.0 + which-boxed-primitive: 1.1.1 which-collection: 1.0.2 which-typed-array: 1.1.20 @@ -21935,14 +21727,6 @@ snapshots: which-module@2.0.1: {} - which-typed-array@1.1.16: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7