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 (
+
+ );
+});
+
+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 (
+
+ );
+});
+
+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