From 31ed0df3f77eee42dea27494c9b3b4e8b41166df Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Wed, 13 May 2026 23:01:04 +0100 Subject: [PATCH 01/12] feat(tree): replace sidebar tree (react-aborist) with custom tree implementation (#2199) * feat(tree): replace react-arborist with custom tree implementation * feat(tree): keyboard arrow navigation between rows * feat(emoji-picker): focus search input on open * refactor(emoji): switch to @slidoapp/emoji-mart fork for accessibility * feat(tree): Home/End and typeahead keyboard navigation * feat(tree): roving tabindex and * to expand sibling subtrees * feat(tree): Space activation and ARIA refinements * fix(tree): move treeitem role to focusable row + aria-current --- apps/client/package.json | 23 +- .../client/src/components/ui/emoji-picker.tsx | 54 +- .../editor/components/emoji-menu/utils.ts | 4 +- .../components/mention/mention-list.tsx | 16 +- .../components/header/page-header-menu.tsx | 6 +- .../src/features/page/queries/page-query.ts | 20 +- .../page/tree/atoms/open-tree-nodes-atom.ts | 5 + .../features/page/tree/atoms/tree-api-atom.ts | 5 - .../doc-tree-drag-preview.module.css | 26 + .../tree/components/doc-tree-drag-preview.tsx | 9 + .../components/doc-tree-drop-indicator.tsx | 39 + .../page/tree/components/doc-tree-row.tsx | 398 ++++++++ .../page/tree/components/doc-tree.tsx | 541 ++++++++++ .../tree/components/space-tree-node-menu.tsx | 259 +++++ .../page/tree/components/space-tree-row.tsx | 288 ++++++ .../page/tree/components/space-tree.tsx | 754 ++------------ .../hooks/drop-op-to-move-payload.test.ts | 100 ++ .../tree/hooks/drop-op-to-move-payload.ts | 36 + .../page/tree/hooks/use-tree-mutation.ts | 437 ++++---- .../page/tree/model/tree-model.test.ts | 329 ++++++ .../features/page/tree/model/tree-model.ts | 222 ++++ .../page/tree/model/tree-model.types.ts | 20 + .../features/page/tree/styles/tree.module.css | 204 ++-- .../atoms/open-shared-tree-nodes-atom.ts | 3 + .../features/share/components/shared-tree.tsx | 269 ++--- .../components/sidebar/space-sidebar.tsx | 6 +- .../src/features/websocket/use-tree-socket.ts | 152 +-- apps/client/vitest.config.ts | 17 + apps/server/src/ws/ws.service.ts | 11 +- package.json | 1 - patches/react-arborist@3.4.0.patch | 33 - pnpm-lock.yaml | 958 +++++++++++++----- 32 files changed, 3816 insertions(+), 1429 deletions(-) create mode 100644 apps/client/src/features/page/tree/atoms/open-tree-nodes-atom.ts delete mode 100644 apps/client/src/features/page/tree/atoms/tree-api-atom.ts create mode 100644 apps/client/src/features/page/tree/components/doc-tree-drag-preview.module.css create mode 100644 apps/client/src/features/page/tree/components/doc-tree-drag-preview.tsx create mode 100644 apps/client/src/features/page/tree/components/doc-tree-drop-indicator.tsx create mode 100644 apps/client/src/features/page/tree/components/doc-tree-row.tsx create mode 100644 apps/client/src/features/page/tree/components/doc-tree.tsx create mode 100644 apps/client/src/features/page/tree/components/space-tree-node-menu.tsx create mode 100644 apps/client/src/features/page/tree/components/space-tree-row.tsx create mode 100644 apps/client/src/features/page/tree/hooks/drop-op-to-move-payload.test.ts create mode 100644 apps/client/src/features/page/tree/hooks/drop-op-to-move-payload.ts create mode 100644 apps/client/src/features/page/tree/model/tree-model.test.ts create mode 100644 apps/client/src/features/page/tree/model/tree-model.ts create mode 100644 apps/client/src/features/page/tree/model/tree-model.types.ts create mode 100644 apps/client/src/features/share/atoms/open-shared-tree-nodes-atom.ts create mode 100644 apps/client/vitest.config.ts delete mode 100644 patches/react-arborist@3.4.0.patch diff --git a/apps/client/package.json b/apps/client/package.json index f85c008e1..854c9f95d 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -7,13 +7,18 @@ "build": "tsc && vite build", "lint": "eslint .", "preview": "vite preview", - "format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"" + "format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"", + "test": "vitest run", + "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", "@docmost/editor-ext": "workspace:*", - "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", "@excalidraw/excalidraw": "0.18.0-3a5ef40", "@mantine/core": "^8.3.18", "@mantine/dates": "^8.3.18", @@ -22,13 +27,16 @@ "@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", "axios": "1.16.0", "blueimp-load-image": "^5.16.0", "clsx": "^2.1.1", - "emoji-mart": "^5.6.0", "file-saver": "^2.0.5", "highlightjs-sap-abap": "^0.3.0", "i18next": "25.10.1", @@ -44,7 +52,6 @@ "mitt": "^3.0.1", "posthog-js": "1.372.2", "react": "^18.3.1", - "react-arborist": "3.4.0", "react-clear-modal": "^2.0.18", "react-dom": "^18.3.1", "react-drawio": "^1.0.7", @@ -59,6 +66,8 @@ "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", @@ -72,6 +81,7 @@ "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", @@ -79,6 +89,7 @@ "prettier": "^3.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.1", - "vite": "8.0.5" + "vite": "8.0.5", + "vitest": "^4.1.6" } } diff --git a/apps/client/src/components/ui/emoji-picker.tsx b/apps/client/src/components/ui/emoji-picker.tsx index 804d1b0f4..c360998a3 100644 --- a/apps/client/src/components/ui/emoji-picker.tsx +++ b/apps/client/src/components/ui/emoji-picker.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from "react"; +import React, { ReactNode, useEffect, useState } from "react"; import { ActionIcon, Popover, @@ -7,9 +7,24 @@ import { } from "@mantine/core"; import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks"; import { Suspense } from "react"; -const Picker = React.lazy(() => import("@emoji-mart/react")); import { useTranslation } from "react-i18next"; +// Load the picker module AND the emoji data in parallel inside the lazy +// resolution, then bind the data into the component. React.lazy only finishes +// suspending once both are in memory, so the Suspense boundary hides the +// Remove button until the Picker can render with real content. +const Picker = React.lazy(async () => { + const [pickerModule, dataModule] = await Promise.all([ + import("@slidoapp/emoji-mart-react"), + import("@slidoapp/emoji-mart-data"), + ]); + const PickerComp = pickerModule.default; + const data = dataModule.default; + return { + default: (props: any) => , + }; +}); + export interface EmojiPickerInterface { onEmojiSelect: (emoji: any) => void; icon: ReactNode; @@ -19,6 +34,7 @@ export interface EmojiPickerInterface { size?: string; variant?: string; c?: string; + tabIndex?: number; }; } @@ -50,6 +66,38 @@ function EmojiPicker({ } }); + // emoji-mart's built-in autoFocus calls .focus() without preventScroll, which + // makes the browser scroll every scrollable ancestor of the search input to + // bring it on screen — including the page editor's scroll container, so the + // page jumps to the top whenever the picker is opened from a scrolled-down + // position. The search input lives inside the custom + // element's shadow root, so we poll for it after the dropdown mounts and + // focus it ourselves with preventScroll. + useEffect(() => { + if (!opened || !dropdown) return; + let cancelled = false; + let rafId = 0; + const tryFocus = (attempts: number) => { + if (cancelled) return; + const pickerEl = dropdown.querySelector("em-emoji-picker"); + const input = pickerEl?.shadowRoot?.querySelector( + 'input[type="search"]', + ); + if (input) { + input.focus({ preventScroll: true }); + return; + } + if (attempts < 60) { + rafId = requestAnimationFrame(() => tryFocus(attempts + 1)); + } + }; + rafId = requestAnimationFrame(() => tryFocus(0)); + return () => { + cancelled = true; + cancelAnimationFrame(rafId); + }; + }, [opened, dropdown]); + const handleEmojiSelect = (emoji) => { onEmojiSelect(emoji); handlers.close(); @@ -74,6 +122,7 @@ function EmojiPicker({ c={actionIconProps?.c || "gray"} variant={actionIconProps?.variant || "transparent"} size={actionIconProps?.size} + tabIndex={actionIconProps?.tabIndex} onClick={handlers.toggle} aria-label={t("Pick emoji")} aria-haspopup="dialog" @@ -85,7 +134,6 @@ function EmojiPicker({ (await import("@emoji-mart/data")).default} onEmojiSelect={handleEmojiSelect} perLine={8} skinTonePosition="search" diff --git a/apps/client/src/features/editor/components/emoji-menu/utils.ts b/apps/client/src/features/editor/components/emoji-menu/utils.ts index 8a86ee501..bded7bcd5 100644 --- a/apps/client/src/features/editor/components/emoji-menu/utils.ts +++ b/apps/client/src/features/editor/components/emoji-menu/utils.ts @@ -21,7 +21,7 @@ let _emojiIndex: EmojiIndexEntry[] | null = null; export const buildEmojiIndex = async (): Promise => { if (_emojiIndex) return _emojiIndex; - const { default: data } = await import("@emoji-mart/data"); + const { default: data } = await import('@slidoapp/emoji-mart-data'); _emojiIndex = (Object.values((data as any).emojis) as any[]) .filter((e) => e.id && e.name && e.skins?.[0]?.native) .map((e) => ({ @@ -74,7 +74,7 @@ let _cats: EmojiCategory[] | null = null; export const getEmojiCategories = async (): Promise => { if (_cats) return _cats; const [{ default: data }, index] = await Promise.all([ - import("@emoji-mart/data"), + import("@slidoapp/emoji-mart-data"), buildEmojiIndex(), ]); const byId = new Map(index.map((e) => [e.id, e])); diff --git a/apps/client/src/features/editor/components/mention/mention-list.tsx b/apps/client/src/features/editor/components/mention/mention-list.tsx index 330bded9a..8f6269060 100644 --- a/apps/client/src/features/editor/components/mention/mention-list.tsx +++ b/apps/client/src/features/editor/components/mention/mention-list.tsx @@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useImperativeHandle, - useMemo, useRef, useState, } from "react"; @@ -36,7 +35,7 @@ import { usePageQuery, } from "@/features/page/queries/page-query"; import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom"; -import { SimpleTree } from "react-arborist"; +import { treeModel } from "@/features/page/tree/model/tree-model"; import { SpaceTreeNode } from "@/features/page/tree/types"; import { useTranslation } from "react-i18next"; import { useQueryEmit } from "@/features/websocket/use-query-emit"; @@ -53,7 +52,6 @@ const MentionList = forwardRef((props, ref) => { const [renderItems, setRenderItems] = useState([]); const { t } = useTranslation(); const [data, setData] = useAtom(treeDataAtom); - const tree = useMemo(() => new SimpleTree(data), [data]); const createPageMutation = useCreatePageMutation(); const emit = useQueryEmit(); const isInCommentContext = props.isInCommentContext ?? false; @@ -220,20 +218,20 @@ const MentionList = forwardRef((props, ref) => { try { createdPage = await createPageMutation.mutateAsync(payload); const parentId = page.id || null; - const data = { + const newNode: SpaceTreeNode = { id: createdPage.id, slugId: createdPage.slugId, name: createdPage.title, position: createdPage.position, spaceId: createdPage.spaceId, parentPageId: createdPage.parentPageId, + hasChildren: false, children: [], - } as any; + }; - const lastIndex = tree.data.length; + const lastIndex = data.length; - tree.create({ parentId, index: lastIndex, data }); - setData(tree.data); + setData(treeModel.insert(data, parentId, newNode, lastIndex)); props.command({ id: uuid7(), @@ -251,7 +249,7 @@ const MentionList = forwardRef((props, ref) => { payload: { parentId, index: lastIndex, - data, + data: newNode, }, }); }, 50); diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx index 81c25e825..6e481b7aa 100644 --- a/apps/client/src/features/page/components/header/page-header-menu.tsx +++ b/apps/client/src/features/page/components/header/page-header-menu.tsx @@ -29,7 +29,7 @@ import { buildPageUrl } from "@/features/page/page.utils.ts"; import { notifications } from "@mantine/notifications"; import { getAppUrl } from "@/lib/config.ts"; import { extractPageSlugId } from "@/lib"; -import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts"; +import { useTreeMutation } from "@/features/page/tree/hooks/use-tree-mutation.ts"; import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx"; import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx"; import { Trans, useTranslation } from "react-i18next"; @@ -134,7 +134,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { pageId: extractPageSlugId(pageSlug), }); const { openDeleteModal } = useDeletePageModal(); - const [tree] = useAtom(treeApiAtom); + const { handleDelete } = useTreeMutation(page?.spaceId ?? ""); const [exportOpened, { open: openExportModal, close: closeExportModal }] = useDisclosure(false); const [ @@ -183,7 +183,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { }; const handleDeletePage = () => { - openDeleteModal({ onConfirm: () => tree?.delete(page.id) }); + openDeleteModal({ onConfirm: () => handleDelete(page.id) }); }; const handleToggleFavorite = () => { diff --git a/apps/client/src/features/page/queries/page-query.ts b/apps/client/src/features/page/queries/page-query.ts index 89526aa69..1ed704ce1 100644 --- a/apps/client/src/features/page/queries/page-query.ts +++ b/apps/client/src/features/page/queries/page-query.ts @@ -37,7 +37,7 @@ import { validate as isValidUuid } from "uuid"; import { useTranslation } from "react-i18next"; import { useAtom } from "jotai"; import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom"; -import { SimpleTree } from "react-arborist"; +import { treeModel } from "@/features/page/tree/model/tree-model"; import { SpaceTreeNode } from "@/features/page/tree/types"; import { useQueryEmit } from "@/features/websocket/use-query-emit"; @@ -170,11 +170,8 @@ export function useRestorePageMutation() { onSuccess: async (restoredPage) => { notifications.show({ message: "Page restored successfully" }); - // Add the restored page back to the tree - const treeApi = new SimpleTree(treeData); - // Check if the page already exists in the tree (it shouldn't) - if (!treeApi.find(restoredPage.id)) { + if (!treeModel.find(treeData, restoredPage.id)) { // Create the tree node data with hasChildren from backend const nodeData: SpaceTreeNode = { id: restoredPage.id, @@ -193,24 +190,17 @@ export function useRestorePageMutation() { let index = 0; if (parentId) { - const parentNode = treeApi.find(parentId); + const parentNode = treeModel.find(treeData, parentId); if (parentNode) { index = parentNode.children?.length || 0; } } else { // Root level page - index = treeApi.data.length; + index = treeData.length; } // Add the node to the tree - treeApi.create({ - parentId, - index, - data: nodeData, - }); - - // Update the tree data - setTreeData(treeApi.data); + setTreeData(treeModel.insert(treeData, parentId, nodeData, index)); // Emit websocket event to sync with other users setTimeout(() => { diff --git a/apps/client/src/features/page/tree/atoms/open-tree-nodes-atom.ts b/apps/client/src/features/page/tree/atoms/open-tree-nodes-atom.ts new file mode 100644 index 000000000..3dd2d98bc --- /dev/null +++ b/apps/client/src/features/page/tree/atoms/open-tree-nodes-atom.ts @@ -0,0 +1,5 @@ +import { atom } from "jotai"; + +export type OpenMap = Record; + +export const openTreeNodesAtom = atom({}); diff --git a/apps/client/src/features/page/tree/atoms/tree-api-atom.ts b/apps/client/src/features/page/tree/atoms/tree-api-atom.ts deleted file mode 100644 index f12106f99..000000000 --- a/apps/client/src/features/page/tree/atoms/tree-api-atom.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { atom } from "jotai"; -import { TreeApi } from "react-arborist"; -import { SpaceTreeNode } from "../types"; - -export const treeApiAtom = atom | null>(null); diff --git a/apps/client/src/features/page/tree/components/doc-tree-drag-preview.module.css b/apps/client/src/features/page/tree/components/doc-tree-drag-preview.module.css new file mode 100644 index 000000000..acd0da016 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree-drag-preview.module.css @@ -0,0 +1,26 @@ +.preview { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + background-color: light-dark( + var(--mantine-color-white), + var(--mantine-color-dark-6) + ); + color: light-dark( + var(--mantine-color-gray-9), + var(--mantine-color-dark-0) + ); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.18); + border: 1px solid light-dark( + var(--mantine-color-gray-3), + var(--mantine-color-dark-4) + ); + max-width: 260px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/apps/client/src/features/page/tree/components/doc-tree-drag-preview.tsx b/apps/client/src/features/page/tree/components/doc-tree-drag-preview.tsx new file mode 100644 index 000000000..f8a8a88b1 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree-drag-preview.tsx @@ -0,0 +1,9 @@ +import styles from './doc-tree-drag-preview.module.css'; + +type Props = { + label: string; +}; + +export function DocTreeDragPreview({ label }: Props) { + return
{label || 'Untitled'}
; +} diff --git a/apps/client/src/features/page/tree/components/doc-tree-drop-indicator.tsx b/apps/client/src/features/page/tree/components/doc-tree-drop-indicator.tsx new file mode 100644 index 000000000..9d6352ab5 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree-drop-indicator.tsx @@ -0,0 +1,39 @@ +import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'; +import styles from '../styles/tree.module.css'; + +type Props = { + instruction: Instruction; + indentPx: number; +}; + +export function DocTreeDropIndicator({ instruction, indentPx }: Props) { + const blocked = instruction.type === 'instruction-blocked'; + const inst = blocked ? instruction.desired : instruction; + + const style = { + ['--drop-line-indent' as never]: `${indentPx}px`, + } as React.CSSProperties; + + if (inst.type === 'reorder-above') { + return ( +
+ ); + } + if (inst.type === 'reorder-below') { + return ( +
+ ); + } + // 'combine' (make-child) is rendered via [data-receiving-drop] on the row itself. + return null; +} diff --git a/apps/client/src/features/page/tree/components/doc-tree-row.tsx b/apps/client/src/features/page/tree/components/doc-tree-row.tsx new file mode 100644 index 000000000..347f1f3e7 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree-row.tsx @@ -0,0 +1,398 @@ +import { + memo, + useCallback, + useEffect, + useRef, + useState, + type ReactNode, +} from 'react'; +import { createRoot } from 'react-dom/client'; +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; +import { + draggable, + dropTargetForElements, +} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview'; +import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'; +import { + attachInstruction, + extractInstruction, + type Instruction, + type ItemMode, +} from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'; +import { triggerPostMoveFlash } from '@atlaskit/pragmatic-drag-and-drop-flourish/trigger-post-move-flash'; +import * as liveRegion from '@atlaskit/pragmatic-drag-and-drop-live-region'; + +import type { TreeNode, DropOp } from '../model/tree-model.types'; +import { treeModel } from '../model/tree-model'; +import { DocTreeDropIndicator } from './doc-tree-drop-indicator'; +import { DocTreeDragPreview } from './doc-tree-drag-preview'; +import type { RenderRowProps } from './doc-tree'; +import styles from '../styles/tree.module.css'; + +type Props = { + node: TreeNode; + level: number; + isLastSibling: boolean; + openIds: ReadonlySet; + selectedId?: string; + // Roving tabindex: the single row that currently carries tabIndex={0}. + activeId?: string; + renderRow: (props: RenderRowProps) => ReactNode; + indentPerLevel: number; + onMove: (sourceId: string, op: DropOp) => void | Promise; + onToggle: (id: string, isOpen: boolean) => void; + readOnly: boolean; + disableDrag?: (node: TreeNode) => boolean; + disableDrop?: (node: TreeNode) => boolean; + getDragLabel: (node: TreeNode) => string; + contextId: symbol; + registerRowElement: (id: string, el: HTMLElement | null) => void; + // Stable accessor — calling it returns the latest tree. Avoids passing the + // tree itself as a prop (which would break memo and re-run every row's DnD + // useEffect on every mutation). + getRootData: () => TreeNode[]; +}; + +const DRAG_TYPE = 'doc-tree-item'; +const AUTO_EXPAND_MS = 500; + +function DocTreeRowInner(props: Props) { + const { + node, + level, + isLastSibling, + openIds, + selectedId, + activeId, + renderRow, + indentPerLevel, + onMove, + onToggle, + readOnly, + disableDrag, + disableDrop, + getDragLabel, + contextId, + registerRowElement, + getRootData, + } = props; + + const isOpen = openIds.has(node.id); + // "Has children" includes both already-loaded children AND the consumer's + // own server-side flag (`hasChildren` is a docmost convention on + // SpaceTreeNode / SharedPageTreeNode). The flag lets the chevron and the + // auto-expand timer recognize unloaded subtrees so the consumer's lazy-load + // (via onToggle) can populate them on demand. + const hasLoadedChildren = !!node.children && node.children.length > 0; + const declaredHasChildren = + (node as { hasChildren?: boolean }).hasChildren === true; + const hasChildren = hasLoadedChildren || declaredHasChildren; + const isSelected = selectedId === node.id; + + const rowRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [instruction, setInstruction] = useState(null); + const autoExpandTimerRef = useRef | null>(null); + + const cancelAutoExpand = useCallback(() => { + if (autoExpandTimerRef.current) { + clearTimeout(autoExpandTimerRef.current); + autoExpandTimerRef.current = null; + } + }, []); + + const toggleOpen = useCallback(() => { + onToggle(node.id, !isOpen); + }, [onToggle, node.id, isOpen]); + + useEffect(() => { + registerRowElement(node.id, rowRef.current); + return () => registerRowElement(node.id, null); + }, [registerRowElement, node.id]); + + // Restore lazy-loaded children when the row mounts open but its children + // aren't loaded (e.g. cross-space page move drops a node into a new tree + // that still has its id in openIds). Calling onToggle(id, true) is + // idempotent for open state and triggers the consumer's lazy-load. + useEffect(() => { + if (isOpen && declaredHasChildren && !hasLoadedChildren) { + onToggle(node.id, true); + } + }, [isOpen, declaredHasChildren, hasLoadedChildren, node.id, onToggle]); + + useEffect(() => { + const el = rowRef.current; + if (!el || readOnly) return; + const dragDisabled = disableDrag?.(node) ?? false; + const dropDisabled = disableDrop?.(node) ?? false; + + const cleanups: Array<() => void> = []; + + if (!dragDisabled) { + cleanups.push( + draggable({ + element: el, + getInitialData: () => ({ + id: node.id, + type: DRAG_TYPE, + uniqueContextId: contextId, + isOpenOnDragStart: isOpen, + }), + onGenerateDragPreview: ({ nativeSetDragImage }) => { + setCustomNativeDragPreview({ + nativeSetDragImage, + getOffset: pointerOutsideOfPreview({ x: '16px', y: '8px' }), + render: ({ container }) => { + const root = createRoot(container); + root.render(); + return () => root.unmount(); + }, + }); + }, + onDragStart: () => setIsDragging(true), + onDrop: () => setIsDragging(false), + }), + ); + } + + if (!dropDisabled) { + const mode: ItemMode = + isOpen && hasChildren + ? 'expanded' + : isLastSibling + ? 'last-in-group' + : 'standard'; + // Always block 'reparent' (out of scope per spec). + // Block 'reorder-below' when the row is open with children — ambiguous gesture, + // force users to drop into the folder via 'make-child' instead. + const block: Instruction['type'][] = ['reparent']; + if (isOpen && hasChildren) block.push('reorder-below'); + + cleanups.push( + dropTargetForElements({ + element: el, + canDrop: ({ source }) => + source.data.type === DRAG_TYPE && + source.data.uniqueContextId === contextId && + source.data.id !== node.id && + !treeModel.isDescendant( + getRootData(), + source.data.id as string, + node.id, + ), + getData: ({ input, element }) => + attachInstruction( + { id: node.id, type: DRAG_TYPE }, + { + input, + element, + currentLevel: level, + indentPerLevel, + mode, + block, + }, + ), + onDrag: ({ self }) => { + const inst = extractInstruction(self.data); + setInstruction(inst); + // Auto-expand on hover over any collapsed row that has children, + // regardless of the specific instruction type. Reorder-before and + // reorder-after also benefit: once expanded, the user can see the + // children and refine their drop target. + if ( + inst && + hasChildren && + !isOpen && + !autoExpandTimerRef.current + ) { + autoExpandTimerRef.current = setTimeout(() => { + onToggle(node.id, true); + autoExpandTimerRef.current = null; + }, AUTO_EXPAND_MS); + } + }, + onDragLeave: () => { + setInstruction(null); + cancelAutoExpand(); + }, + onDrop: ({ source, self }) => { + setInstruction(null); + cancelAutoExpand(); + const inst = extractInstruction(self.data); + if (!inst || inst.type === 'instruction-blocked') return; + const sourceId = source.data.id as string; + const op: DropOp = + inst.type === 'reorder-above' + ? { kind: 'reorder-before', targetId: node.id } + : inst.type === 'reorder-below' + ? { kind: 'reorder-after', targetId: node.id } + : inst.type === 'make-child' + ? { kind: 'make-child', targetId: node.id } + : null!; + if (!op) return; + onMove(sourceId, op); + triggerPostMoveFlash(el); + const liveTree = getRootData(); + const parentName = + op.kind === 'make-child' + ? getDragLabel(node) + : (() => { + const sib = treeModel.siblingsOf(liveTree, op.targetId); + const parent = sib?.parentId + ? treeModel.find(liveTree, sib.parentId) + : null; + return parent ? getDragLabel(parent) : 'root'; + })(); + const sourceNode = treeModel.find(liveTree, sourceId); + const sourceLabel = sourceNode + ? getDragLabel(sourceNode) + : 'item'; + liveRegion.announce(`Moved ${sourceLabel} under ${parentName}.`); + // After a make-child drop, expand this row so the user sees the + // just-dropped child — especially important when the row had no + // children before (chevron just appeared) so the drop would + // otherwise be invisible. + if (op.kind === 'make-child') onToggle(node.id, true); + if (source.data.isOpenOnDragStart) onToggle(sourceId, true); + }, + }), + ); + } + + return combine(...cleanups); + }, [ + node, + level, + isOpen, + hasChildren, + isLastSibling, + readOnly, + disableDrag, + disableDrop, + contextId, + indentPerLevel, + getDragLabel, + onMove, + onToggle, + getRootData, + cancelAutoExpand, + ]); + + useEffect(() => () => cancelAutoExpand(), [cancelAutoExpand]); + + const effectiveInst = + instruction?.type === 'instruction-blocked' + ? instruction.desired + : instruction; + const blocked = instruction?.type === 'instruction-blocked'; + const receivingDrop: 'before' | 'after' | 'make-child' | null = (() => { + if (!effectiveInst) return null; + if (effectiveInst.type === 'reorder-above') return 'before'; + if (effectiveInst.type === 'reorder-below') return 'after'; + if (effectiveInst.type === 'make-child') return 'make-child'; + return null; + })(); + + // Treeitem semantics ride on the row's focusable element (the consumer's + // ). The outer
  • is presentational layout. aria-label uses the row's + // label so the SR's accessible name is just the page title, not the + // concatenation of inner action-button aria-labels. + const treeItemProps = { + role: 'treeitem' as const, + 'aria-level': level + 1, + 'aria-expanded': hasChildren ? isOpen : undefined, + 'aria-selected': isSelected ? (true as const) : undefined, + 'aria-current': isSelected ? ('page' as const) : undefined, + 'aria-label': getDragLabel(node), + 'data-row-id': node.id, + }; + + return ( +
    +
    + {renderRow({ + node, + level, + isOpen, + hasChildren, + isSelected, + isDragging, + isReceivingDrop: receivingDrop, + rowRef, + tabIndex: activeId === node.id ? 0 : -1, + treeItemProps, + toggleOpen, + })} +
    + {instruction && ( + + )} +
    + ); +} + +// Custom memo comparator. The default shallow compare re-renders every row +// when `openIds` (a Set) or `selectedId` (a string) on the parent changes, +// because all rows receive the same reference via {...props} spread. With 1K +// rows that's a perceptible stall on every expand and every navigate. +// +// Resolve openIds / selectedId per-row: only re-render if THIS row's own +// open-state or selected-state actually flipped. Everything else uses +// reference equality (callbacks are useCallback-stable from the parent). +function arePropsEqual( + prev: Props, + next: Props, +): boolean { + if (prev.node !== next.node) return false; + if (prev.level !== next.level) return false; + if (prev.isLastSibling !== next.isLastSibling) return false; + if (prev.readOnly !== next.readOnly) return false; + if (prev.contextId !== next.contextId) return false; + if (prev.indentPerLevel !== next.indentPerLevel) return false; + if (prev.renderRow !== next.renderRow) return false; + if (prev.onMove !== next.onMove) return false; + if (prev.onToggle !== next.onToggle) return false; + if (prev.disableDrag !== next.disableDrag) return false; + if (prev.disableDrop !== next.disableDrop) return false; + if (prev.getDragLabel !== next.getDragLabel) return false; + if (prev.registerRowElement !== next.registerRowElement) return false; + if (prev.getRootData !== next.getRootData) return false; + + const id = next.node.id; + // openIds: only this row's own membership matters. + if (prev.openIds.has(id) !== next.openIds.has(id)) return false; + // selectedId: re-render only the rows whose isSelected actually flipped. + const wasSelected = prev.selectedId === id; + const isSelected = next.selectedId === id; + if (wasSelected !== isSelected) return false; + // activeId: same trick — only the outgoing and incoming active rows + // re-render when the user moves focus through the tree. + const wasActive = prev.activeId === id; + const isActive = next.activeId === id; + if (wasActive !== isActive) return false; + + return true; +} + +export const DocTreeRow = memo( + DocTreeRowInner, + arePropsEqual, +) as typeof DocTreeRowInner; diff --git a/apps/client/src/features/page/tree/components/doc-tree.tsx b/apps/client/src/features/page/tree/components/doc-tree.tsx new file mode 100644 index 000000000..3dbbfda16 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree.tsx @@ -0,0 +1,541 @@ +import { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, + type ReactNode, + type Ref, +} from 'react'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; +import type { TreeNode, DropOp } from '../model/tree-model.types'; +import { treeModel } from '../model/tree-model'; +import { DocTreeRow } from './doc-tree-row'; +import styles from '../styles/tree.module.css'; + +export type RenderRowProps = { + node: TreeNode; + level: number; + isOpen: boolean; + hasChildren: boolean; + isSelected: boolean; + isDragging: boolean; + isReceivingDrop: 'before' | 'after' | 'make-child' | null; + + rowRef: Ref; + // Roving tabindex: exactly one row in the tree carries tabIndex={0} (the + // active row); every other row gets tabIndex={-1}. Consumers must spread + // this onto the same element they wire rowRef to. + tabIndex: 0 | -1; + // Treeitem semantics for the row's focusable element. Consumers MUST spread + // these onto the same element rowRef points at, so the focused element IS + // the treeitem. This makes screen readers announce "treeitem" (not "link") + // and replaces the descendant-text accname with the row's label, so action + // button labels inside the row don't get concatenated. + treeItemProps: { + role: 'treeitem'; + 'aria-level': number; + 'aria-expanded'?: boolean; + 'aria-selected'?: true; + 'aria-current'?: 'page'; + 'aria-label': string; + 'data-row-id': string; + }; + toggleOpen: () => void; +}; + +export type DocTreeProps = { + data: TreeNode[]; + openIds: ReadonlySet; + selectedId?: string; + + renderRow: (props: RenderRowProps) => ReactNode; + indentPerLevel?: number; + rowHeight?: number; + emptyState?: ReactNode; + + onMove: (sourceId: string, op: DropOp) => void | Promise; + onToggle: (id: string, isOpen: boolean) => void; + onSelect?: (id: string) => void; + + readOnly?: boolean; + disableDrag?: (node: TreeNode) => boolean; + disableDrop?: (node: TreeNode) => boolean; + + getDragLabel: (node: TreeNode) => string; + uniqueContextId?: symbol; + + // Accessible name for the tree itself (e.g. "Pages"). Rendered as + // aria-label on the
  • + + + + + + } + onClick={() => editor.chain().focus().mergeCells().run()} + disabled={!editor.can().mergeCells()} + > + {t("Merge cells")} + + } + onClick={() => editor.chain().focus().splitCell().run()} + disabled={!editor.can().splitCell()} + > + {t("Split cell")} + + } + onClick={() => editor.chain().focus().toggleHeaderCell().run()} + > + {t("Toggle header cell")} + + + + + } + onClick={() => editor.chain().focus().addColumnAfter().run()} + > + {t("Add column right")} + + } + onClick={() => editor.chain().focus().addRowAfter().run()} + > + {t("Add row below")} + + + } onClick={clearCell}> + {t("Clear cell")} + + } + onClick={() => editor.chain().focus().deleteColumn().run()} + > + {t("Delete column")} + + } + onClick={() => editor.chain().focus().deleteRow().run()} + > + {t("Delete row")} + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx new file mode 100644 index 000000000..8dbe9d326 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx @@ -0,0 +1,177 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { ColorSwatch, Menu } from "@mantine/core"; +import { TABLE_COLORS } from "../../table-background-color"; +import { + IconArrowLeft, + IconArrowRight, + IconColumnInsertLeft, + IconColumnInsertRight, + IconColumnRemove, + IconEraser, + IconPalette, + IconSortAscendingLetters, + IconSortDescendingLetters, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useTableMoveRowColumn } from "../hooks/use-table-move-row-column"; +import { useTableClear } from "../hooks/use-table-clear"; +import { useTableSort } from "../hooks/use-table-sort"; +import { AlignmentSubmenu } from "./alignment-submenu"; + +interface ColumnHandleMenuProps { + editor: Editor; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const ColumnHandleMenu = React.memo(function ColumnHandleMenu({ + editor, + index, + tableNode, + tablePos, +}: ColumnHandleMenuProps) { + const { t } = useTranslation(); + + const moveLeft = useTableMoveRowColumn(editor, "col", index, "left", tableNode, tablePos); + const moveRight = useTableMoveRowColumn(editor, "col", index, "right", tableNode, tablePos); + const clearCol = useTableClear(editor, tableNode, tablePos, { + kind: "col", + index, + }); + + const setBackground = (color: string, name: string) => { + editor + .chain() + .focus() + .updateAttributes("tableCell", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .updateAttributes("tableHeader", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .run(); + }; + + const sortAsc = useTableSort({ + editor, + orientation: "col", + index, + tableNode, + tablePos, + direction: "asc", + }); + const sortDesc = useTableSort({ + editor, + orientation: "col", + index, + tableNode, + tablePos, + direction: "desc", + }); + + return ( + <> + } + onClick={sortAsc.handleSort} + disabled={!sortAsc.canSort} + > + {t("Sort A → Z")} + + } + onClick={sortDesc.handleSort} + disabled={!sortDesc.canSort} + > + {t("Sort Z → A")} + + + + + + }> + {t("Background color")} + + + +
    + {TABLE_COLORS.map((c) => ( + + ))} +
    +
    +
    + + + + + + } + onClick={() => editor.chain().focus().addColumnBefore().run()} + > + {t("Add column left")} + + } + onClick={() => editor.chain().focus().addColumnAfter().run()} + > + {t("Add column right")} + + + + + } + onClick={clearCol} + > + {t("Clear cells")} + + } + onClick={() => editor.chain().focus().deleteColumn().run()} + > + {t("Delete column")} + + + + + } + onClick={moveLeft.handleMove} + disabled={!moveLeft.canMove} + > + {t("Move column left")} + + } + onClick={moveRight.handleMove} + disabled={!moveRight.canMove} + > + {t("Move column right")} + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx new file mode 100644 index 000000000..13b968b76 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { ColorSwatch, Menu } from "@mantine/core"; +import { TABLE_COLORS } from "../../table-background-color"; +import { + IconArrowDown, + IconArrowUp, + IconEraser, + IconPalette, + IconRowInsertBottom, + IconRowInsertTop, + IconRowRemove, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useTableMoveRowColumn } from "../hooks/use-table-move-row-column"; +import { useTableClear } from "../hooks/use-table-clear"; +import { AlignmentSubmenu } from "./alignment-submenu"; + +interface RowHandleMenuProps { + editor: Editor; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const RowHandleMenu = React.memo(function RowHandleMenu({ + editor, + index, + tableNode, + tablePos, +}: RowHandleMenuProps) { + const { t } = useTranslation(); + + const setBackground = (color: string, name: string) => { + editor + .chain() + .focus() + .updateAttributes("tableCell", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .updateAttributes("tableHeader", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .run(); + }; + + const moveUp = useTableMoveRowColumn(editor, "row", index, "up", tableNode, tablePos); + const moveDown = useTableMoveRowColumn(editor, "row", index, "down", tableNode, tablePos); + const clearRow = useTableClear(editor, tableNode, tablePos, { + kind: "row", + index, + }); + + return ( + <> + + + }> + {t("Background color")} + + + +
    + {TABLE_COLORS.map((c) => ( + + ))} +
    +
    +
    + + + + + + } + onClick={() => editor.chain().focus().addRowBefore().run()} + > + {t("Add row above")} + + } + onClick={() => editor.chain().focus().addRowAfter().run()} + > + {t("Add row below")} + + + + + } onClick={clearRow}> + {t("Clear cells")} + + } + onClick={() => editor.chain().focus().deleteRow().run()} + > + {t("Delete row")} + + + + + } + onClick={moveUp.handleMove} + disabled={!moveUp.canMove} + > + {t("Move row up")} + + } + onClick={moveDown.handleMove} + disabled={!moveDown.canMove} + > + {t("Move row down")} + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/row-handle.tsx b/apps/client/src/features/editor/components/table/handle/row-handle.tsx new file mode 100644 index 000000000..7a5483558 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/row-handle.tsx @@ -0,0 +1,122 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { useFloating, offset, autoUpdate, hide } from "@floating-ui/react"; +import { Menu } from "@mantine/core"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { useTableHandleDrag } from "./hooks/use-table-handle-drag"; +import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle"; +import { RowHandleMenu } from "./menus/row-handle-menu"; +import classes from "./handle.module.css"; + +interface RowHandleProps { + editor: Editor; + index: number; + anchorPos: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const RowHandle = React.memo(function RowHandle({ + editor, + index, + anchorPos, + tableNode, + tablePos, +}: RowHandleProps) { + const { t } = useTranslation(); + // See ColumnHandle for the rationale: keep the last valid cell DOM cached + // so the handle div stays mounted across stale-anchor renders, otherwise + // pragmatic-dnd silently aborts an in-flight drag. + const lookupCellDom = editor.view.nodeDOM(anchorPos) as HTMLElement | null; + const [cellDom, setCellDom] = useState(lookupCellDom); + const lastCellDomRef = useRef(lookupCellDom); + useEffect(() => { + if (lookupCellDom && lookupCellDom !== lastCellDomRef.current) { + lastCellDomRef.current = lookupCellDom; + setCellDom(lookupCellDom); + } + }, [lookupCellDom]); + + const [handleEl, setHandleEl] = useState(null); + + const { refs, floatingStyles, middlewareData } = useFloating({ + placement: "left", + middleware: [offset(-4), hide()], + whileElementsMounted: autoUpdate, + }); + const isReferenceHidden = !!middlewareData.hide?.referenceHidden; + + useEffect(() => { + refs.setReference(cellDom); + }, [cellDom, refs]); + + const wrapper = cellDom?.closest(".tableWrapper") ?? null; + + const [menuOpened, setMenuOpened] = useState(false); + const closeMenu = useCallback(() => setMenuOpened(false), []); + useTableHandleDrag(editor, "row", handleEl, wrapper, closeMenu); + + const { onOpen, onClose } = useColumnRowMenuLifecycle({ + editor, + orientation: "row", + index, + tableNode, + tablePos, + }); + + if (!cellDom) return null; + + return ( + + +
    { + refs.setFloating(node); + setHandleEl(node); + }} + style={{ + ...floatingStyles, + ...(isReferenceHidden ? { visibility: "hidden" as const } : {}), + }} + className={clsx(classes.handle, classes.rowHandle)} + role="button" + tabIndex={0} + aria-label={t("Row actions")} + > + + + +
    +
    + + + +
    + ); +}); + +function GripIcon() { + return ( + + + + ); +} diff --git a/apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx b/apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx new file mode 100644 index 000000000..e40c7baac --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import { useTableHandleState } from "./hooks/use-table-handle-state"; +import { ColumnHandle } from "./column-handle"; +import { RowHandle } from "./row-handle"; +import { CellChevron } from "./cell-chevron"; + +interface TableHandlesLayerProps { + editor: Editor | null; +} + +export const TableHandlesLayer = React.memo(function TableHandlesLayer({ + editor, +}: TableHandlesLayerProps) { + const state = useTableHandleState(editor); + + if (!editor || !editor.isEditable) return null; + if (!state.hoveringCell || !state.tableNode || state.tablePos == null) return null; + + return ( + <> + + + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/table-background-color.tsx b/apps/client/src/features/editor/components/table/table-background-color.tsx index 3e4ce6168..c0df52d81 100644 --- a/apps/client/src/features/editor/components/table/table-background-color.tsx +++ b/apps/client/src/features/editor/components/table/table-background-color.tsx @@ -22,7 +22,7 @@ interface TableBackgroundColorProps { editor: Editor | null; } -const TABLE_COLORS: TableColorItem[] = [ +export const TABLE_COLORS: TableColorItem[] = [ { name: "Default", color: "" }, { name: "Blue", color: "#b4d5ff" }, { name: "Green", color: "#acf5d2" }, diff --git a/apps/client/src/features/editor/components/table/table-menu.tsx b/apps/client/src/features/editor/components/table/table-menu.tsx index 4adafb206..3be7ec539 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -104,12 +104,12 @@ export const TableMenu = React.memo( element.style.zIndex = "99"; }} options={{ - placement: "top", + placement: "bottom", offset: { mainAxis: 15, }, flip: { - fallbackPlacements: ["top", "bottom"], + fallbackPlacements: ["bottom", "top"], padding: { top: 35 + 15, left: 8, right: 8, bottom: -Infinity }, boundary: editor.options.element as HTMLElement, }, diff --git a/apps/client/src/features/editor/components/table/table-text-alignment.tsx b/apps/client/src/features/editor/components/table/table-text-alignment.tsx index 4d4646cf5..17ef7c42e 100644 --- a/apps/client/src/features/editor/components/table/table-text-alignment.tsx +++ b/apps/client/src/features/editor/components/table/table-text-alignment.tsx @@ -86,11 +86,11 @@ export const TableTextAlignment: FC = ({ editor }) => { transitionProps={{ transition: "pop" }} > - + setOpened(!opened)} > diff --git a/apps/client/src/features/editor/extensions/drag-handle.ts b/apps/client/src/features/editor/extensions/drag-handle.ts index a4843ed67..6b10678a1 100644 --- a/apps/client/src/features/editor/extensions/drag-handle.ts +++ b/apps/client/src/features/editor/extensions/drag-handle.ts @@ -60,6 +60,23 @@ function nodeDOMAtCoords( options: GlobalDragHandleOptions, view: EditorView, ) { + // Custom nodes (transclusion, …) render via tiptap's React node-view + // renderer, which emits `class="react-renderer node-${name}"` on the + // live wrapper — the `data-type` attribute is for static HTML + // serialization only. Match both so we cover live and parsed DOM. + // Inside a custom node, also match plain `p` so the first paragraph + // (which doesn't match `:not(:first-child)`) still gets its own + // handle; only hovers on the custom node's padding/border fall + // through to the wrapper. + const customSelectors = options.customNodes.flatMap((node) => [ + `[data-type=${node}]`, + `.node-${node}`, + ]); + const customParagraphSelectors = options.customNodes.flatMap((node) => [ + `[data-type=${node}] p`, + `.node-${node} p`, + ]); + const selectors = [ "li", "p:not(:first-child)", @@ -71,7 +88,13 @@ function nodeDOMAtCoords( "h4", "h5", "h6", - ...options.customNodes.map((node) => `[data-type=${node}]`), + // Tables nested in another block (toggle, transclusion, …) have a + // wrapper that isn't a direct child of .ProseMirror, so the + // parent-check below skips it. Match the wrapper explicitly so the + // handle shows up even with empty cells. + ".tableWrapper", + ...customParagraphSelectors, + ...customSelectors, ].join(", "); return document .elementsFromPoint(coords.x, coords.y) @@ -99,6 +122,22 @@ function nodePosAtDOM( })?.inside; } +function isCustomNodeDOM( + elem: Element | null | undefined, + options: GlobalDragHandleOptions, +): boolean { + if (!elem) return false; + for (const name of options.customNodes) { + if ( + elem.getAttribute("data-type") === name || + elem.classList.contains(`node-${name}`) + ) { + return true; + } + } + return false; +} + function calcNodePos(pos: number, view: EditorView) { const $pos = view.state.doc.resolve(pos); if ($pos.depth > 1) return $pos.before($pos.depth); @@ -137,7 +176,6 @@ export function DragHandlePlugin( const nodePos = view.state.doc.resolve(fromSelectionPos); - // Check if nodePos points to the top level node if (nodePos.node().type.name === "doc") differentNodeSelected = true; else { const nodeSelection = NodeSelection.create( @@ -166,14 +204,46 @@ export function DragHandlePlugin( } else { selection = NodeSelection.create(view.state.doc, draggedNodePos); - // if inline node is selected, e.g mention -> go to the parent node to select the whole node - // if table row is selected, go to the parent node to select the whole node - if ( - (selection as NodeSelection).node.type.isInline || - (selection as NodeSelection).node.type.name === "tableRow" - ) { - let $pos = view.state.doc.resolve(selection.from); - selection = NodeSelection.create(view.state.doc, $pos.before()); + const $sel = view.state.doc.resolve(selection.from); + + if (isCustomNodeDOM(node, options)) { + // The drag landed on a custom-node container (transclusion etc.). + // Walk up to the matching node so the drag moves the whole + // container, not whatever inner element the click landed on. + const customTypes = new Set(options.customNodes); + for (let d = $sel.depth; d > 0; d--) { + if (customTypes.has($sel.node(d).type.name)) { + selection = NodeSelection.create( + view.state.doc, + $sel.before(d), + ); + break; + } + } + } else { + // If the selected node lives inside a table (at any nesting + // depth), promote to the whole table — the global drag handle is + // meant to move the table as a single block, not a row/cell. The + // earlier tableRow-only check only worked when the table sat at + // the doc root; once wrapped in another node (toggle, layout, + // etc.) the selection lands on a cell/paragraph and that check + // never fired. + let tableDepth = -1; + for (let d = $sel.depth; d > 0; d--) { + if ($sel.node(d).type.name === "table") { + tableDepth = d; + break; + } + } + if (tableDepth > 0) { + selection = NodeSelection.create( + view.state.doc, + $sel.before(tableDepth), + ); + } else if ((selection as NodeSelection).node.type.isInline) { + // Inline node (e.g. mention): walk up to the parent block. + selection = NodeSelection.create(view.state.doc, $sel.before()); + } } } view.dispatch(view.state.tr.setSelection(selection)); @@ -313,6 +383,27 @@ export function DragHandlePlugin( return; } + const isCustomNode = isCustomNodeDOM(node, options); + + // Custom nodes pin the handle to the inner NodeViewWrapper's top-left: + // the natural anchor sits in transient/empty space outside the visible block. + if (isCustomNode) { + // tiptap React node-views emit an outer `.react-renderer` whose first + // child is the visible NodeViewWrapper; walk to that outer first since + // `node` may be either the outer or an inner element with data-type. + const rendererOuter = + (node.closest(".react-renderer") as HTMLElement | null) ?? node; + const inner = + (rendererOuter.firstElementChild as HTMLElement | null) ?? + rendererOuter; + const innerRect = absoluteRect(inner); + if (!dragHandleElement) return; + dragHandleElement.style.left = `${innerRect.left + 4}px`; + dragHandleElement.style.top = `${innerRect.top + 4}px`; + showDragHandle(); + return; + } + const compStyle = window.getComputedStyle(node); const parsedLineHeight = parseInt(compStyle.lineHeight, 10); const lineHeight = isNaN(parsedLineHeight) @@ -328,6 +419,13 @@ export function DragHandlePlugin( if (node.matches("ul:not([data-type=taskList]) li, ol li")) { rect.left -= options.dragHandleWidth; } + // Tables: clear the table's own row-drag handle so the two + // grips don't stack on each other. `nodeDOMAtCoords` returns + // the wrapper for top-level hovers (wrapper is direct child of + // .ProseMirror) and a descendant for deeper hovers — cover both. + if (node.closest(".tableWrapper")) { + rect.left -= options.dragHandleWidth; + } rect.width = options.dragHandleWidth; if (!dragHandleElement) return; diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 23be85aa1..1f09bef37 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -45,6 +45,9 @@ import { SearchAndReplace, Mention, TableDndExtension, + TableHandleCommandsExtension, + TableHeaderPin, + TableReadonlySort, Subpages, Heading, Highlight, @@ -56,6 +59,7 @@ import { Status, TransclusionSource, TransclusionReference, + TableView, } from "@docmost/editor-ext"; import { randomElement, @@ -259,11 +263,16 @@ export const mainExtensions = [ resizable: true, lastColumnResizable: true, allowTableNodeSelection: true, + cellMinWidth: 49, + View: TableView, }), TableRow, TableCell, TableHeader, TableDndExtension, + TableHandleCommandsExtension, + TableHeaderPin, + TableReadonlySort, MathInline.configure({ view: MathInlineView, }), diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index 57aab5bb0..4e2fcccf6 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -44,6 +44,7 @@ import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubbl import { ReadonlyBubbleMenu } from "@/features/editor/components/bubble-menu/readonly-bubble-menu"; import TableCellMenu from "@/features/editor/components/table/table-cell-menu.tsx"; import TableMenu from "@/features/editor/components/table/table-menu.tsx"; +import { TableHandlesLayer } from "@/features/editor/components/table/handle/table-handles-layer"; import ImageMenu from "@/features/editor/components/image/image-menu.tsx"; import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx"; import VideoMenu from "@/features/editor/components/video/video-menu.tsx"; @@ -424,7 +425,7 @@ export default function PageEditor({ - + diff --git a/apps/client/src/features/editor/styles/core.css b/apps/client/src/features/editor/styles/core.css index 34ddaca3c..077570fb5 100644 --- a/apps/client/src/features/editor/styles/core.css +++ b/apps/client/src/features/editor/styles/core.css @@ -203,7 +203,8 @@ } } - .resize-cursor { + &.resize-cursor, + &.resize-cursor * { cursor: ew-resize; cursor: col-resize; } diff --git a/apps/client/src/features/editor/styles/table.css b/apps/client/src/features/editor/styles/table.css index 9926d0bc0..5d802e4ab 100644 --- a/apps/client/src/features/editor/styles/table.css +++ b/apps/client/src/features/editor/styles/table.css @@ -15,7 +15,8 @@ } .table-dnd-drop-indicator { - background-color: #adf; + background-color: var(--mantine-color-blue-5); + z-index: 3; } .ProseMirror { @@ -57,13 +58,14 @@ } .column-resize-handle { - background-color: #adf; + background-color: var(--mantine-color-blue-5); bottom: -1px; position: absolute; - right: -2px; + right: -1px; pointer-events: none; top: 0; - width: 4px; + width: 2px; + z-index: 3; } .selectedCell:after { @@ -129,6 +131,139 @@ } } + +/* Header-row pinning. Two CSS paths, picked by `header-pin/controller.ts`: + - native sticky (preferred): wrapper drops its overflow constraint so + `position: sticky` on the row can resolve against the document scroll. + - transform fallback: wrapper keeps `overflow-x: auto` for horizontal + scrolling; the row is positioned imperatively per scroll frame. + + `--editor-pin-offset` is published to :root by `pinOffsetWatcher` in + `header-pin/offset.ts`, measured against the lowest fixed surface above + the editor (app shell header, page header, fixed toolbar). */ + +.tableWrapper.tableWrapperNoOverflow, +.tableWrapper.tableWrapperNoOverflow table { + overflow: visible; +} + +.tableWrapper.tableHeaderPinned table tr:first-child { + z-index: 2; +} + +.tableWrapper.tableWrapperNoOverflow.tableHeaderPinned table tr:first-child { + position: sticky; + top: var(--editor-pin-offset, 90px); +} + +.tableWrapper.tableHeaderPinned:not(.tableWrapperNoOverflow) table tr:first-child { + position: relative; + transform: translateY(var(--table-pin-offset, 0px)); +} + +@media print { + .tableWrapper.tableHeaderPinned table tr:first-child { + position: static; + transform: none; + } +} + +.tableReadonlySortChevron { + /* Anchor to the cell's right edge, vertically centered with the cell + content. The cell content (a

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

    +
    diff --git a/packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts b/packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts deleted file mode 100644 index 9b8304d54..000000000 --- a/packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { DraggingDOMs } from "./utils"; - -const EDGE_THRESHOLD = 100; -const SCROLL_SPEED = 10; - -export class AutoScrollController { - private _autoScrollInterval?: number; - - checkYAutoScroll = (clientY: number) => { - const scrollContainer = document.documentElement; - - if (clientY < 0 + EDGE_THRESHOLD) { - this._startYAutoScroll(scrollContainer!, -1 * SCROLL_SPEED); - } else if (clientY > window.innerHeight - EDGE_THRESHOLD) { - this._startYAutoScroll(scrollContainer!, SCROLL_SPEED); - } else { - this._stopYAutoScroll(); - } - } - - checkXAutoScroll = (clientX: number, draggingDOMs: DraggingDOMs) => { - const table = draggingDOMs?.table; - if (!table) return; - - const scrollContainer = table.closest('.tableWrapper'); - const editorRect = scrollContainer.getBoundingClientRect(); - if (!scrollContainer) return; - - if (clientX < editorRect.left + EDGE_THRESHOLD) { - this._startXAutoScroll(scrollContainer!, -1 * SCROLL_SPEED); - } else if (clientX > editorRect.right - EDGE_THRESHOLD) { - this._startXAutoScroll(scrollContainer!, SCROLL_SPEED); - } else { - this._stopXAutoScroll(); - } - } - - stop = () => { - this._stopXAutoScroll(); - this._stopYAutoScroll(); - } - - private _startXAutoScroll = (scrollContainer: HTMLElement, speed: number) => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - } - - this._autoScrollInterval = window.setInterval(() => { - scrollContainer.scrollLeft += speed; - }, 16); - } - - private _stopXAutoScroll = () => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - this._autoScrollInterval = undefined; - } - } - - private _startYAutoScroll = (scrollContainer: HTMLElement, speed: number) => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - } - - this._autoScrollInterval = window.setInterval(() => { - scrollContainer.scrollTop += speed; - }, 16); - } - - private _stopYAutoScroll = () => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - this._autoScrollInterval = undefined; - } - } -} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts index 1ad57ec1f..b4ca516ae 100644 --- a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts +++ b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts @@ -1,316 +1,393 @@ import { Editor, Extension } from "@tiptap/core"; -import { PluginKey, Plugin, PluginSpec } from "@tiptap/pm/state"; +import { PluginKey, Plugin, PluginSpec, TextSelection, Transaction } from "@tiptap/pm/state"; +import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { EditorProps, EditorView } from "@tiptap/pm/view"; +import { columnResizingPluginKey } from "@tiptap/pm/tables"; +import { cellAround } from "@tiptap/pm/tables"; import { + cellInfoFromResolvedCell, DraggingDOMs, getDndRelatedDOMs, getHoveringCell, HoveringCellInfo, } from "./utils"; import { getDragOverColumn, getDragOverRow } from "./calc-drag-over"; +import { findTable } from "../utils/query"; import { moveColumn, moveRow } from "../utils"; import { PreviewController } from "./preview/preview-controller"; import { DropIndicatorController } from "./preview/drop-indicator-controller"; -import { DragHandleController } from "./handle/drag-handle-controller"; -import { EmptyImageController } from "./handle/empty-image-controller"; -import { AutoScrollController } from "./auto-scroll-controller"; -export const TableDndKey = new PluginKey("table-drag-and-drop"); +export interface TableHandleState { + hoveringCell: HoveringCellInfo | null; + tableNode: ProseMirrorNode | null; + tablePos: number | null; + dragging: { orientation: "col" | "row"; index: number } | null; + frozen: boolean; +} -class TableDragHandlePluginSpec implements PluginSpec { +const INITIAL_STATE: TableHandleState = { + hoveringCell: null, + tableNode: null, + tablePos: null, + dragging: null, + frozen: false, +}; + +export const TableDndKey = new PluginKey("table-handles"); + +class TableHandlePluginSpec implements PluginSpec { key = TableDndKey; - props: EditorProps>; + props: EditorProps>; + + private _previewController: PreviewController; + private _dropIndicatorController: DropIndicatorController; - private _colDragHandle: HTMLElement; - private _rowDragHandle: HTMLElement; private _hoveringCell?: HoveringCellInfo; private _disposables: (() => void)[] = []; - private _draggingCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _dragging = false; private _draggingDirection: "col" | "row" = "col"; private _draggingIndex = -1; private _droppingIndex = -1; - private _draggingDOMs?: DraggingDOMs | undefined; - private _startCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _previewController: PreviewController; - private _dropIndicatorController: DropIndicatorController; - private _dragHandleController: DragHandleController; - private _emptyImageController: EmptyImageController; - private _autoScrollController: AutoScrollController; + private _draggingDOMs?: DraggingDOMs; + private _startCoords = { x: 0, y: 0 }; + private _dragging = false; + + state = { + init: (): TableHandleState => INITIAL_STATE, + apply: (tr: Transaction, prev: TableHandleState): TableHandleState => { + const meta = tr.getMeta(TableDndKey) as Partial | null; + if (!meta) return prev; + let changed = false; + for (const key in meta) { + if (!Object.is(prev[key as keyof TableHandleState], meta[key as keyof TableHandleState])) { + changed = true; + break; + } + } + return changed ? { ...prev, ...meta } : prev; + }, + }; constructor(public editor: Editor) { this.props = { handleDOMEvents: { - pointerover: this._pointerOver, + pointermove: this._pointerMove, + // Force-unfreeze on any pointerdown that lands on the editor. + // Mantine's `Menu.onClose` doesn't always fire on outside click + // (the dropdown vanishes visually but the callback is skipped), + // which would otherwise leave `frozen=true` permanently. + pointerdown: this._pointerDown, }, }; - this._dragHandleController = new DragHandleController(); - this._colDragHandle = this._dragHandleController.colDragHandle; - this._rowDragHandle = this._dragHandleController.rowDragHandle; - this._previewController = new PreviewController(); this._dropIndicatorController = new DropIndicatorController(); - this._emptyImageController = new EmptyImageController(); - - this._autoScrollController = new AutoScrollController(); - - this._bindDragEvents(); } view = () => { const wrapper = this.editor.options.element; - //@ts-ignore - wrapper.appendChild(this._colDragHandle); - //@ts-ignore - wrapper.appendChild(this._rowDragHandle); - //@ts-ignore + // @ts-ignore wrapper.appendChild(this._previewController.previewRoot); - //@ts-ignore + // @ts-ignore wrapper.appendChild(this._dropIndicatorController.dropIndicatorRoot); + // Track the cursor cell so handles follow keyboard nav and clicks too. + this.editor.on("selectionUpdate", this._onSelectionUpdate); + this._disposables.push(() => + this.editor.off("selectionUpdate", this._onSelectionUpdate), + ); + return { - update: this.update, destroy: this.destroy, }; }; - update = () => {}; - destroy = () => { - if (!this.editor.isDestroyed) return; - this._dragHandleController.destroy(); - this._emptyImageController.destroy(); this._previewController.destroy(); this._dropIndicatorController.destroy(); - this._autoScrollController.stop(); - - this._disposables.forEach((disposable) => disposable()); + this._disposables.forEach((d) => d()); }; - private _pointerOver = (view: EditorView, event: PointerEvent) => { - if (this._dragging) return; + private _pointerDown = (view: EditorView, _event: PointerEvent): boolean => { + const current = TableDndKey.getState(view.state); + if (current?.frozen) this.editor.commands.unfreezeHandles(); + return false; + }; + + private _pointerMove = (view: EditorView, event: PointerEvent) => { + const current = TableDndKey.getState(view.state); + if (current?.frozen || current?.dragging) return; + + const resizeState = columnResizingPluginKey.getState(view.state); + if (resizeState?.dragging) return; - // Don't show drag handles in readonly mode if (!this.editor.isEditable) { - this._dragHandleController.hide(); + if (current?.hoveringCell == null && current?.tableNode == null && current?.tablePos == null) return; + this._dispatchMeta({ hoveringCell: null, tableNode: null, tablePos: null }); return; } const hoveringCell = getHoveringCell(view, event); - this._hoveringCell = hoveringCell; - if (!hoveringCell) { - this._dragHandleController.hide(); - } else { - this._dragHandleController.show(this.editor, hoveringCell); + if (hoveringCell) { + if (current?.hoveringCell?.cellPos === hoveringCell.cellPos) return; + this._hoveringCell = hoveringCell; + const $cell = view.state.doc.resolve(hoveringCell.cellPos); + const tableInfo = findTable($cell); + this._dispatchMeta({ + hoveringCell, + tableNode: tableInfo?.node ?? null, + tablePos: tableInfo?.pos ?? null, + }); + return; } + + // Pointer isn't over a cell but may be transiting toward a handle that + // floats outside the cell — fall back to the selection's cell so the + // handles stay visible. + const $cellPos = cellAround(view.state.selection.$head); + if ($cellPos) { + const cellInfo = cellInfoFromResolvedCell($cellPos); + if (current?.hoveringCell?.cellPos === cellInfo.cellPos) return; + this._hoveringCell = cellInfo; + const tableInfo = findTable($cellPos); + this._dispatchMeta({ + hoveringCell: cellInfo, + tableNode: tableInfo?.node ?? null, + tablePos: tableInfo?.pos ?? null, + }); + return; + } + + this._hoveringCell = undefined; + if (current?.hoveringCell == null && current?.tableNode == null && current?.tablePos == null) return; + this._dispatchMeta({ hoveringCell: null, tableNode: null, tablePos: null }); }; - private _onDragColStart = (event: DragEvent) => { - this._onDragStart(event, "col"); + private _onSelectionUpdate = () => { + if (!this.editor.isEditable) return; + + const current = TableDndKey.getState(this.editor.state); + if (current?.frozen || current?.dragging) return; + + const $cellPos = cellAround(this.editor.state.selection.$head); + if (!$cellPos) return; + + const cellInfo = cellInfoFromResolvedCell($cellPos); + if (current?.hoveringCell?.cellPos === cellInfo.cellPos) return; + + this._hoveringCell = cellInfo; + const tableInfo = findTable($cellPos); + this._dispatchMeta({ + hoveringCell: cellInfo, + tableNode: tableInfo?.node ?? null, + tablePos: tableInfo?.pos ?? null, + }); }; - private _onDraggingCol = (event: DragEvent) => { + private _dispatchMeta = (patch: Partial) => { + const tr = this.editor.state.tr.setMeta(TableDndKey, patch); + tr.setMeta("addToHistory", false); + this.editor.view.dispatch(tr); + }; + + // ---- Public API for the React handle layer ---- + + // Returns true if the drag was set up successfully. + startDragFromHandle = ( + orientation: "col" | "row", + clientX: number, + clientY: number, + ): boolean => { + if (!this._hoveringCell) return false; + this._dragging = true; + this._draggingDirection = orientation; + this._startCoords = { x: clientX, y: clientY }; + + const draggingIndex = + (orientation === "col" + ? this._hoveringCell.colIndex + : this._hoveringCell.rowIndex) ?? 0; + this._draggingIndex = draggingIndex; + + const relatedDoms = getDndRelatedDOMs( + this.editor.view, + this._hoveringCell.cellPos, + draggingIndex, + orientation, + ); + if (!relatedDoms) { + this._dragging = false; + return false; + } + this._draggingDOMs = relatedDoms; + + this._previewController.onDragStart(relatedDoms, draggingIndex, orientation); + this._dropIndicatorController.onDragStart(relatedDoms, orientation); + + // Park the selection inside the dragged cell unless it's already in the + // same table. PM auto-maps `selection.from` through concurrent remote + // transactions, so commitDrop can resolve the table even if the doc + // shifted mid-drag — same trick the pre-pragmatic-dnd implementation + // relied on. + const state = this.editor.state; + const currentTable = findTable(state.selection.$from); + const hoverTable = (() => { + try { + return findTable(state.doc.resolve(this._hoveringCell.cellPos)); + } catch { + return undefined; + } + })(); + const tr = state.tr; + if ( + hoverTable && + (!currentTable || currentTable.pos !== hoverTable.pos) + ) { + try { + const $inside = state.doc.resolve(this._hoveringCell.cellPos + 1); + tr.setSelection(TextSelection.near($inside, 1)); + } catch {} + } + tr.setMeta(TableDndKey, { + dragging: { orientation, index: draggingIndex }, + }); + tr.setMeta("addToHistory", false); + this.editor.view.dispatch(tr); + return true; + }; + + updateDragPosition = (clientX: number, clientY: number) => { const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; + if (!draggingDOMs || !this._dragging) return; - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging( - draggingDOMs, - this._draggingCoords.x, - this._draggingCoords.y, - "col", - ); + if (this._draggingDirection === "col") { + this._previewController.onDragging( + draggingDOMs, + clientX, + clientY, + "col", + ); + const direction = this._startCoords.x > clientX ? "left" : "right"; + const dragOverColumn = getDragOverColumn(draggingDOMs.table, clientX); + if (!dragOverColumn) return; + const [col, index] = dragOverColumn; + this._droppingIndex = index; + this._dropIndicatorController.onDragging(col, direction, "col"); + return; + } - this._autoScrollController.checkXAutoScroll(event.clientX, draggingDOMs); - - const direction = - this._startCoords.x > this._draggingCoords.x ? "left" : "right"; - const dragOverColumn = getDragOverColumn( - draggingDOMs.table, - this._draggingCoords.x, - ); - if (!dragOverColumn) return; - - const [col, index] = dragOverColumn; - this._droppingIndex = index; - this._dropIndicatorController.onDragging(col, direction, "col"); - }; - - private _onDragRowStart = (event: DragEvent) => { - this._onDragStart(event, "row"); - }; - - private _onDraggingRow = (event: DragEvent) => { - const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; - - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging( - draggingDOMs, - this._draggingCoords.x, - this._draggingCoords.y, - "row", - ); - - this._autoScrollController.checkYAutoScroll(event.clientY); - - const direction = - this._startCoords.y > this._draggingCoords.y ? "up" : "down"; - const dragOverRow = getDragOverRow( - draggingDOMs.table, - this._draggingCoords.y, - ); + this._previewController.onDragging(draggingDOMs, clientX, clientY, "row"); + const direction = this._startCoords.y > clientY ? "up" : "down"; + const dragOverRow = getDragOverRow(draggingDOMs.table, clientY); if (!dragOverRow) return; - const [row, index] = dragOverRow; this._droppingIndex = index; this._dropIndicatorController.onDragging(row, direction, "row"); }; - private _onDragEnd = () => { - this._dragging = false; - this._draggingIndex = -1; - this._droppingIndex = -1; - this._startCoords = { x: 0, y: 0 }; - this._autoScrollController.stop(); - this._dropIndicatorController.onDragEnd(); - this._previewController.onDragEnd(); - }; - - private _bindDragEvents = () => { - this._colDragHandle.addEventListener("dragstart", this._onDragColStart); - this._disposables.push(() => { - this._colDragHandle.removeEventListener( - "dragstart", - this._onDragColStart, - ); - }); - - this._colDragHandle.addEventListener("dragend", this._onDragEnd); - this._disposables.push(() => { - this._colDragHandle.removeEventListener("dragend", this._onDragEnd); - }); - - this._rowDragHandle.addEventListener("dragstart", this._onDragRowStart); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener( - "dragstart", - this._onDragRowStart, - ); - }); - - this._rowDragHandle.addEventListener("dragend", this._onDragEnd); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener("dragend", this._onDragEnd); - }); - - const ownerDocument = this.editor.view.dom?.ownerDocument; - if (ownerDocument) { - // To make `drop` event work, we need to prevent the default behavior of the - // `dragover` event for drop zone. Here we set the whole document as the - // drop zone so that even the mouse moves outside the editor, the `drop` - // event will still be triggered. - ownerDocument.addEventListener("drop", this._onDrop); - ownerDocument.addEventListener("dragover", this._onDrag); - this._disposables.push(() => { - ownerDocument.removeEventListener("drop", this._onDrop); - ownerDocument.removeEventListener("dragover", this._onDrag); - }); - } - }; - - private _onDragStart = (event: DragEvent, type: "col" | "row") => { - const dataTransfer = event.dataTransfer; - if (dataTransfer) { - dataTransfer.effectAllowed = "move"; - this._emptyImageController.hideDragImage(dataTransfer); - } - this._dragging = true; - this._draggingDirection = type; - this._startCoords = { x: event.clientX, y: event.clientY }; - const draggingIndex = - (type === "col" - ? this._hoveringCell?.colIndex - : this._hoveringCell?.rowIndex) ?? 0; - - this._draggingIndex = draggingIndex; - - const relatedDoms = getDndRelatedDOMs( - this.editor.view, - this._hoveringCell?.cellPos, - draggingIndex, - type, - ); - this._draggingDOMs = relatedDoms; - - const index = - type === "col" - ? this._hoveringCell?.colIndex - : this._hoveringCell?.rowIndex; - - this._previewController.onDragStart(relatedDoms, index, type); - this._dropIndicatorController.onDragStart(relatedDoms, type); - }; - - private _onDrag = (event: DragEvent) => { - event.preventDefault(); - if (!this._dragging) return; - if (this._draggingDirection === "col") { - this._onDraggingCol(event); - } else { - this._onDraggingRow(event); - } - }; - - private _onDrop = () => { + commitDrop = () => { if (!this._dragging) return; const direction = this._draggingDirection; const from = this._draggingIndex; const to = this._droppingIndex; + + if (from < 0 || to < 0 || from === to) return; + + // Use the live (auto-mapped) selection as the table anchor — PM has + // already mapped it through any concurrent remote transactions, so + // it's safe to resolve even if the doc shifted mid-drag. const tr = this.editor.state.tr; const pos = this.editor.state.selection.from; if (direction === "col") { - const canMove = moveColumn({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }); - if (canMove) { + if (moveColumn({ tr, originIndex: from, targetIndex: to, select: true, pos })) { this.editor.view.dispatch(tr); } - return; } - - if (direction === "row") { - const canMove = moveRow({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }); - if (canMove) { - this.editor.view.dispatch(tr); - } - - return; + if (moveRow({ tr, originIndex: from, targetIndex: to, select: true, pos })) { + this.editor.view.dispatch(tr); } }; + + endDrag = () => { + this._dragging = false; + this._draggingIndex = -1; + this._droppingIndex = -1; + this._startCoords = { x: 0, y: 0 }; + this._draggingDOMs = undefined; + this._dropIndicatorController.onDragEnd(); + this._previewController.onDragEnd(); + this._dispatchMeta({ dragging: null }); + }; +} + +export type { TableHandlePluginSpec }; + +// Resolve via plugin key, not a module singleton — survives StrictMode / HMR. +export function getTableHandlePluginSpec( + editor: Editor, +): TableHandlePluginSpec | null { + const plugin = TableDndKey.get(editor.state); + if (!plugin) return null; + return plugin.spec as unknown as TableHandlePluginSpec; } export const TableDndExtension = Extension.create({ name: "table-drag-and-drop", addProseMirrorPlugins() { const editor = this.editor; - - const dragHandlePluginSpec = new TableDragHandlePluginSpec(editor); - const dragHandlePlugin = new Plugin(dragHandlePluginSpec); - - return [dragHandlePlugin]; + const spec = new TableHandlePluginSpec(editor); + return [new Plugin(spec)]; }, }); + +export const TableHandleCommandsExtension = Extension.create({ + name: "table-handle-commands", + addCommands() { + return { + freezeHandles: + () => + ({ tr, dispatch }) => { + if (dispatch) { + tr.setMeta(TableDndKey, { frozen: true }); + tr.setMeta("addToHistory", false); + } + return true; + }, + unfreezeHandles: + () => + ({ tr, state, dispatch }) => { + if (dispatch) { + // Re-sync `hoveringCell` to the cursor's cell as we unfreeze: + // `selectionUpdate` was gated while frozen, so the stored + // hoveringCell may be stale. + const patch: Partial = { frozen: false }; + const $cellPos = cellAround(state.selection.$head); + if ($cellPos) { + const cellInfo = cellInfoFromResolvedCell($cellPos); + const tableInfo = findTable($cellPos); + patch.hoveringCell = cellInfo; + patch.tableNode = tableInfo?.node ?? null; + patch.tablePos = tableInfo?.pos ?? null; + } else { + patch.hoveringCell = null; + patch.tableNode = null; + patch.tablePos = null; + } + tr.setMeta(TableDndKey, patch); + tr.setMeta("addToHistory", false); + } + return true; + }, + }; + }, +}); + +declare module "@tiptap/core" { + interface Commands { + tableHandleCommands: { + freezeHandles: () => ReturnType; + unfreezeHandles: () => ReturnType; + }; + } +} diff --git a/packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts b/packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts deleted file mode 100644 index 33137e91f..000000000 --- a/packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { HoveringCellInfo } from "../utils"; -import { computePosition, offset } from "@floating-ui/dom"; - -export class DragHandleController { - private _colDragHandle: HTMLElement; - private _rowDragHandle: HTMLElement; - - constructor() { - this._colDragHandle = this._createDragHandleDom('col'); - this._rowDragHandle = this._createDragHandleDom('row'); - } - - get colDragHandle() { - return this._colDragHandle; - } - - get rowDragHandle() { - return this._rowDragHandle; - } - - show = (editor: Editor, hoveringCell: HoveringCellInfo) => { - this._showColDragHandle(editor, hoveringCell); - this._showRowDragHandle(editor, hoveringCell); - } - - hide = () => { - Object.assign(this._colDragHandle.style, { - display: 'none', - left: '-999px', - top: '-999px', - }); - Object.assign(this._rowDragHandle.style, { - display: 'none', - left: '-999px', - top: '-999px', - }); - } - - destroy = () => { - this._colDragHandle.remove() - this._rowDragHandle.remove() - } - - private _createDragHandleDom = (type: 'col' | 'row') => { - const dragHandle = document.createElement('div') - dragHandle.classList.add('drag-handle') - dragHandle.setAttribute('draggable', 'true') - dragHandle.setAttribute('data-direction', type === 'col' ? 'horizontal' : 'vertical') - dragHandle.setAttribute('data-drag-handle', '') - Object.assign(dragHandle.style, { - position: 'absolute', - top: '-999px', - left: '-999px', - display: 'none', - }) - return dragHandle; - } - - private _showColDragHandle(editor: Editor, hoveringCell: HoveringCellInfo) { - const referenceCell = editor.view.nodeDOM(hoveringCell.colFirstCellPos); - if (!referenceCell) return; - - const yOffset = -1 * parseInt(getComputedStyle(this._colDragHandle).height) / 2; - - computePosition( - referenceCell as HTMLElement, - this._colDragHandle, - { - placement: 'top', - middleware: [offset(yOffset)] - } - ) - .then(({ x, y }) => { - Object.assign(this._colDragHandle.style, { - display: 'block', - top: `${y}px`, - left: `${x}px`, - }); - }) - } - - private _showRowDragHandle(editor: Editor, hoveringCell: HoveringCellInfo) { - const referenceCell = editor.view.nodeDOM(hoveringCell.rowFirstCellPos); - if (!referenceCell) return; - - const xOffset = -1 * parseInt(getComputedStyle(this._rowDragHandle).width) / 2; - - computePosition( - referenceCell as HTMLElement, - this._rowDragHandle, - { - middleware: [offset(xOffset)], - placement: 'left' - } - ) - .then(({ x, y}) => { - Object.assign(this._rowDragHandle.style, { - display: 'block', - top: `${y}px`, - left: `${x}px`, - }); - }) - } -} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts b/packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts deleted file mode 100644 index 8848a6b04..000000000 --- a/packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts +++ /dev/null @@ -1,21 +0,0 @@ -export class EmptyImageController { - private _emptyImage: HTMLImageElement; - - constructor() { - this._emptyImage = new Image(1, 1); - this._emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; - } - - get emptyImage() { - return this._emptyImage; - } - - hideDragImage = (dataTransfer: DataTransfer) => { - dataTransfer.effectAllowed = 'move'; - dataTransfer.setDragImage(this._emptyImage, 0, 0); - } - - destroy = () => { - this._emptyImage.remove(); - } -} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/dnd/index.ts b/packages/editor-ext/src/lib/table/dnd/index.ts index cb21bec14..eaeade987 100644 --- a/packages/editor-ext/src/lib/table/dnd/index.ts +++ b/packages/editor-ext/src/lib/table/dnd/index.ts @@ -1 +1,7 @@ -export * from './dnd-extension' \ No newline at end of file +export { + TableDndExtension, + TableHandleCommandsExtension, + TableDndKey, + getTableHandlePluginSpec, +} from "./dnd-extension"; +export type { TableHandleState, TableHandlePluginSpec } from "./dnd-extension"; diff --git a/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts b/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts index 0f0798282..a42c632f7 100644 --- a/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts +++ b/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts @@ -99,4 +99,4 @@ export class DropIndicatorController { }); } -} \ No newline at end of file +} diff --git a/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts b/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts index b7a0ea40d..9884f00f6 100644 --- a/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts +++ b/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts @@ -1,4 +1,4 @@ -import { computePosition, offset, ReferenceElement } from "@floating-ui/dom"; +import { computePosition, offset, shift, ReferenceElement } from "@floating-ui/dom"; import { DraggingDOMs } from "../utils"; import { clearPreviewDOM, createPreviewDOM } from "./render-preview"; @@ -23,7 +23,7 @@ export class PreviewController { onDragStart = (relatedDoms: DraggingDOMs, index: number | undefined, type: 'col' | 'row') => { this._initPreviewStyle(relatedDoms.table, relatedDoms.cell, type); createPreviewDOM(relatedDoms.table, this._preview, index, type) - this._initPreviewPosition(relatedDoms.cell, type); + this._initPreviewPosition(relatedDoms.table, relatedDoms.cell, type); } onDragEnd = () => { @@ -32,7 +32,7 @@ export class PreviewController { } onDragging = (relatedDoms: DraggingDOMs, x: number, y: number, type: 'col' | 'row') => { - this._updatePreviewPosition(x, y, relatedDoms.cell, type); + this._updatePreviewPosition(x, y, relatedDoms.table, relatedDoms.cell, type); } destroy = () => { @@ -60,7 +60,7 @@ export class PreviewController { } } - private _initPreviewPosition(cell: HTMLElement, type: 'col' | 'row') { + private _initPreviewPosition(table: HTMLElement, cell: HTMLElement, type: 'col' | 'row') { void computePosition(cell, this._preview, { placement: type === 'row' ? 'right' : 'bottom', middleware: [ @@ -70,6 +70,7 @@ export class PreviewController { } return -rects.reference.width }), + shift({ boundary: table, padding: 0 }), ], }).then(({ x, y }) => { Object.assign(this._preview.style, { @@ -79,11 +80,20 @@ export class PreviewController { }); } - private _updatePreviewPosition(x: number, y: number, cell: HTMLElement, type: 'col' | 'row') { + // Clamp the preview to within the table's bounds via `shift({ boundary })` + // so it can't track the cursor past the table edge. Without the clamp, + // dragging near the viewport edge pushes the preview's `left` (or `top`) + // beyond the document's natural width/height, the browser extends the + // page to contain it, and the auto-scroll plugin then has a wider area + // to keep scrolling into — a feedback loop that grows the page forever. + private _updatePreviewPosition(x: number, y: number, table: HTMLElement, cell: HTMLElement, type: 'col' | 'row') { computePosition( getVirtualElement(cell, x, y), this._preview, - { placement: type === 'row' ? 'right' : 'bottom' }, + { + placement: type === 'row' ? 'right' : 'bottom', + middleware: [shift({ boundary: table, padding: 0 })], + }, ).then(({ x, y }) => { if (type === 'row') { Object.assign(this._preview.style, { diff --git a/packages/editor-ext/src/lib/table/dnd/utils.ts b/packages/editor-ext/src/lib/table/dnd/utils.ts index d184368f4..9b00769d3 100644 --- a/packages/editor-ext/src/lib/table/dnd/utils.ts +++ b/packages/editor-ext/src/lib/table/dnd/utils.ts @@ -1,4 +1,5 @@ import { cellAround, TableMap } from "@tiptap/pm/tables" +import { ResolvedPos } from "@tiptap/pm/model" import { EditorView } from "@tiptap/pm/view" export function getHoveringCell( @@ -8,19 +9,30 @@ export function getHoveringCell( const domCell = domCellAround(event.target as HTMLElement | null) if (!domCell) return - const { left, top, width, height } = domCell.getBoundingClientRect() - const eventPos = view.posAtCoords({ - // Use the center coordinates of the cell to ensure we're within the - // selected cell. This prevents potential issues when the mouse is on the - // border of two cells. - left: left + width / 2, - top: top + height / 2, - }) - if (!eventPos) return - - const $cellPos = cellAround(view.state.doc.resolve(eventPos.pos)) + // Resolve directly from the cell DOM rather than via coords. The previous + // center-coords approach broke on tall merged cells — their visual center + // can land in empty space whose closest PM position resolves to an + // adjacent cell. `posAtDOM(td, 0)` is always inside this cell, regardless + // of rowspan/colspan. + let pos: number + try { + pos = view.posAtDOM(domCell, 0) + } catch { + return + } + const $cellPos = cellAround(view.state.doc.resolve(pos)) if (!$cellPos) return + return cellInfoFromResolvedCell($cellPos) +} + +/** + * Build HoveringCellInfo from a resolved position whose parent is a + * table cell (i.e. the result of `cellAround` on some inner position). + */ +export function cellInfoFromResolvedCell( + $cellPos: ResolvedPos, +): HoveringCellInfo { const map = TableMap.get($cellPos.node(-1)) const tableStart = $cellPos.start(-1) const cellRect = map.findCell($cellPos.pos - tableStart) diff --git a/packages/editor-ext/src/lib/table/header-pin/controller.ts b/packages/editor-ext/src/lib/table/header-pin/controller.ts new file mode 100644 index 000000000..318d4145d --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/controller.ts @@ -0,0 +1,186 @@ +// Per-table header-pin controller: native sticky when table fits its wrapper, transform fallback when it doesn't. + +import { computePinTop, pinOffsetWatcher } from './offset'; + +const WRAPPER_NO_OVERFLOW = 'tableWrapperNoOverflow'; +const HEADER_PINNED = 'tableHeaderPinned'; +const PIN_OFFSET_VAR = '--table-pin-offset'; + +type PinMode = 'off' | 'native' | 'fallback'; + +function firstRowIsAllHeaders(row: HTMLTableRowElement | null): boolean { + if (!row) return false; + const cells = Array.from(row.cells); + return cells.length > 0 && cells.every((c) => c.tagName === 'TH'); +} + +function isNestedTable(wrapper: HTMLElement): boolean { + return wrapper.closest('table .tableWrapper') !== null; +} + +function isLayoutInert(rect: DOMRectReadOnly): boolean { + return rect.width === 0 && rect.height === 0; +} + +const fallbackControllers = new Set(); +let fallbackScrollListener: (() => void) | null = null; +let fallbackRafPending = false; + +function ensureFallbackListener() { + if (fallbackScrollListener) return; + fallbackScrollListener = () => { + if (fallbackRafPending) return; + fallbackRafPending = true; + requestAnimationFrame(() => { + fallbackRafPending = false; + for (const ctrl of fallbackControllers) ctrl.updateFallbackOffset(); + }); + }; + document.addEventListener('scroll', fallbackScrollListener, { + passive: true, + capture: true, + }); +} + +function maybeTeardownFallbackListener() { + if (!fallbackScrollListener || fallbackControllers.size > 0) return; + document.removeEventListener('scroll', fallbackScrollListener, { + capture: true, + }); + fallbackScrollListener = null; + fallbackRafPending = false; +} + +export class TablePinController { + private wrapper: HTMLElement; + private table: HTMLTableElement; + private fitsObserver?: IntersectionObserver; + private mode: PinMode = 'off'; + private cachedHeaderRow: HTMLTableRowElement | null = null; + + constructor(wrapper: HTMLElement, table: HTMLTableElement) { + this.wrapper = wrapper; + this.table = table; + pinOffsetWatcher.acquire(); + this.fitsObserver = new IntersectionObserver( + (entries) => { + for (const entry of entries) this.evaluateFit(entry); + }, + { root: this.wrapper, threshold: 1 }, + ); + this.fitsObserver.observe(this.table); + } + + private getHeaderRow(): HTMLTableRowElement | null { + if (this.cachedHeaderRow && this.table.contains(this.cachedHeaderRow)) { + return this.cachedHeaderRow; + } + this.cachedHeaderRow = this.table.querySelector('tr'); + return this.cachedHeaderRow; + } + + private evaluateFit(entry: IntersectionObserverEntry) { + if (!this.isEligible()) { + this.apply('off'); + return; + } + if (isLayoutInert(entry.boundingClientRect)) return; + this.apply(entry.isIntersecting ? 'native' : 'fallback'); + } + + private isEligible(): boolean { + return ( + !isNestedTable(this.wrapper) && firstRowIsAllHeaders(this.getHeaderRow()) + ); + } + + private apply(next: PinMode) { + if (next === this.mode) return; + + if (this.mode === 'fallback' && next !== 'fallback') { + fallbackControllers.delete(this); + maybeTeardownFallbackListener(); + } + + this.mode = next; + const cls = this.wrapper.classList; + + if (next === 'off') { + cls.remove(HEADER_PINNED); + cls.remove(WRAPPER_NO_OVERFLOW); + this.wrapper.style.removeProperty(PIN_OFFSET_VAR); + } else if (next === 'native') { + cls.add(HEADER_PINNED); + cls.add(WRAPPER_NO_OVERFLOW); + // Native mode reads --editor-pin-offset from :root; clear stale per-wrapper var from fallback. + this.wrapper.style.removeProperty(PIN_OFFSET_VAR); + } else if (next === 'fallback') { + cls.add(HEADER_PINNED); + cls.remove(WRAPPER_NO_OVERFLOW); + fallbackControllers.add(this); + ensureFallbackListener(); + // Avoid one stale-frame paint under translateY. + this.updateFallbackOffset(); + } + } + + updateFallbackOffset() { + const pinTop = computePinTop(); + const tableRect = this.table.getBoundingClientRect(); + const headerRow = this.getHeaderRow(); + if (!headerRow) return; + const rowHeight = headerRow.getBoundingClientRect().height; + + const active = tableRect.top < pinTop && tableRect.bottom > pinTop + rowHeight; + + if (active) { + const offset = Math.min(pinTop - tableRect.top, tableRect.height - rowHeight); + this.wrapper.style.setProperty(PIN_OFFSET_VAR, `${offset}px`); + } else { + this.wrapper.style.removeProperty(PIN_OFFSET_VAR); + } + } + + refresh() { + // The header may have been replaced by a PM transaction; drop + // the cached reference before checking eligibility. + this.cachedHeaderRow = null; + if (!this.isEligible()) { + this.apply('off'); + return; + } + if (this.mode === 'off') { + // Eligibility just flipped back on; re-trigger the observer so it + // emits the current intersection state. + this.fitsObserver?.unobserve(this.table); + this.fitsObserver?.observe(this.table); + } + } + + destroy() { + this.fitsObserver?.disconnect(); + this.fitsObserver = undefined; + this.apply('off'); + pinOffsetWatcher.release(); + } +} + +const controllers = new WeakMap(); + +export function attach(wrapper: HTMLElement) { + if (controllers.has(wrapper)) return; + const table = wrapper.querySelector(':scope > table') as HTMLTableElement | null; + if (!table) return; + controllers.set(wrapper, new TablePinController(wrapper, table)); +} + +export function detach(wrapper: HTMLElement) { + const ctrl = controllers.get(wrapper); + if (!ctrl) return; + ctrl.destroy(); + controllers.delete(wrapper); +} + +export function getController(wrapper: HTMLElement): TablePinController | undefined { + return controllers.get(wrapper); +} diff --git a/packages/editor-ext/src/lib/table/header-pin/extension.ts b/packages/editor-ext/src/lib/table/header-pin/extension.ts new file mode 100644 index 000000000..8e5157ede --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/extension.ts @@ -0,0 +1,78 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +import { attach, detach, getController } from './controller'; + +const tableHeaderPinKey = new PluginKey('tableHeaderPin'); + +export const TableHeaderPin = Extension.create({ + name: 'tableHeaderPin', + + addProseMirrorPlugins() { + let editorRoot: HTMLElement | null = null; + let domObserver: MutationObserver | null = null; + const tracked = new Set(); + let rafHandle: number | null = null; + + const reconcile = () => { + rafHandle = null; + if (!editorRoot) return; + const current = new Set( + editorRoot.querySelectorAll('.tableWrapper'), + ); + for (const w of tracked) { + if (!current.has(w)) { + detach(w); + tracked.delete(w); + } + } + for (const w of current) { + if (!tracked.has(w)) { + attach(w); + tracked.add(w); + } + } + }; + + const schedule = () => { + if (rafHandle !== null) return; + rafHandle = requestAnimationFrame(reconcile); + }; + + return [ + new Plugin({ + key: tableHeaderPinKey, + + view(editorView) { + editorRoot = editorView.dom as HTMLElement; + + schedule(); + + domObserver = new MutationObserver(schedule); + domObserver.observe(editorRoot, { subtree: true, childList: true }); + + return { + update(view, prevState) { + if (!editorRoot) return; + if (view.state.doc === prevState.doc) return; + editorRoot + .querySelectorAll('.tableWrapper') + .forEach((w) => getController(w)?.refresh()); + }, + destroy() { + if (rafHandle !== null) { + cancelAnimationFrame(rafHandle); + rafHandle = null; + } + domObserver?.disconnect(); + domObserver = null; + for (const w of tracked) detach(w); + tracked.clear(); + editorRoot = null; + }, + }; + }, + }), + ]; + }, +}); diff --git a/packages/editor-ext/src/lib/table/header-pin/index.ts b/packages/editor-ext/src/lib/table/header-pin/index.ts new file mode 100644 index 000000000..b45e01aee --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/index.ts @@ -0,0 +1 @@ +export { TableHeaderPin } from './extension'; diff --git a/packages/editor-ext/src/lib/table/header-pin/offset.ts b/packages/editor-ext/src/lib/table/header-pin/offset.ts new file mode 100644 index 000000000..89cc6bf9e --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/offset.ts @@ -0,0 +1,65 @@ +// Pin-offset measurement and watcher used by the table header-pin controller. + +// Fallback app-bar height (px) when no fixed surface is mounted; matches global-app-shell.tsx. +const APP_BAR_FALLBACK_HEIGHT = 45; + +export const EDITOR_PIN_OFFSET_VAR = '--editor-pin-offset'; + +// Selectors for fixed surfaces between viewport top and editor content. Use data attributes — +// CSS module classes are build-time hashed and won't match. +const PIN_ANCHOR_SELECTORS = [ + '[data-page-header]', + '[data-fixed-toolbar]', +] as const; + +export function computePinTop(): number { + let bottom = APP_BAR_FALLBACK_HEIGHT; + for (const sel of PIN_ANCHOR_SELECTORS) { + const el = document.querySelector(sel) as HTMLElement | null; + if (!el) continue; + const rect = el.getBoundingClientRect(); + if (rect.height > 0 && rect.bottom > bottom) bottom = rect.bottom; + } + return bottom; +} + +// Reference-counted watcher that publishes the editor's top offset to a CSS custom property. +export const pinOffsetWatcher = { + refs: 0, + resizeObserver: null as ResizeObserver | null, + rafPending: false, + lastValue: -1, + + acquire() { + if (this.refs++ > 0) return; + this.publish(); + const schedule = () => { + if (this.rafPending) return; + this.rafPending = true; + requestAnimationFrame(() => { + this.rafPending = false; + this.publish(); + }); + }; + this.resizeObserver = new ResizeObserver(schedule); + this.resizeObserver.observe(document.body); + }, + + release() { + if (--this.refs > 0) return; + this.resizeObserver?.disconnect(); + this.resizeObserver = null; + document.documentElement.style.removeProperty(EDITOR_PIN_OFFSET_VAR); + this.lastValue = -1; + }, + + publish() { + const top = computePinTop(); + if (top === this.lastValue) return; + this.lastValue = top; + document.documentElement.style.setProperty( + EDITOR_PIN_OFFSET_VAR, + `${top}px`, + ); + }, +}; diff --git a/packages/editor-ext/src/lib/table/index.ts b/packages/editor-ext/src/lib/table/index.ts index 9e5a92651..ed06582e3 100644 --- a/packages/editor-ext/src/lib/table/index.ts +++ b/packages/editor-ext/src/lib/table/index.ts @@ -2,4 +2,14 @@ export * from "./row"; export * from "./cell"; export * from "./header"; export * from "./table"; -export * from "./dnd"; \ No newline at end of file +export * from "./dnd"; +export * from "./table-view"; +export * from "./header-pin"; +export * from "./table-readonly-sort"; +export { moveColumn } from "./utils/move-column"; +export type { MoveColumnParams } from "./utils/move-column"; +export { moveRow } from "./utils/move-row"; +export type { MoveRowParams } from "./utils/move-row"; +export { convertTableNodeToArrayOfRows } from "./utils/convert-table-node-to-array-of-rows"; +export { convertArrayOfRowsToTableNode } from "./utils/convert-array-of-rows-to-table-node"; +export { transpose } from "./utils/transpose"; diff --git a/packages/editor-ext/src/lib/table/table-readonly-sort.ts b/packages/editor-ext/src/lib/table/table-readonly-sort.ts new file mode 100644 index 000000000..3e246a411 --- /dev/null +++ b/packages/editor-ext/src/lib/table/table-readonly-sort.ts @@ -0,0 +1,233 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +type SortDirection = 'asc' | 'desc'; + +type SortState = { + col: number; + direction: SortDirection; +}; + +const CHEVRON_CLASS = 'tableReadonlySortChevron'; + +const tableReadonlySortKey = new PluginKey('tableReadonlySort'); + +const sortStates = new WeakMap(); +const originalOrders = new WeakMap(); + +const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }); + +function getColumnIndex(th: HTMLTableCellElement): number { + const row = th.parentElement as HTMLTableRowElement; + if (!row) return -1; + let col = 0; + for (let i = 0; i < row.cells.length; i++) { + if (row.cells[i] === th) return col; + col += row.cells[i].colSpan ?? 1; + } + return -1; +} + +function getHeaderTh(target: EventTarget | null): HTMLTableCellElement | null { + if (!(target instanceof Element)) return null; + const th = target.closest('th') as HTMLTableCellElement | null; + if (!th) return null; + const row = th.parentElement; + if (!row) return null; + const tbody = row.parentElement; + if (!tbody) return null; + const table = tbody.closest('table'); + if (!table) return null; + + // th must be in the first row of the table (could be in thead or tbody) + const firstRow = table.querySelector('tr'); + if (firstRow !== row) return null; + + return th; +} + +function getCellText(row: HTMLTableRowElement, colIndex: number): string { + let col = 0; + for (let i = 0; i < row.cells.length; i++) { + if (col === colIndex) return row.cells[i].textContent?.trim() ?? ''; + col += row.cells[i].colSpan ?? 1; + } + return ''; +} + +function getOrSaveOriginalOrder( + table: HTMLTableElement, + dataRows: HTMLTableRowElement[], +): HTMLTableRowElement[] { + if (!originalOrders.has(table)) { + originalOrders.set(table, [...dataRows]); + } + return originalOrders.get(table)!; +} + +function sortDataRows( + dataRows: HTMLTableRowElement[], + colIndex: number, + direction: SortDirection, +): HTMLTableRowElement[] { + return [...dataRows].sort((a, b) => { + const textA = getCellText(a, colIndex); + const textB = getCellText(b, colIndex); + const emptyA = textA === ''; + const emptyB = textB === ''; + if (emptyA && emptyB) return 0; + if (emptyA) return 1; + if (emptyB) return -1; + const cmp = collator.compare(textA, textB); + return direction === 'asc' ? cmp : -cmp; + }); +} + +function applySort(table: HTMLTableElement, colIndex: number): void { + const tbody = table.querySelector('tbody'); + if (!tbody) return; + + const allRows = Array.from(tbody.querySelectorAll(':scope > tr')); + if (allRows.length === 0) return; + + const headerRow = allRows[0]; + const dataRows = allRows.slice(1); + if (dataRows.length === 0) return; + + const current = sortStates.get(table) ?? null; + const saved = getOrSaveOriginalOrder(table, dataRows); + + let next: SortState | null; + if (!current || current.col !== colIndex) { + next = { col: colIndex, direction: 'asc' }; + } else if (current.direction === 'asc') { + next = { col: colIndex, direction: 'desc' }; + } else { + next = null; + } + + if (next === null) { + sortStates.delete(table); + tbody.append(headerRow, ...saved); + } else { + sortStates.set(table, next); + const sorted = sortDataRows(saved, next.col, next.direction); + tbody.append(headerRow, ...sorted); + } + + updateChevrons(table); +} + +const CHEVRON_SVG = + ''; + +function ensureChevron(th: HTMLTableCellElement): HTMLSpanElement { + let chevron = th.querySelector(`.${CHEVRON_CLASS}`); + if (!chevron) { + chevron = document.createElement('span'); + chevron.className = CHEVRON_CLASS; + chevron.setAttribute('aria-hidden', 'true'); + chevron.innerHTML = CHEVRON_SVG; + th.appendChild(chevron); + } + return chevron; +} + +function updateChevrons(table: HTMLTableElement): void { + const firstRow = table.querySelector('tr'); + if (!firstRow) return; + + const state = sortStates.get(table) ?? null; + let col = 0; + for (let i = 0; i < firstRow.cells.length; i++) { + const cell = firstRow.cells[i]; + if (cell.tagName !== 'TH') { + col += cell.colSpan ?? 1; + continue; + } + const chevron = ensureChevron(cell as HTMLTableCellElement); + let label: string; + if (state && state.col === col) { + chevron.setAttribute('data-sort', state.direction); + label = state.direction === 'asc' ? 'Sort descending' : 'Clear sort'; + } else { + chevron.removeAttribute('data-sort'); + label = 'Sort ascending'; + } + chevron.setAttribute('data-tooltip', label); + chevron.setAttribute('aria-label', label); + chevron.title = label; + col += cell.colSpan ?? 1; + } +} + +function addChevronsToAllTables(editorRoot: HTMLElement): void { + const tables = editorRoot.querySelectorAll('table'); + tables.forEach((table) => updateChevrons(table)); +} + +function removeAllChevrons(editorRoot: HTMLElement): void { + editorRoot + .querySelectorAll(`.${CHEVRON_CLASS}`) + .forEach((el) => el.remove()); +} + +export const TableReadonlySort = Extension.create({ + name: 'tableReadonlySort', + + addProseMirrorPlugins() { + const editor = this.editor; + let editorRoot: HTMLElement | null = null; + + const onClick = (event: MouseEvent) => { + if (editor.isEditable) return; + // Only react to clicks on the chevron, not anywhere else in the header + // cell. This lets the user click into a header to select text without + // accidentally triggering a sort. + if (!(event.target instanceof Element)) return; + const chevron = event.target.closest(`.${CHEVRON_CLASS}`); + if (!chevron) return; + const th = getHeaderTh(chevron); + if (!th) return; + const table = th.closest('table') as HTMLTableElement | null; + if (!table) return; + const colIndex = getColumnIndex(th); + if (colIndex < 0) return; + applySort(table, colIndex); + }; + + return [ + new Plugin({ + key: tableReadonlySortKey, + + view(editorView) { + editorRoot = editorView.dom as HTMLElement; + editorRoot.addEventListener('click', onClick); + + if (!editor.isEditable) { + addChevronsToAllTables(editorRoot); + } + + return { + update(view) { + const root = view.dom as HTMLElement; + if (!editor.isEditable) { + addChevronsToAllTables(root); + } else { + removeAllChevrons(root); + } + }, + destroy() { + if (editorRoot) { + editorRoot.removeEventListener('click', onClick); + removeAllChevrons(editorRoot); + } + }, + }; + }, + }), + ]; + }, +}); diff --git a/packages/editor-ext/src/lib/table/table-view.ts b/packages/editor-ext/src/lib/table/table-view.ts new file mode 100644 index 000000000..7e410918e --- /dev/null +++ b/packages/editor-ext/src/lib/table/table-view.ts @@ -0,0 +1,158 @@ +import type { Node as ProseMirrorNode } from '@tiptap/pm/model'; +import type { NodeView, ViewMutationRecord } from '@tiptap/pm/view'; +import { getColStyleDeclaration } from './utils/col-style'; + +export function updateColumns( + node: ProseMirrorNode, + colgroup: HTMLElement, + table: HTMLTableElement, + cellMinWidth: number, + overrideCol?: number, + overrideValue?: number, +) { + let totalWidth = 0; + let fixedWidth = true; + let nextDOM = colgroup.firstChild; + const row = node.firstChild; + + if (row !== null) { + for (let i = 0, col = 0; i < row.childCount; i += 1) { + const { colspan, colwidth } = row.child(i).attrs; + + for (let j = 0; j < colspan; j += 1, col += 1) { + const hasWidth = + overrideCol === col + ? overrideValue + : ((colwidth && colwidth[j]) as number | undefined); + const cssWidth = hasWidth ? `${hasWidth}px` : ''; + + totalWidth += hasWidth || cellMinWidth; + + if (!hasWidth) { + fixedWidth = false; + } + + if (!nextDOM) { + const colElement = document.createElement('col'); + + const [propertyKey, propertyValue] = getColStyleDeclaration( + cellMinWidth, + hasWidth, + ); + + colElement.style.setProperty(propertyKey, propertyValue); + + colgroup.appendChild(colElement); + } else { + if ((nextDOM as HTMLTableColElement).style.width !== cssWidth) { + const [propertyKey, propertyValue] = getColStyleDeclaration( + cellMinWidth, + hasWidth, + ); + + (nextDOM as HTMLTableColElement).style.setProperty( + propertyKey, + propertyValue, + ); + } + + nextDOM = nextDOM.nextSibling; + } + } + } + } + + while (nextDOM) { + const after = nextDOM.nextSibling; + + nextDOM.parentNode?.removeChild(nextDOM); + nextDOM = after; + } + + const hasUserWidth = + node.attrs.style && + typeof node.attrs.style === 'string' && + /\bwidth\s*:/i.test(node.attrs.style); + + if (fixedWidth && !hasUserWidth) { + table.style.width = `${totalWidth}px`; + table.style.minWidth = ''; + } else { + table.style.width = ''; + table.style.minWidth = `${totalWidth}px`; + } +} + +export class TableView implements NodeView { + node: ProseMirrorNode; + + cellMinWidth: number; + + dom: HTMLDivElement; + + table: HTMLTableElement; + + colgroup: HTMLTableColElement; + + contentDOM: HTMLTableSectionElement; + + constructor(node: ProseMirrorNode, cellMinWidth: number) { + this.node = node; + this.cellMinWidth = cellMinWidth; + this.dom = document.createElement('div'); + this.dom.className = 'tableWrapper'; + this.table = this.dom.appendChild(document.createElement('table')); + + if (node.attrs.style) { + this.table.style.cssText = node.attrs.style; + } + + this.colgroup = this.table.appendChild(document.createElement('colgroup')); + updateColumns(node, this.colgroup, this.table, cellMinWidth); + this.contentDOM = this.table.appendChild(document.createElement('tbody')); + } + + update(node: ProseMirrorNode) { + if (node.type !== this.node.type) return false; + + this.node = node; + updateColumns(node, this.colgroup, this.table, this.cellMinWidth); + + return true; + } + + ignoreMutation(mutation: ViewMutationRecord) { + const target = mutation.target as Node; + const isInsideWrapper = this.dom.contains(target); + const isInsideContent = this.contentDOM.contains(target); + + if (isInsideWrapper && !isInsideContent) { + if ( + mutation.type === 'attributes' || + mutation.type === 'childList' || + mutation.type === 'characterData' + ) { + return true; + } + } + + // Chevron span (.tableReadonlySortChevron) added/removed by sort plugin. + if (mutation.type === 'childList') { + const nodes = [ + ...Array.from(mutation.addedNodes), + ...Array.from(mutation.removedNodes), + ]; + if ( + nodes.some( + (n) => + n instanceof Element && + n.classList.contains('tableReadonlySortChevron'), + ) + ) { + return true; + } + } + + return false; + } +} diff --git a/packages/editor-ext/src/lib/table/table.ts b/packages/editor-ext/src/lib/table/table.ts index f1436c28d..e87048a46 100644 --- a/packages/editor-ext/src/lib/table/table.ts +++ b/packages/editor-ext/src/lib/table/table.ts @@ -1,6 +1,8 @@ import { Table } from "@tiptap/extension-table"; import { Editor } from "@tiptap/core"; import { DOMOutputSpec } from "@tiptap/pm/model"; +import { TextSelection } from "@tiptap/pm/state"; +import { cellAround } from "@tiptap/pm/tables"; const LIST_TYPES = ["bulletList", "orderedList", "taskList"]; @@ -32,9 +34,36 @@ function handleListOutdent(editor: Editor): boolean { } export const CustomTable = Table.extend({ + addKeyboardShortcuts() { return { ...this.parent?.(), + "Mod-a": () => { + const { state, view } = this.editor; + const { selection, doc } = state; + + const $cellPos = cellAround(selection.$anchor); + if (!$cellPos) return false; + + const cellNode = doc.nodeAt($cellPos.pos); + // Empty cells have nothing useful to scope to — let the default + // Mod-a fall through and select the whole doc. + if (!cellNode || !cellNode.textContent) return false; + + const from = $cellPos.pos + 1; + const to = $cellPos.pos + cellNode.nodeSize - 1; + if (from >= to) return true; + + const nextSel = TextSelection.between( + doc.resolve(from), + doc.resolve(to), + 1, + ); + if (!nextSel || selection.eq(nextSel)) return true; + + view.dispatch(state.tr.setSelection(nextSel)); + return true; + }, Tab: () => { // If we're in a list within a table, handle list indentation if (isInList(this.editor) && this.editor.isActive("table")) { diff --git a/packages/editor-ext/src/lib/table/utils/col-style.ts b/packages/editor-ext/src/lib/table/utils/col-style.ts new file mode 100644 index 000000000..8060962fd --- /dev/null +++ b/packages/editor-ext/src/lib/table/utils/col-style.ts @@ -0,0 +1,7 @@ +export function getColStyleDeclaration(minWidth: number, width: number | undefined): [string, string] { + if (width) { + return ['width', `${Math.max(width, minWidth)}px`] + } + + return ['min-width', `${minWidth}px`] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a6dd37f6..f4a628064 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,22 +248,22 @@ importers: apps/client: dependencies: '@atlaskit/pragmatic-drag-and-drop': - specifier: ^1.8.1 + specifier: 1.8.1 version: 1.8.1 '@atlaskit/pragmatic-drag-and-drop-auto-scroll': - specifier: ^2.1.0 + specifier: 2.1.5 version: 2.1.5 '@atlaskit/pragmatic-drag-and-drop-flourish': - specifier: ^2.0.15 + specifier: 2.0.15 version: 2.0.15(react@18.3.1) '@atlaskit/pragmatic-drag-and-drop-hitbox': - specifier: ^1.1.0 + specifier: 1.1.0 version: 1.1.0 '@atlaskit/pragmatic-drag-and-drop-live-region': - specifier: ^1.3.4 + specifier: 1.3.4 version: 1.3.4 '@casl/react': - specifier: ^5.0.1 + specifier: 5.0.1 version: 5.0.1(@casl/ability@6.8.0)(react@18.3.1) '@docmost/editor-ext': specifier: workspace:* @@ -272,37 +272,37 @@ importers: specifier: 0.18.0-3a5ef40 version: 0.18.0-3a5ef40(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/core': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/dates': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(dayjs@1.11.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/form': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(react@18.3.1) '@mantine/hooks': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(react@18.3.1) '@mantine/modals': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/notifications': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/spotlight': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@slidoapp/emoji-mart': - specifier: ^5.8.7 + specifier: 5.8.7 version: 5.8.7 '@slidoapp/emoji-mart-data': - specifier: ^1.2.4 + specifier: 1.2.4 version: 1.2.4 '@slidoapp/emoji-mart-react': - specifier: ^1.1.5 + specifier: 1.1.5 version: 1.1.5(@slidoapp/emoji-mart@5.8.7)(react@18.3.1) '@tabler/icons-react': - specifier: ^3.40.0 + specifier: 3.40.0 version: 3.40.0(react@18.3.1) '@tanstack/react-query': specifier: 5.90.17 @@ -311,22 +311,22 @@ importers: specifier: 3.13.24 version: 3.13.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) alfaaz: - specifier: ^1.1.0 + specifier: 1.1.0 version: 1.1.0 axios: specifier: 1.16.0 version: 1.16.0 blueimp-load-image: - specifier: ^5.16.0 + specifier: 5.16.0 version: 5.16.0 clsx: - specifier: ^2.1.1 + specifier: 2.1.1 version: 2.1.1 file-saver: - specifier: ^2.0.5 + specifier: 2.0.5 version: 2.0.5 highlightjs-sap-abap: - specifier: ^0.3.0 + specifier: 0.3.0 version: 0.3.0 i18next: specifier: 25.10.1 @@ -335,37 +335,37 @@ importers: specifier: 3.0.6 version: 3.0.6 jotai: - specifier: ^2.18.1 + specifier: 2.18.1 version: 2.18.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.12)(react@18.3.1) jotai-optics: - specifier: ^0.4.0 + specifier: 0.4.0 version: 0.4.0(jotai@2.18.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.12)(react@18.3.1))(optics-ts@2.4.1) js-cookie: - specifier: ^3.0.5 + specifier: 3.0.5 version: 3.0.5 jwt-decode: - specifier: ^4.0.0 + specifier: 4.0.0 version: 4.0.0 katex: specifier: 0.16.40 version: 0.16.40 lowlight: - specifier: ^3.3.0 + specifier: 3.3.0 version: 3.3.0 mantine-form-zod-resolver: - specifier: ^1.3.0 + specifier: 1.3.0 version: 1.3.0(@mantine/form@8.3.18(react@18.3.1))(zod@4.3.6) mermaid: specifier: 11.13.0 version: 11.13.0 mitt: - specifier: ^3.0.1 + specifier: 3.0.1 version: 3.0.1 posthog-js: specifier: 1.372.2 version: 1.372.2 react: - specifier: ^18.3.1 + specifier: 18.3.1 version: 18.3.1 react-clear-modal: specifier: ^2.0.18 @@ -374,111 +374,111 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-drawio: - specifier: ^1.0.7 + specifier: 1.0.7 version: 1.0.7(react@18.3.1) react-error-boundary: - specifier: ^6.1.1 + specifier: 6.1.1 version: 6.1.1(react@18.3.1) react-helmet-async: - specifier: ^3.0.0 + specifier: 3.0.0 version: 3.0.0(react@18.3.1) react-i18next: specifier: 16.5.8 version: 16.5.8(i18next@25.10.1(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) react-router-dom: - specifier: ^7.13.1 + specifier: 7.13.1 version: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: - specifier: ^7.7.4 + specifier: 7.7.4 version: 7.7.4 socket.io-client: - specifier: ^4.8.3 + specifier: 4.8.3 version: 4.8.3 zod: - specifier: ^4.3.6 + specifier: 4.3.6 version: 4.3.6 devDependencies: '@eslint/js': - specifier: ^9.28.0 - version: 9.39.4 + specifier: 9.28.0 + version: 9.28.0 '@tanstack/eslint-plugin-query': - specifier: ^5.94.4 - version: 5.94.4(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) + specifier: 5.94.4 + version: 5.94.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) '@testing-library/jest-dom': - specifier: ^6.6.0 - version: 6.9.1 + specifier: 6.6.0 + version: 6.6.0 '@testing-library/react': - specifier: ^16.1.0 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 16.1.0 + version: 16.1.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/blueimp-load-image': - specifier: ^5.16.6 + specifier: 5.16.6 version: 5.16.6 '@types/file-saver': - specifier: ^2.0.7 + specifier: 2.0.7 version: 2.0.7 '@types/js-cookie': - specifier: ^3.0.6 + specifier: 3.0.6 version: 3.0.6 '@types/katex': - specifier: ^0.16.8 + specifier: 0.16.8 version: 0.16.8 '@types/node': specifier: 22.19.1 version: 22.19.1 '@types/react': - specifier: ^18.3.12 + specifier: 18.3.12 version: 18.3.12 '@types/react-dom': - specifier: ^18.3.1 + specifier: 18.3.1 version: 18.3.1 '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 6.0.1 + version: 6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) eslint: - specifier: ^9.28.0 - version: 9.39.4(jiti@2.4.2) + specifier: 9.28.0 + version: 9.28.0(jiti@2.4.2) eslint-plugin-react: - specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.4(jiti@2.4.2)) + specifier: 7.37.5 + version: 7.37.5(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-react-hooks: - specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.4(jiti@2.4.2)) + specifier: 7.0.1 + version: 7.0.1(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-react-refresh: - specifier: ^0.5.2 - version: 0.5.2(eslint@9.39.4(jiti@2.4.2)) + specifier: 0.5.2 + version: 0.5.2(eslint@9.28.0(jiti@2.4.2)) globals: - specifier: ^15.13.0 + specifier: 15.13.0 version: 15.13.0 jsdom: - specifier: ^25.0.0 - version: 25.0.1 + specifier: 25.0.0 + version: 25.0.0 optics-ts: - specifier: ^2.4.1 + specifier: 2.4.1 version: 2.4.1 postcss: - specifier: ^8.5.12 - version: 8.5.12 + specifier: 8.5.14 + version: 8.5.14 postcss-preset-mantine: - specifier: ^1.18.0 - version: 1.18.0(postcss@8.5.12) + specifier: 1.18.0 + version: 1.18.0(postcss@8.5.14) postcss-simple-vars: - specifier: ^7.0.1 - version: 7.0.1(postcss@8.5.12) + specifier: 7.0.1 + version: 7.0.1(postcss@8.5.14) prettier: - specifier: ^3.8.1 + specifier: 3.8.1 version: 3.8.1 typescript: - specifier: ^5.9.3 + specifier: 5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.57.1 - version: 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) + specifier: 8.57.1 + version: 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) vite: specifier: 8.0.5 - version: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + version: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) vitest: - specifier: ^4.1.6 - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.6 + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) apps/server: dependencies: @@ -2237,14 +2237,30 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/config-array@0.20.1': + resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.21.2': resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.2.3': + resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.4.2': resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.14.0': + resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.17.0': resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2253,6 +2269,10 @@ packages: resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.28.0': + resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.39.4': resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2261,6 +2281,10 @@ packages: resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.4.1': resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4463,12 +4487,12 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + '@testing-library/jest-dom@6.6.0': + resolution: {integrity: sha512-Y76dmd7C85xekWqylJqRmO6lr83cdVprTs0muSvkXr6M73auYK5OvZMc3tKe1F7wMFdzfeBCwVbkoGrRKWb+fg==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + '@testing-library/react@16.1.0': + resolution: {integrity: sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -5540,10 +5564,6 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - array-buffer-byte-length@1.0.1: - resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} - engines: {node: '>= 0.4'} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -5571,10 +5591,6 @@ packages: resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} engines: {node: '>= 0.4'} - arraybuffer.prototype.slice@1.0.3: - resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} - engines: {node: '>= 0.4'} - arraybuffer.prototype.slice@1.0.4: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} @@ -5787,10 +5803,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.7: - resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} - engines: {node: '>= 0.4'} - call-bind@1.0.8: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} @@ -5825,6 +5837,10 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -6135,9 +6151,6 @@ packages: resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} engines: {node: '>=18'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -6301,26 +6314,14 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} - data-view-buffer@1.0.1: - resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} - engines: {node: '>= 0.4'} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} - data-view-byte-length@1.0.1: - resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} - engines: {node: '>= 0.4'} - data-view-byte-length@1.0.2: resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} engines: {node: '>= 0.4'} - data-view-byte-offset@1.0.0: - resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} - engines: {node: '>= 0.4'} - data-view-byte-offset@1.0.1: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} @@ -6606,10 +6607,6 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - es-abstract@1.23.5: - resolution: {integrity: sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==} - engines: {node: '>= 0.4'} - es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -6626,9 +6623,6 @@ packages: resolution: {integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==} engines: {node: '>= 0.4'} - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} @@ -6727,6 +6721,16 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.28.0: + resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6954,9 +6958,6 @@ packages: debug: optional: true - for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -7016,10 +7017,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.1.6: - resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} - engines: {node: '>= 0.4'} - function.prototype.name@1.1.8: resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} engines: {node: '>= 0.4'} @@ -7059,10 +7056,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-symbol-description@1.0.2: - resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} - engines: {node: '>= 0.4'} - get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -7137,10 +7130,6 @@ packages: has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - has-proto@1.0.3: - resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} - engines: {node: '>= 0.4'} - has-proto@1.2.0: resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} engines: {node: '>= 0.4'} @@ -7292,10 +7281,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - internal-slot@1.0.7: - resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} - engines: {node: '>= 0.4'} - internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -7326,10 +7311,6 @@ packages: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} - is-array-buffer@3.0.4: - resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} - engines: {node: '>= 0.4'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -7349,10 +7330,6 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-boolean-object@1.2.0: - resolution: {integrity: sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw==} - engines: {node: '>= 0.4'} - is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -7364,18 +7341,10 @@ packages: is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - is-data-view@1.0.1: - resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} - engines: {node: '>= 0.4'} - is-data-view@1.0.2: resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} engines: {node: '>= 0.4'} - is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - is-date-object@1.1.0: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} @@ -7421,10 +7390,6 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} - is-number-object@1.1.0: - resolution: {integrity: sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw==} - engines: {node: '>= 0.4'} - is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -7439,10 +7404,6 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.0: - resolution: {integrity: sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA==} - engines: {node: '>= 0.4'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -7451,10 +7412,6 @@ packages: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} - is-shared-array-buffer@1.0.3: - resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} - engines: {node: '>= 0.4'} - is-shared-array-buffer@1.0.4: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} @@ -7463,26 +7420,14 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-string@1.1.0: - resolution: {integrity: sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g==} - engines: {node: '>= 0.4'} - is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} - is-symbol@1.1.0: - resolution: {integrity: sha512-qS8KkNNXUZ/I+nX6QT8ZS1/Yx0A444yhzdTKxCzKkNjQ9sHErBxJnJAgh+f5YhusYECEcjo4XcyH87hn6+ks0A==} - engines: {node: '>= 0.4'} - is-symbol@1.1.1: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} - is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} - is-typed-array@1.1.15: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} @@ -7499,9 +7444,6 @@ packages: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} - is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - is-weakref@1.1.1: resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} engines: {node: '>= 0.4'} @@ -7785,8 +7727,8 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@25.0.1: - resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + jsdom@25.0.0: + resolution: {integrity: sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==} engines: {node: '>=18'} peerDependencies: canvas: ^2.11.2 @@ -8555,10 +8497,6 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - object.assign@4.1.5: - resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} - engines: {node: '>= 0.4'} - object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -8948,8 +8886,8 @@ packages: peerDependencies: postcss: ^8.2.1 - postcss@8.5.12: - resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -9095,6 +9033,9 @@ packages: prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -9124,6 +9065,9 @@ packages: query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -9324,10 +9268,6 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - reflect.getprototypeof@1.0.7: - resolution: {integrity: sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g==} - engines: {node: '>= 0.4'} - regenerate-unicode-properties@10.1.1: resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} engines: {node: '>=4'} @@ -9338,10 +9278,6 @@ packages: regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} - regexp.prototype.flags@1.5.3: - resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} - engines: {node: '>= 0.4'} - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -9365,6 +9301,9 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -9446,10 +9385,6 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-array-concat@1.1.2: - resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} - engines: {node: '>=0.4'} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -9464,10 +9399,6 @@ packages: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} - safe-regex-test@1.0.3: - resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} - engines: {node: '>= 0.4'} - safe-regex-test@1.1.0: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} @@ -9710,13 +9641,6 @@ packages: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} - string.prototype.trim@1.2.9: - resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.8: - resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} - string.prototype.trimend@1.0.9: resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} engines: {node: '>= 0.4'} @@ -9920,6 +9844,10 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -10046,26 +9974,14 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typed-array-buffer@1.0.2: - resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} - engines: {node: '>= 0.4'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} - typed-array-byte-length@1.0.1: - resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} - engines: {node: '>= 0.4'} - typed-array-byte-length@1.0.3: resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.3: - resolution: {integrity: sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==} - engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.4: resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} engines: {node: '>= 0.4'} @@ -10118,9 +10034,6 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -10154,6 +10067,10 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -10174,6 +10091,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -10445,18 +10365,10 @@ packages: when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} - which-boxed-primitive@1.1.0: - resolution: {integrity: sha512-Ei7Miu/AXe2JJ4iNF5j/UphAgRoma4trE6PtisM09bPygb3egMH3YLW/befsWb1A1AxvNSFidOFTB18XtnIIng==} - engines: {node: '>= 0.4'} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} - which-builtin-type@1.2.0: - resolution: {integrity: sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==} - engines: {node: '>= 0.4'} - which-builtin-type@1.2.1: resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} engines: {node: '>= 0.4'} @@ -10468,10 +10380,6 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which-typed-array@1.1.16: - resolution: {integrity: sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==} - engines: {node: '>= 0.4'} - which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -12430,6 +12338,11 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.28.0(jiti@2.4.2))': + dependencies: + eslint: 9.28.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.4.2))': dependencies: eslint: 9.39.4(jiti@2.4.2) @@ -12437,6 +12350,14 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} + '@eslint/config-array@0.20.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 @@ -12445,10 +12366,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/config-helpers@0.2.3': {} + '@eslint/config-helpers@0.4.2': dependencies: '@eslint/core': 0.17.0 + '@eslint/core@0.14.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 @@ -12467,10 +12398,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/js@9.28.0': {} + '@eslint/js@9.39.4': {} '@eslint/object-schema@2.1.7': {} + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + '@eslint/plugin-kit@0.4.1': dependencies: '@eslint/core': 0.17.0 @@ -13065,7 +13003,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': @@ -14985,10 +14923,10 @@ snapshots: '@tabler/icons@3.40.0': {} - '@tanstack/eslint-plugin-query@5.94.4(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.94.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) - eslint: 9.39.4(jiti@2.4.2) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + eslint: 9.28.0(jiti@2.4.2) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -15020,16 +14958,17 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.9.1': + '@testing-library/jest-dom@6.6.0': dependencies: '@adobe/css-tools': 4.4.3 aria-query: 5.3.2 + chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 - picocolors: 1.1.1 + lodash: 4.18.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.1.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.29.2 '@testing-library/dom': 10.4.1 @@ -15517,11 +15456,11 @@ snapshots: '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 8.56.10 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/eslint@8.56.10': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/estree@1.0.8': {} @@ -15686,7 +15625,7 @@ snapshots: '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.11 - csstype: 3.1.3 + csstype: 3.2.3 '@types/send@0.17.4': dependencies: @@ -15747,6 +15686,22 @@ snapshots: dependencies: '@types/node': 25.5.0 + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/type-utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 + eslint: 9.28.0(jiti@2.4.2) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3))(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -15763,6 +15718,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3 + eslint: 9.28.0(jiti@2.4.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.57.1 @@ -15793,6 +15760,18 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/type-utils@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.28.0(jiti@2.4.2) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.57.1 @@ -15822,6 +15801,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.28.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + eslint: 9.28.0(jiti@2.4.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.4.2)) @@ -15922,10 +15912,10 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/expect@4.1.6': dependencies: @@ -15936,13 +15926,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@4.1.6': dependencies: @@ -16187,11 +16177,6 @@ snapshots: aria-query@5.3.2: {} - array-buffer-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - is-array-buffer: 3.0.4 - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -16199,57 +16184,46 @@ snapshots: array-includes@3.1.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 - is-string: 1.1.0 + is-string: 1.1.1 array-timsort@1.0.3: {} array.prototype.findlast@1.2.5: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.0.2 array.prototype.flat@1.3.2: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-shim-unscopables: 1.0.2 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-shim-unscopables: 1.0.2 array.prototype.tosorted@1.1.4: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-errors: 1.3.0 es-shim-unscopables: 1.0.2 - arraybuffer.prototype.slice@1.0.3: - dependencies: - array-buffer-byte-length: 1.0.1 - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.4 - is-shared-array-buffer: 1.0.3 - arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 @@ -16531,14 +16505,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.7: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - call-bind@1.0.8: dependencies: call-bind-apply-helpers: 1.0.2 @@ -16565,6 +16531,11 @@ snapshots: chai@6.2.2: {} + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -16890,8 +16861,6 @@ snapshots: '@asamuzakjp/css-color': 2.8.3 rrweb-cssom: 0.8.0 - csstype@3.1.3: {} - csstype@3.2.3: {} cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): @@ -17083,36 +17052,18 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - data-view-buffer@1.0.1: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 - data-view-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - data-view-byte-length@1.0.2: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 - data-view-byte-offset@1.0.0: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - data-view-byte-offset@1.0.1: dependencies: call-bound: 1.0.4 @@ -17230,7 +17181,7 @@ snapshots: dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.29.2 - csstype: 3.1.3 + csstype: 3.2.3 dom-serializer@2.0.0: dependencies: @@ -17368,55 +17319,6 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-abstract@1.23.5: - dependencies: - array-buffer-byte-length: 1.0.1 - arraybuffer.prototype.slice: 1.0.3 - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - data-view-buffer: 1.0.1 - data-view-byte-length: 1.0.1 - data-view-byte-offset: 1.0.0 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.6 - get-intrinsic: 1.3.0 - get-symbol-description: 1.0.2 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.0.3 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.0.7 - is-array-buffer: 3.0.4 - is-callable: 1.2.7 - is-data-view: 1.0.1 - is-negative-zero: 2.0.3 - is-regex: 1.2.0 - is-shared-array-buffer: 1.0.3 - is-string: 1.1.0 - is-typed-array: 1.1.13 - is-weakref: 1.0.2 - object-inspect: 1.13.3 - object-keys: 1.1.1 - object.assign: 4.1.5 - regexp.prototype.flags: 1.5.3 - safe-array-concat: 1.1.2 - safe-regex-test: 1.0.3 - string.prototype.trim: 1.2.9 - string.prototype.trimend: 1.0.8 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.2 - typed-array-byte-length: 1.0.1 - typed-array-byte-offset: 1.0.3 - typed-array-length: 1.0.7 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.16 - es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 @@ -17498,8 +17400,6 @@ snapshots: math-intrinsics: 1.1.0 safe-array-concat: 1.1.3 - es-module-lexer@2.0.0: {} - es-module-lexer@2.1.0: {} es-object-atoms@1.1.1: @@ -17520,8 +17420,8 @@ snapshots: es-to-primitive@1.3.0: dependencies: is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.1.0 + is-date-object: 1.1.0 + is-symbol: 1.1.1 es6-promise-pool@2.5.0: {} @@ -17599,22 +17499,22 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.4.2) - eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.4.2)): + eslint-plugin-react-hooks@7.0.1(eslint@9.28.0(jiti@2.4.2)): dependencies: '@babel/core': 7.28.5 '@babel/parser': 7.28.5 - eslint: 9.39.4(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@9.39.4(jiti@2.4.2)): + eslint-plugin-react-refresh@0.5.2(eslint@9.28.0(jiti@2.4.2)): dependencies: - eslint: 9.39.4(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) - eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.4.2)): + eslint-plugin-react@7.37.5(eslint@9.28.0(jiti@2.4.2)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -17622,7 +17522,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.3.1 - eslint: 9.39.4(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -17652,6 +17552,48 @@ snapshots: eslint-visitor-keys@5.0.1: {} + eslint@9.28.0(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.20.1 + '@eslint/config-helpers': 0.2.3 + '@eslint/core': 0.14.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.28.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.3 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + eslint@9.39.4(jiti@2.4.2): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.4.2)) @@ -17955,10 +17897,6 @@ snapshots: follow-redirects@1.16.0: {} - for-each@0.3.3: - dependencies: - is-callable: 1.2.7 - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -18027,13 +17965,6 @@ snapshots: function-bind@1.1.2: {} - function.prototype.name@1.1.6: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - functions-have-names: 1.2.3 - function.prototype.name@1.1.8: dependencies: call-bind: 1.0.8 @@ -18075,12 +18006,6 @@ snapshots: get-stream@6.0.1: {} - get-symbol-description@1.0.2: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -18157,8 +18082,6 @@ snapshots: dependencies: es-define-property: 1.0.1 - has-proto@1.0.3: {} - has-proto@1.2.0: dependencies: dunder-proto: 1.0.1 @@ -18306,12 +18229,6 @@ snapshots: inherits@2.0.4: {} - internal-slot@1.0.7: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -18346,11 +18263,6 @@ snapshots: ipaddr.js@2.2.0: {} - is-array-buffer@3.0.4: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.3.0 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -18371,11 +18283,6 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-boolean-object@1.2.0: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -18387,20 +18294,12 @@ snapshots: dependencies: hasown: 2.0.2 - is-data-view@1.0.1: - dependencies: - is-typed-array: 1.1.13 - is-data-view@1.0.2: dependencies: call-bound: 1.0.4 get-intrinsic: 1.3.0 is-typed-array: 1.1.15 - is-date-object@1.0.5: - dependencies: - has-tostringtag: 1.0.2 - is-date-object@1.1.0: dependencies: call-bound: 1.0.4 @@ -18432,11 +18331,6 @@ snapshots: is-negative-zero@2.0.3: {} - is-number-object@1.1.0: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -18448,13 +18342,6 @@ snapshots: is-promise@4.0.0: {} - is-regex@1.2.0: - dependencies: - call-bind: 1.0.7 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -18464,42 +18351,23 @@ snapshots: is-set@2.0.3: {} - is-shared-array-buffer@1.0.3: - dependencies: - call-bind: 1.0.7 - is-shared-array-buffer@1.0.4: dependencies: call-bound: 1.0.4 is-stream@2.0.1: {} - is-string@1.1.0: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-string@1.1.1: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-symbol@1.1.0: - dependencies: - call-bind: 1.0.7 - has-symbols: 1.1.0 - safe-regex-test: 1.0.3 - is-symbol@1.1.1: dependencies: call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 - is-typed-array@1.1.13: - dependencies: - which-typed-array: 1.1.16 - is-typed-array@1.1.15: dependencies: which-typed-array: 1.1.20 @@ -18510,10 +18378,6 @@ snapshots: is-weakmap@2.0.2: {} - is-weakref@1.0.2: - dependencies: - call-bind: 1.0.7 - is-weakref@1.1.1: dependencies: call-bound: 1.0.4 @@ -18990,7 +18854,7 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@25.0.1: + jsdom@25.0.0: dependencies: cssstyle: 4.2.1 data-urls: 5.0.0 @@ -19005,7 +18869,7 @@ snapshots: rrweb-cssom: 0.7.1 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 5.1.2 + tough-cookie: 4.1.4 w3c-xmlserializer: 5.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 3.1.1 @@ -19098,7 +18962,7 @@ snapshots: dependencies: array-includes: 3.1.8 array.prototype.flat: 1.3.2 - object.assign: 4.1.5 + object.assign: 4.1.7 object.values: 1.2.1 jszip@3.10.1: @@ -19729,13 +19593,6 @@ snapshots: object-keys@1.1.1: {} - object.assign@4.1.5: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - object.assign@4.1.7: dependencies: call-bind: 1.0.8 @@ -19754,9 +19611,9 @@ snapshots: object.fromentries@2.0.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 object.values@1.2.1: @@ -20134,40 +19991,40 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-js@4.0.1(postcss@8.5.12): + postcss-js@4.0.1(postcss@8.5.14): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.12 + postcss: 8.5.14 - postcss-mixins@12.1.2(postcss@8.5.12): + postcss-mixins@12.1.2(postcss@8.5.14): dependencies: - postcss: 8.5.12 - postcss-js: 4.0.1(postcss@8.5.12) - postcss-simple-vars: 7.0.1(postcss@8.5.12) - sugarss: 5.0.1(postcss@8.5.12) + postcss: 8.5.14 + postcss-js: 4.0.1(postcss@8.5.14) + postcss-simple-vars: 7.0.1(postcss@8.5.14) + sugarss: 5.0.1(postcss@8.5.14) tinyglobby: 0.2.15 - postcss-nested@7.0.2(postcss@8.5.12): + postcss-nested@7.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.12 + postcss: 8.5.14 postcss-selector-parser: 7.1.1 - postcss-preset-mantine@1.18.0(postcss@8.5.12): + postcss-preset-mantine@1.18.0(postcss@8.5.14): dependencies: - postcss: 8.5.12 - postcss-mixins: 12.1.2(postcss@8.5.12) - postcss-nested: 7.0.2(postcss@8.5.12) + postcss: 8.5.14 + postcss-mixins: 12.1.2(postcss@8.5.14) + postcss-nested: 7.0.2(postcss@8.5.14) postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-simple-vars@7.0.1(postcss@8.5.12): + postcss-simple-vars@7.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.12 + postcss: 8.5.14 - postcss@8.5.12: + postcss@8.5.14: dependencies: nanoid: 3.3.8 picocolors: 1.1.1 @@ -20382,6 +20239,10 @@ snapshots: prr@1.0.1: optional: true + psl@1.15.0: + dependencies: + punycode: 2.3.1 + pump@3.0.3: dependencies: end-of-stream: 1.4.4 @@ -20407,6 +20268,8 @@ snapshots: query-selector-shadow-dom@1.0.1: {} + querystringify@2.2.0: {} + quick-format-unescaped@4.0.4: {} radix-ui@1.4.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -20677,16 +20540,6 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 - reflect.getprototypeof@1.0.7: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - which-builtin-type: 1.2.0 - regenerate-unicode-properties@10.1.1: dependencies: regenerate: 1.4.2 @@ -20697,13 +20550,6 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - regexp.prototype.flags@1.5.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - set-function-name: 2.0.2 - regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -20732,6 +20578,8 @@ snapshots: require-main-filename@2.0.0: {} + requires-port@1.0.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -20832,13 +20680,6 @@ snapshots: dependencies: tslib: 2.8.1 - safe-array-concat@1.1.2: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -20856,12 +20697,6 @@ snapshots: es-errors: 1.3.0 isarray: 2.0.5 - safe-regex-test@1.0.3: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-regex: 1.2.0 - safe-regex-test@1.1.0: dependencies: call-bound: 1.0.4 @@ -21149,14 +20984,14 @@ snapshots: gopd: 1.2.0 has-symbols: 1.1.0 internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.3 + regexp.prototype.flags: 1.5.4 set-function-name: 2.0.2 side-channel: 1.1.0 string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 string.prototype.trim@1.2.10: dependencies: @@ -21168,19 +21003,6 @@ snapshots: es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 - string.prototype.trim@1.2.9: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - es-object-atoms: 1.1.1 - - string.prototype.trimend@1.0.8: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - string.prototype.trimend@1.0.9: dependencies: call-bind: 1.0.8 @@ -21190,7 +21012,7 @@ snapshots: string.prototype.trimstart@1.0.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -21239,9 +21061,9 @@ snapshots: stylis@4.3.6: {} - sugarss@5.0.1(postcss@8.5.12): + sugarss@5.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.12 + postcss: 8.5.14 superagent@10.3.0: dependencies: @@ -21371,6 +21193,13 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tough-cookie@5.1.2: dependencies: tldts: 6.1.72 @@ -21497,49 +21326,25 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typed-array-buffer@1.0.2: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-typed-array: 1.1.13 - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 - typed-array-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.2.0 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - typed-array-byte-length@1.0.3: dependencies: call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 - typed-array-byte-offset@1.0.3: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.2.0 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - reflect.getprototypeof: 1.0.7 - typed-array-byte-offset@1.0.4: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 @@ -21547,12 +21352,23 @@ snapshots: typed-array-length@1.0.7: dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 + call-bind: 1.0.8 + for-each: 0.3.5 gopd: 1.2.0 - is-typed-array: 1.1.13 + is-typed-array: 1.1.15 possible-typed-array-names: 1.0.0 - reflect.getprototypeof: 1.0.7 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + eslint: 9.28.0(jiti@2.4.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color typescript-eslint@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3): dependencies: @@ -21593,13 +21409,6 @@ snapshots: uint8array-extras@1.5.0: {} - unbox-primitive@1.0.2: - dependencies: - call-bind: 1.0.7 - has-bigints: 1.0.2 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.0 - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -21626,6 +21435,8 @@ snapshots: unicode-property-aliases-ecmascript@2.1.0: {} + universalify@0.2.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -21664,6 +21475,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + use-callback-ref@1.3.3(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 @@ -21724,11 +21540,11 @@ snapshots: vary@1.1.2: {} - vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.12 + postcss: 8.5.14 rolldown: 1.0.0-rc.12 tinyglobby: 0.2.15 optionalDependencies: @@ -21737,15 +21553,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 less: 4.2.0 - sugarss: 5.0.1(postcss@8.5.12) + sugarss: 5.0.1(postcss@8.5.14) terser: 5.39.0 tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.1)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.6 '@vitest/runner': 4.1.6 '@vitest/snapshot': 4.1.6 @@ -21762,13 +21578,13 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.12))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.1 happy-dom: 20.8.9 - jsdom: 25.0.1 + jsdom: 25.0.0 transitivePeerDependencies: - msw @@ -21825,7 +21641,7 @@ snapshots: webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5)): dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 @@ -21835,7 +21651,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.3 enhanced-resolve: 5.20.1 - es-module-lexer: 2.0.0 + es-module-lexer: 2.1.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -21878,14 +21694,6 @@ snapshots: when-exit@2.1.5: {} - which-boxed-primitive@1.1.0: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.0 - is-number-object: 1.1.0 - is-string: 1.1.0 - is-symbol: 1.1.0 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -21894,22 +21702,6 @@ snapshots: is-string: 1.1.1 is-symbol: 1.1.1 - which-builtin-type@1.2.0: - dependencies: - call-bind: 1.0.7 - function.prototype.name: 1.1.6 - has-tostringtag: 1.0.2 - is-async-function: 2.0.0 - is-date-object: 1.0.5 - is-finalizationregistry: 1.1.0 - is-generator-function: 1.0.10 - is-regex: 1.2.0 - is-weakref: 1.0.2 - isarray: 2.0.5 - which-boxed-primitive: 1.1.0 - which-collection: 1.0.2 - which-typed-array: 1.1.16 - which-builtin-type@1.2.1: dependencies: call-bound: 1.0.4 @@ -21922,7 +21714,7 @@ snapshots: is-regex: 1.2.1 is-weakref: 1.1.1 isarray: 2.0.5 - which-boxed-primitive: 1.1.0 + which-boxed-primitive: 1.1.1 which-collection: 1.0.2 which-typed-array: 1.1.20 @@ -21935,14 +21727,6 @@ snapshots: which-module@2.0.1: {} - which-typed-array@1.1.16: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 From 299a9ca3c886f74ddddbed38578747e3ba463f81 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Thu, 14 May 2026 02:54:00 +0100 Subject: [PATCH 03/12] fix: bug fixes (#2201) * fix(editor): hide transclusion borders and reset spacing in read-only mode * feat(share): add full width toggle for shared pages * feat(share): support resizing sidebar on shared pages * fix: auto redirect if there is only one SSO provider. - fix tighten sso redirect - fix share tree margin * sync * package overrides --- apps/client/src/ee/components/sso-login.tsx | 69 +- apps/client/src/ee/security/sso.utils.ts | 13 +- .../src/features/auth/hooks/use-auth.ts | 2 +- .../transclusion-reference-view.tsx | 1 + .../transclusion/transclusion-view.tsx | 1 + .../transclusion/transclusion.module.css | 31 +- .../features/share/atoms/shared-page-atom.ts | 7 +- .../features/share/components/share-shell.tsx | 73 +- .../share/components/share.module.css | 24 + apps/client/src/lib/app-route.ts | 42 +- apps/client/src/pages/share/shared-page.tsx | 8 +- apps/server/package.json | 4 +- apps/server/src/ee | 2 +- package.json | 9 +- pnpm-lock.yaml | 1232 ++++++----------- 15 files changed, 696 insertions(+), 822 deletions(-) diff --git a/apps/client/src/ee/components/sso-login.tsx b/apps/client/src/ee/components/sso-login.tsx index ff739dd35..3e84f8efc 100644 --- a/apps/client/src/ee/components/sso-login.tsx +++ b/apps/client/src/ee/components/sso-login.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useWorkspacePublicDataQuery } from "@/features/workspace/queries/workspace-query.ts"; import { Button, Divider, Stack } from "@mantine/core"; import { IconLock, IconServer } from "@tabler/icons-react"; @@ -7,15 +7,37 @@ import { buildSsoLoginUrl } from "@/ee/security/sso.utils.ts"; import { SSO_PROVIDER } from "@/ee/security/contants.ts"; import { GoogleIcon } from "@/components/icons/google-icon.tsx"; import { LdapLoginModal } from "@/ee/components/ldap-login-modal.tsx"; +import { getRedirectParam } from "@/lib/app-route.ts"; +import useCurrentUser from "@/features/user/hooks/use-current-user.ts"; + +const SSO_AUTO_ATTEMPT_KEY = "docmost:ssoAutoAttempt"; +const SSO_AUTO_ATTEMPT_TTL_MS = 5 * 60_000; + +function recentAutoAttempt(): boolean { + try { + const raw = window.sessionStorage.getItem(SSO_AUTO_ATTEMPT_KEY); + if (!raw) return false; + const ts = Number(raw); + return Number.isFinite(ts) && Date.now() - ts < SSO_AUTO_ATTEMPT_TTL_MS; + } catch { + return false; + } +} + +function markAutoAttempt(): void { + try { + window.sessionStorage.setItem(SSO_AUTO_ATTEMPT_KEY, String(Date.now())); + } catch { + /* sessionStorage unavailable (private mode, etc.) — best effort */ + } +} export default function SsoLogin() { const { data, isLoading } = useWorkspacePublicDataQuery(); + const { data: currentUser } = useCurrentUser(); const [ldapModalOpened, setLdapModalOpened] = useState(false); const [selectedLdapProvider, setSelectedLdapProvider] = useState(null); - - if (!data?.authProviders || data?.authProviders?.length === 0) { - return null; - } + const autoRedirectedRef = useRef(false); const handleSsoLogin = (provider: IAuthProvider) => { if (provider.type === SSO_PROVIDER.LDAP) { @@ -28,10 +50,47 @@ export default function SsoLogin() { providerId: provider.id, type: provider.type, workspaceId: data.id, + redirect: getRedirectParam() ?? undefined, }); } }; + // Auto-redirect when SSO is enforced and there is exactly one non-LDAP + // provider. The user has no other option, so skip the extra click. + useEffect(() => { + if (autoRedirectedRef.current) return; + if (!data?.enforceSso) return; + if (!data.authProviders || data.authProviders.length !== 1) return; + const onlyProvider = data.authProviders[0]; + if (onlyProvider.type === SSO_PROVIDER.LDAP) return; + + // Already signed in: let useRedirectIfAuthenticated handle navigation + // instead of racing it through the IdP. + if (currentUser?.user) return; + + // Explicit logout: don't immediately bounce them back to the IdP. + const params = new URLSearchParams(window.location.search); + if (params.has("logout")) return; + + // Circuit-breaker: if we already auto-redirected within the TTL, the + // user came back (likely from an IdP failure). Show the page so they + // can read errors or pick a different account. + if (recentAutoAttempt()) return; + + autoRedirectedRef.current = true; + markAutoAttempt(); + window.location.href = buildSsoLoginUrl({ + providerId: onlyProvider.id, + type: onlyProvider.type, + workspaceId: data.id, + redirect: getRedirectParam() ?? undefined, + }); + }, [data, currentUser]); + + if (!data?.authProviders || data?.authProviders?.length === 0) { + return null; + } + const getProviderIcon = (provider: IAuthProvider) => { if (provider.type === SSO_PROVIDER.GOOGLE) { return ; diff --git a/apps/client/src/ee/security/sso.utils.ts b/apps/client/src/ee/security/sso.utils.ts index 4a4665c16..bfeb7c062 100644 --- a/apps/client/src/ee/security/sso.utils.ts +++ b/apps/client/src/ee/security/sso.utils.ts @@ -18,14 +18,21 @@ export function buildSsoLoginUrl(opts: { providerId: string; type: SSO_PROVIDER; workspaceId?: string; + redirect?: string; }): string { - const { providerId, type, workspaceId } = opts; + const { providerId, type, workspaceId, redirect } = opts; const domain = getAppUrl(); + const params = new URLSearchParams(); + if (redirect) params.set("redirect", redirect); + if (type === SSO_PROVIDER.GOOGLE) { - return `${getServerAppUrl()}/api/sso/${type}/login?workspaceId=${workspaceId}`; + if (workspaceId) params.set("workspaceId", workspaceId); + return `${getServerAppUrl()}/api/sso/${type}/login?${params.toString()}`; } - return `${domain}/api/sso/${type}/${providerId}/login`; + const query = params.toString(); + const base = `${domain}/api/sso/${type}/${providerId}/login`; + return query ? `${base}?${query}` : base; } export function getGoogleSignupUrl(): string { diff --git a/apps/client/src/features/auth/hooks/use-auth.ts b/apps/client/src/features/auth/hooks/use-auth.ts index 411e04b41..970a85897 100644 --- a/apps/client/src/features/auth/hooks/use-auth.ts +++ b/apps/client/src/features/auth/hooks/use-auth.ts @@ -166,7 +166,7 @@ export default function useAuth() { const handleLogout = async () => { setCurrentUser(RESET); await logout(); - window.location.replace(APP_ROUTE.AUTH.LOGIN); + window.location.replace(`${APP_ROUTE.AUTH.LOGIN}?logout=1`); }; const handleForgotPassword = async (data: IForgotPassword) => { diff --git a/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx b/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx index 490e179b2..e50793149 100644 --- a/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx +++ b/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx @@ -35,6 +35,7 @@ export default function TransclusionReferenceView(props: NodeViewProps) { return ( 0 ? "true" : "false"} contentEditable={false} diff --git a/apps/client/src/features/editor/components/transclusion/transclusion-view.tsx b/apps/client/src/features/editor/components/transclusion/transclusion-view.tsx index c27024472..82997f5d5 100644 --- a/apps/client/src/features/editor/components/transclusion/transclusion-view.tsx +++ b/apps/client/src/features/editor/components/transclusion/transclusion-view.tsx @@ -62,6 +62,7 @@ export default function TransclusionView(props: NodeViewProps) { return ( 0 ? "true" : "false"} data-id={transclusionId ?? undefined} > diff --git a/apps/client/src/features/editor/components/transclusion/transclusion.module.css b/apps/client/src/features/editor/components/transclusion/transclusion.module.css index 2fb5f7547..4d8d321a1 100644 --- a/apps/client/src/features/editor/components/transclusion/transclusion.module.css +++ b/apps/client/src/features/editor/components/transclusion/transclusion.module.css @@ -44,8 +44,29 @@ transition: border 0.3s; } -.transclusionWrap:hover, -.transclusionWrap:focus-within { +.transclusionWrap[data-editable="false"], +.includeWrap[data-editable="false"] { + margin-left: 0; + margin-right: 0; + width: 100%; + padding: 0; +} + +/* Cancel the wrapping .react-renderer's vertical spacing in read-only mode + so the synced block sits flush with surrounding paragraphs (whose own + margins already provide the right rhythm). */ +:global(.react-renderer.node-transclusionSource):has( + .transclusionWrap[data-editable="false"] + ), +:global(.react-renderer.node-transclusionReference):has( + .includeWrap[data-editable="false"] + ) { + margin-top: 0; + margin-bottom: 0; +} + +.transclusionWrap[data-editable="true"]:hover, +.transclusionWrap[data-editable="true"]:focus-within { border: 2px solid light-dark( var(--mantine-color-orange-2), @@ -114,9 +135,9 @@ transition: border 0.3s; } -.includeWrap:hover, -.includeWrap[data-focused="true"], -.includeWrap[data-menu-open="true"] { +.includeWrap[data-editable="true"]:hover, +.includeWrap[data-editable="true"][data-focused="true"], +.includeWrap[data-editable="true"][data-menu-open="true"] { border: 2px solid light-dark( var(--mantine-color-orange-2), diff --git a/apps/client/src/features/share/atoms/shared-page-atom.ts b/apps/client/src/features/share/atoms/shared-page-atom.ts index 813f5e61b..bf8929942 100644 --- a/apps/client/src/features/share/atoms/shared-page-atom.ts +++ b/apps/client/src/features/share/atoms/shared-page-atom.ts @@ -1,6 +1,11 @@ import { atom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; import { ISharedPageTree } from "@/features/share/types/share.types"; import { SharedPageTreeNode } from "@/features/share/utils"; export const sharedPageTreeAtom = atom(null); -export const sharedTreeDataAtom = atom(null); \ No newline at end of file +export const sharedTreeDataAtom = atom(null); +export const sharedPageFullWidthAtom = atomWithStorage( + "sharedPageFullWidth", + false, +); \ No newline at end of file diff --git a/apps/client/src/features/share/components/share-shell.tsx b/apps/client/src/features/share/components/share-shell.tsx index bba226fe7..1bd559258 100644 --- a/apps/client/src/features/share/components/share-shell.tsx +++ b/apps/client/src/features/share/components/share-shell.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActionIcon, AppShell, @@ -14,11 +14,16 @@ import { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts"; import { ThemeToggle } from "@/components/theme-toggle.tsx"; import { useAtomValue, useSetAtom } from "jotai"; import { useAtom } from "jotai"; -import { sharedPageTreeAtom, sharedTreeDataAtom } from "@/features/share/atoms/shared-page-atom"; +import { + sharedPageFullWidthAtom, + sharedPageTreeAtom, + sharedTreeDataAtom, +} from "@/features/share/atoms/shared-page-atom"; import { buildSharedPageTree } from "@/features/share/utils"; import { desktopSidebarAtom, mobileSidebarAtom, + sidebarWidthAtom, } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx"; import { useTranslation } from "react-i18next"; @@ -27,7 +32,7 @@ import { mobileTableOfContentAsideAtom, tableOfContentAsideAtom, } from "@/features/share/atoms/sidebar-atom.ts"; -import { IconList } from "@tabler/icons-react"; +import { IconArrowsHorizontal, IconList } from "@tabler/icons-react"; import { useToggleToc } from "@/features/share/hooks/use-toggle-toc.ts"; import classes from "./share.module.css"; import { @@ -55,6 +60,46 @@ export default function ShareShell({ const [mobileTocOpened] = useAtom(mobileTableOfContentAsideAtom); const toggleTocMobile = useToggleToc(mobileTableOfContentAsideAtom); const toggleToc = useToggleToc(tableOfContentAsideAtom); + const [fullWidth, setFullWidth] = useAtom(sharedPageFullWidthAtom); + const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom); + const [isResizing, setIsResizing] = useState(false); + const sidebarRef = useRef(null); + + const startResizing = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + setIsResizing(true); + }, []); + + const stopResizing = useCallback(() => { + setIsResizing(false); + }, []); + + const resize = useCallback( + (e: MouseEvent) => { + if (!isResizing || !sidebarRef.current) return; + const newWidth = + e.clientX - sidebarRef.current.getBoundingClientRect().left; + if (newWidth < 220) { + setSidebarWidth(220); + return; + } + if (newWidth > 600) { + setSidebarWidth(600); + return; + } + setSidebarWidth(newWidth); + }, + [isResizing, setSidebarWidth], + ); + + useEffect(() => { + window.addEventListener("mousemove", resize); + window.addEventListener("mouseup", stopResizing); + return () => { + window.removeEventListener("mousemove", resize); + window.removeEventListener("mouseup", stopResizing); + }; + }, [resize, stopResizing]); const { shareId } = useParams(); const { data } = useGetSharedPageTreeQuery(shareId); @@ -81,7 +126,7 @@ export default function ShareShell({ header={{ height: 50 }} {...(data?.pageTree?.length > 1 && { navbar: { - width: 300, + width: sidebarWidth, breakpoint: "sm", collapsed: { mobile: !mobileOpened, @@ -166,6 +211,20 @@ export default function ShareShell({ + + + setFullWidth((v) => !v)} + visibleFrom="sm" + size="sm" + > + + + @@ -174,7 +233,11 @@ export default function ShareShell({ {data?.pageTree?.length > 1 && ( - + +
    )} diff --git a/apps/client/src/features/share/components/share.module.css b/apps/client/src/features/share/components/share.module.css index 617768ff1..ebf1e74cb 100644 --- a/apps/client/src/features/share/components/share.module.css +++ b/apps/client/src/features/share/components/share.module.css @@ -10,6 +10,7 @@ .treeNode { text-decoration: none; user-select: none; + padding-bottom: 0; } .navbar, @@ -18,3 +19,26 @@ width: 350px; } } + +.resizeHandle { + width: 3px; + cursor: col-resize; + position: absolute; + right: 0; + top: 0; + bottom: 0; + z-index: 1; + + &:hover, + &:active { + width: 5px; + background: light-dark( + var(--mantine-color-gray-4), + var(--mantine-color-dark-5) + ); + } + + @media (max-width: $mantine-breakpoint-sm) { + display: none; + } +} diff --git a/apps/client/src/lib/app-route.ts b/apps/client/src/lib/app-route.ts index e817c0722..48c1b87d1 100644 --- a/apps/client/src/lib/app-route.ts +++ b/apps/client/src/lib/app-route.ts @@ -31,20 +31,38 @@ const APP_ROUTE = { }, }; +export function safeRedirectPath(input: unknown): string | null { + if (typeof input !== "string") return null; + if (input.length === 0 || input.length > 2048) return null; + // Reject whitespace, backslash, and any Unicode "Other" category char + // (ASCII controls, zero-width space, BOM, bidi marks, etc). + if (/[\s\\]|\p{C}/u.test(input)) return null; + if (!input.startsWith("/") || input.startsWith("//")) return null; + if (input.toLowerCase().includes("://")) return null; + if (/^\/[a-z][a-z0-9+\-.]*:/i.test(input)) return null; + try { + const resolved = new URL(input, window.location.origin); + if (resolved.origin !== window.location.origin) return null; + return resolved.pathname + resolved.search + resolved.hash; + } catch { + return null; + } +} + export function getPostLoginRedirect(): string { const params = new URLSearchParams(window.location.search); - const redirect = params.get("redirect"); - if (redirect) { - try { - const resolved = new URL(redirect, window.location.origin); - if (resolved.origin === window.location.origin) { - return resolved.pathname + resolved.search + resolved.hash; - } - } catch { - // malformed URL, fall through to default - } - } - return APP_ROUTE.HOME; + return safeRedirectPath(params.get("redirect")) ?? APP_ROUTE.HOME; +} + +/** + * Returns the `?redirect=` value from the current URL only when it is a safe + * same-origin path. Unlike {@link getPostLoginRedirect} this returns `null` + * (not `/home`) when no redirect is present, so callers can distinguish + * "user came here directly" from "user was bounced from a deep link". + */ +export function getRedirectParam(): string | null { + const params = new URLSearchParams(window.location.search); + return safeRedirectPath(params.get("redirect")); } export default APP_ROUTE; diff --git a/apps/client/src/pages/share/shared-page.tsx b/apps/client/src/pages/share/shared-page.tsx index 0db817f60..f156208e5 100644 --- a/apps/client/src/pages/share/shared-page.tsx +++ b/apps/client/src/pages/share/shared-page.tsx @@ -9,7 +9,10 @@ import { extractPageSlugId } from "@/lib"; import { Error404 } from "@/components/ui/error-404.tsx"; import ShareBranding from "@/features/share/components/share-branding.tsx"; import { useAtomValue } from "jotai"; -import { sharedTreeDataAtom } from "@/features/share/atoms/shared-page-atom.ts"; +import { + sharedPageFullWidthAtom, + sharedTreeDataAtom, +} from "@/features/share/atoms/shared-page-atom.ts"; import { isPageInTree } from "@/features/share/utils.ts"; export default function SharedPage() { @@ -23,6 +26,7 @@ export default function SharedPage() { }); const sharedTreeData = useAtomValue(sharedTreeDataAtom); + const fullWidth = useAtomValue(sharedPageFullWidthAtom); useEffect(() => { if (shareId && data) { @@ -59,7 +63,7 @@ export default function SharedPage() { )} - + =20.0.0'} - '@aws-sdk/core@3.974.7': - resolution: {integrity: sha512-YhRC90ofz5oolTJZlA8voU/oUrCB2azi8Usx51k8hhB5LpWbYQMMXKUqSqkoL0Cru+RQJgWTHpAfEDDIwfUhJw==} + '@aws-sdk/core@3.974.9': + resolution: {integrity: sha512-bXxosFunr+v/kqNb99r1NRkrVBha7CG036fRSpWGbC1A/e363XFQN6wcZMx7MYTdRr1tYwNnkrWX2xc1rT3BCQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/crc64-nvme@3.972.7': - resolution: {integrity: sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==} + '@aws-sdk/crc64-nvme@3.972.8': + resolution: {integrity: sha512-fVfUCL/Xh2zINYMPZvj+iBn6XWouQf0DAnjaWCI9MkmqXzL2Iy5FoQB8O7syFe6gN6AH1ecDDU58T51Ou0kFkA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.33': - resolution: {integrity: sha512-bJV7eViSJV6GSuuN+VIdNVPdwPsNSf75BiC2v5alPrjR/OCcqgKwSZInKbDFz9mNeizldsyf67jt6YSIiv53Cw==} + '@aws-sdk/credential-provider-env@3.972.35': + resolution: {integrity: sha512-WkFQ8BedszVomhh/Zzs8WwnE/XBmTqZjoQVB8u/4zH6kZCjouXZpPpb93gD8m0EZmzAl7dxHE/y+yDpuKzNCMw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.35': - resolution: {integrity: sha512-x/BQGEIdq0oI+4WxLjKmnQvT7CnF9r8ezdGt7wXwxb7ckHXQz0Zmgxt8v3Ne0JaT3R5YefmuybHX6E8EnsDXyA==} + '@aws-sdk/credential-provider-http@3.972.37': + resolution: {integrity: sha512-ylx0ZJTU+2eNcvXQ69VNR3TVSYa/ibpvdK717/NxqR9aXRMn2QRWZaiI8aa5yY/fOWZ5mknSmxGaVxxtdwv3EA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.37': - resolution: {integrity: sha512-eUTpmWfd/BKsq9medhCRcu+GRAhFP2Zrn7/2jKDHHOOjCkhrMoTp/t4cEthqFoG7gE0VGp5wUxrXTdvBCmSmJg==} + '@aws-sdk/credential-provider-ini@3.972.39': + resolution: {integrity: sha512-QhRSrdkk+Gq0AFIylpiI0N6lcJqFYV9Jtr4Luz5FpYOYbjJSfyTG6iLhnK/UPIgN1Jnon8WAmSC//16XYGvwkA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.37': - resolution: {integrity: sha512-Ty68y8ISSC+g5Q3D0K8uAaoINwvfaOslnNpsF/LgVUxyosYXHawcK2yV4HLXDVugiTTYLQfJfcw0ce5meAGkKw==} + '@aws-sdk/credential-provider-login@3.972.39': + resolution: {integrity: sha512-1hU0NtC04QbFIuoBuF4aQ2A97GsSE5/A0ZJpDijwexsBREIQ4KPRYl3v/FfKCPBYsaTeGjkOFx5nLhWHY24LOw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.38': - resolution: {integrity: sha512-BQ9XYnBDVxR2HuV5huXYQYF/PZMTsY+EnwfGnCU2cA8Zw63XpkOtPY8WqiMIZMQCrKPQQEiFURS/o9CIolRLqg==} + '@aws-sdk/credential-provider-node@3.972.40': + resolution: {integrity: sha512-ZgrQaGkpyTlVSCCsffzijVg+KgftTAWYvI5Otc36J/4jNiHb+7MmBiJIR0a5AHLvifC92PiYHt5pijP0dswd1w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.33': - resolution: {integrity: sha512-yfjGksI9WQbdMObb0VeLXqzTLI+a0qXLJT9gCDiv0+X/xjPpI3mTz6a5FibrhpuEKIe0gSgvs3MaoFZy5cx4WA==} + '@aws-sdk/credential-provider-process@3.972.35': + resolution: {integrity: sha512-hNj1rAwZWT1vfz54BwH8FUWxZuqStrM25Q5LEIwn2erHPMRVAjLlpZqEbCEEqS99eEEOhdeetnS0WeNa3iYeEQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.37': - resolution: {integrity: sha512-fpwE+20ntpp3i9Xb9vUuQfXLDKYHH+5I2V+ZG96SX1nBzrruhy10RXDgmN7t1etOz3c55stlA3TeQASUA451NQ==} + '@aws-sdk/credential-provider-sso@3.972.39': + resolution: {integrity: sha512-mwIPNPldyCZkvHozb6E0X/vuQLN1UCjcA6MwUf1gaO7EwghCmuNZXatq0L3zptKFvPC4Nds7+WFUkifI1XmbSw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.37': - resolution: {integrity: sha512-aryawqyebf+3WhAFNHfF62rekFpYtVcVN7dQ89qnAWsa4n5hJst8qBG6gXC24WHtW7Nnhkf9ScYnjwo0Brn3bw==} + '@aws-sdk/credential-provider-web-identity@3.972.39': + resolution: {integrity: sha512-b9HT8CnpyPVn1hU14Q7ihjwSPlRzToYmRYJxRd5jNHEZ43lrIhoLaTT8JmfQQt5j5M8rTX1iN1X8mvu0SM1dXA==} engines: {node: '>=20.0.0'} '@aws-sdk/lib-storage@3.1040.0': @@ -1057,20 +1058,20 @@ packages: peerDependencies: '@aws-sdk/client-s3': ^3.1040.0 - '@aws-sdk/middleware-bucket-endpoint@3.972.10': - resolution: {integrity: sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==} + '@aws-sdk/middleware-bucket-endpoint@3.972.11': + resolution: {integrity: sha512-AhVDn+qObNacklqmBABnFa3YfVk08CzksuuecL/x+lo95dZxXuAkqJZLUsAEKQ3EiDd5E9wTUBjh0cSogmKMYA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-expect-continue@3.972.10': - resolution: {integrity: sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==} + '@aws-sdk/middleware-expect-continue@3.972.11': + resolution: {integrity: sha512-xpobcctR1AHSrvkiArgTyLffn78Lt9unPMpa/yic9RKn+bOf/5M55UIM6RaPL5xKzI06/GSsTDywTWvzEAbyyw==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.974.15': - resolution: {integrity: sha512-j4Zp7rA1HfhDTteICnx/tPax4N/v5wmytgguXExUGyEwQ8Ug4EBA4kjp9puFAN1UZoBVpxoiXMiuTFvjaHjeEw==} + '@aws-sdk/middleware-flexible-checksums@3.974.17': + resolution: {integrity: sha512-Js24a6sdH9SU5DI5++nlQJayCuOweiiTjnCcAsY75/JtaXF+xysDQ6nRBYx6pUPNY22viRYmdDTFZDaA9AF46g==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.972.10': - resolution: {integrity: sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==} + '@aws-sdk/middleware-host-header@3.972.11': + resolution: {integrity: sha512-CBC6+tVYaOJo7QXgN1zJ4Ba2f3/Cpy4eRViYFimXW/O5Mn8hBmgXXzHu4vy4ubT80YWnp8aCFygr7dTOa14yQg==} engines: {node: '>=20.0.0'} '@aws-sdk/middleware-location-constraint@3.972.10': @@ -1081,67 +1082,63 @@ packages: resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.972.11': - resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==} + '@aws-sdk/middleware-recursion-detection@3.972.12': + resolution: {integrity: sha512-5eltYxKB4MfdQv7/VhWxRbAVQKow5dz9votRFigTYrWJHMQXwLMltlbk7KFWSZh5NDBySfmjT7Jv/DWfYCmDng==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.972.36': - resolution: {integrity: sha512-YhPix+0x/MdQrb1Ug1GDKeS5fqylIy+naz800asX8II4jqfTk2KY2KhmmYCwZcky8YWtRQQwWCGdoqeAnip8Uw==} + '@aws-sdk/middleware-sdk-s3@3.972.38': + resolution: {integrity: sha512-Yuv3urkJtd1/b3kIURzHwihc1SV6n1t+uiXffOD2OpylZ7+4/QnO2W73yhLZzK1Z762BaqwQ3IVRqAHWzNbQ4A==} engines: {node: '>=20.0.0'} '@aws-sdk/middleware-ssec@3.972.10': resolution: {integrity: sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.972.37': - resolution: {integrity: sha512-N1oNpdiLoVAWYD3WFBnUi3LlfoDA06ZHo4ozyjbsJNLvILzvt//0CnR8N+CZ0NWeYgVB/5V59ivixHCWCx2ALw==} + '@aws-sdk/middleware-user-agent@3.972.39': + resolution: {integrity: sha512-MlNSvNsSVlMKKWaCzA0GP1nS4Cuq3WCXUN1vmMvd+Ctztib5kmRcpmTtKx9kikN8szAc+gcdp7uqJJervV2nQg==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.997.5': - resolution: {integrity: sha512-jGFr6DxtcMTmzOkG/a0jCZYv4BBDmeNYVeO+/memSoDkYCJu4Y58xviYmzwJfYyIVSts+X/BVjJm1uGBnwHEMg==} + '@aws-sdk/nested-clients@3.997.7': + resolution: {integrity: sha512-jT2AXOODobQfTYGC2SChMSnZ/voIcRV/LHlY1suyhY1bdgP/voKkhEg8Ci1jiGQ4lBiaso5BEAV3ZWWpPTfmYA==} engines: {node: '>=20.0.0'} - '@aws-sdk/region-config-resolver@3.972.13': - resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==} + '@aws-sdk/region-config-resolver@3.972.14': + resolution: {integrity: sha512-VuLXVmm7+lKVxqFcOItPkXhjbJ02iUfxkxheRu41SfWf6/xrZup2A2SwHZos/LeQGu3SBHeqTQht80Uo3ienPA==} engines: {node: '>=20.0.0'} '@aws-sdk/s3-request-presigner@3.1040.0': resolution: {integrity: sha512-AmesZGG/B5sDIiWahyY11fOkXSsuHc7LciE88YFURehMVSdEORo2Vzz1d2kBgmJG9oar5Vmmwf9X/w7mqb7ytg==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.24': - resolution: {integrity: sha512-amP7tLikppN940wbBFISYqiuzVmpzMS9U3mcgtmVLjX4fdWI/SNCvrXv6ZxfVzTT4cT0rPKOLhFah2xLwzREWw==} + '@aws-sdk/signature-v4-multi-region@3.996.26': + resolution: {integrity: sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1039.0': - resolution: {integrity: sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw==} + '@aws-sdk/token-providers@3.1046.0': + resolution: {integrity: sha512-9je8nZt+ntB8IjhpGNayU/AkBgvq/f4aFO2bH1LSNC5JX6K8zY4LUnr/ymqunePrwq+B5OVBpL7ILjYzMFSZAw==} engines: {node: '>=20.0.0'} '@aws-sdk/types@3.973.8': resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-arn-parser@3.972.3': - resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + '@aws-sdk/util-endpoints@3.996.9': + resolution: {integrity: sha512-ibx8Vd73rCTHekNGeXX8cpGWoBKbNAlwKHL3yjSxxttu5QnNDaSAM7/0MFYDjU31/F4lyrPoQcGirT0ew61xcg==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.996.8': - resolution: {integrity: sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-format-url@3.972.10': - resolution: {integrity: sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==} + '@aws-sdk/util-format-url@3.972.11': + resolution: {integrity: sha512-LSzASo5djqKTUWWHhmuj9oZ6KHLaH8eUKhZLPi75imT3CcNqzkPflU9+mwl7jHP9ROsC0dSarcnpDYwyp9J/uw==} engines: {node: '>=20.0.0'} '@aws-sdk/util-locate-window@3.535.0': resolution: {integrity: sha512-PHJ3SL6d2jpcgbqdgiPxkXpu7Drc2PYViwxSIqvvMKhDwzSB1W3mMvtpzwKM4IE7zLFodZo0GKjJ9AsoXndXhA==} engines: {node: '>=14.0.0'} - '@aws-sdk/util-user-agent-browser@3.972.10': - resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==} + '@aws-sdk/util-user-agent-browser@3.972.11': + resolution: {integrity: sha512-kq3RS6XQtHMrLFShbkem6h+8fxazB3jEIsbMC6aaSInOciRGE+eGAqTgJ+obO7Euo/pjM8thVqLiLISEH9X9DA==} - '@aws-sdk/util-user-agent-node@3.973.23': - resolution: {integrity: sha512-gGwq8L2Euw0aNG6Ey4EktiAo3fSCVoDy1CaBIthd+oeaKHPXUrNaApMewQ6La5Hv0lcznOtECZaNvYyc5LXXfA==} + '@aws-sdk/util-user-agent-node@3.973.25': + resolution: {integrity: sha512-066hKH/0nvV7x4ofV/iK9kz8r/qNfcR6rzuEOFqI2vQL/fcTTsDAbTw0jmXkyMzANK8ltQdALj19ns3zuOJiUw==} engines: {node: '>=20.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -1149,8 +1146,8 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.972.22': - resolution: {integrity: sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==} + '@aws-sdk/xml-builder@3.972.23': + resolution: {integrity: sha512-A0YmgYFv+hTI9c17Ntvd2hSehm9bmJfkb+ggADBwVKA8H/3+Jx94SzR2qOB9bAA9WFeDqnfz9PKKQ+D+YAKomA==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.2.3': @@ -1817,21 +1814,12 @@ packages: '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} - '@chevrotain/cst-dts-gen@11.1.2': - resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==} - '@chevrotain/gast@11.0.3': resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} - '@chevrotain/gast@11.1.2': - resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==} - '@chevrotain/regexp-to-ast@11.0.3': resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} - '@chevrotain/regexp-to-ast@11.1.2': - resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==} - '@chevrotain/types@11.0.3': resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} @@ -1841,9 +1829,6 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@chevrotain/utils@11.1.2': - resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==} - '@clickhouse/client-common@1.18.2': resolution: {integrity: sha512-J0SG6q9V31ydxonglpj9xhNRsUxCsF71iEZ784yldqMYwsHixj/9xHFDgBDX3DuMiDx/kPDfXnf+pimp08wIBA==} @@ -2413,7 +2398,7 @@ packages: resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} engines: {node: '>=18.14.1'} peerDependencies: - hono: 4.12.14 + hono: 4.12.18 '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -2729,8 +2714,8 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@langchain/core@1.1.39': - resolution: {integrity: sha512-DP9c7TREy6iA7HnywstmUAsNyJNYTFpRg2yBfQ+6H0l1HnvQzei9GsQ36GeOLxgRaD3vm9K8urCcawSC7yQpCw==} + '@langchain/core@1.1.46': + resolution: {integrity: sha512-i8rDC83BpItxChCw4Lf+6tAr+k+OUcbirc5ZkrhI9ywYWmvxegUljLGOGYvtJNTbEAIFkhYIODPE5QRqyjF6sA==} engines: {node: '>=20'} '@langchain/textsplitters@1.0.1': @@ -2811,8 +2796,8 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} - '@mermaid-js/parser@1.0.1': - resolution: {integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==} + '@mermaid-js/parser@1.1.1': + resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} @@ -3280,8 +3265,8 @@ packages: '@protobufjs/base64@1.1.2': resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - '@protobufjs/codegen@2.0.4': - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} '@protobufjs/eventemitter@1.1.0': resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} @@ -3292,8 +3277,8 @@ packages: '@protobufjs/float@1.0.2': resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - '@protobufjs/inquire@1.1.0': - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + '@protobufjs/inquire@1.1.1': + resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -3301,8 +3286,8 @@ packages: '@protobufjs/pool@1.1.0': resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - '@protobufjs/utf8@1.1.0': - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -4140,217 +4125,156 @@ packages: '@slidoapp/emoji-mart@5.8.7': resolution: {integrity: sha512-zn8CBoZRFY0M0Tps0rFgcbu1nhQDmnSoD8CqkTVobI9k3MK2WHIYRp50DXhMxRoQnoct4nomrVaY9kU01ARDTQ==} - '@smithy/chunked-blob-reader-native@4.2.3': - resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} + '@smithy/config-resolver@4.5.1': + resolution: {integrity: sha512-abXk3LhODsvRHsk0ZS9ztrg/fZatTa9Z/z4pgx65YSLR+rY6kvUG/1IgcDKEUciR8MfdnkT5oPeHJTy/HhzDIQ==} engines: {node: '>=18.0.0'} - '@smithy/chunked-blob-reader@5.2.2': - resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} + '@smithy/core@3.24.1': + resolution: {integrity: sha512-3mT7o4qQyUWttYnVK3A0Z/u3Xha3E81tXn32Tz6vjZiUXhBrkEivpw1hBYfh84iFF9CSzkBU9Y1DJ3Q6RQ231g==} engines: {node: '>=18.0.0'} - '@smithy/config-resolver@4.4.17': - resolution: {integrity: sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==} + '@smithy/credential-provider-imds@4.3.1': + resolution: {integrity: sha512-0S/acwHnqX4WrjXzhdiDRxsG2s9SC0cpPIK9nZ1R6UOHd+j7uL28+4bHu22urbLk2TVw3fkp6na/+fkUt/pLNQ==} engines: {node: '>=18.0.0'} - '@smithy/core@3.23.17': - resolution: {integrity: sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==} + '@smithy/eventstream-serde-browser@4.3.1': + resolution: {integrity: sha512-X7MyI1fu8M84IPKk49kO4kb27Mqp6un9/0o/MsA1ngZ5OxxWKGUxPS3S/AJ9q1cPVTSGmRcbaGNfGUSsflTJkg==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.2.14': - resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==} + '@smithy/eventstream-serde-config-resolver@4.4.1': + resolution: {integrity: sha512-JZGbSXaBk7JY8VPzsh66ksJ0nTWXbApduFDkA/pEl3aTm2EoAiUZE1Iltp6c+X1bB8kxPQW0mHDfVdYCpWTOzg==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-codec@4.2.14': - resolution: {integrity: sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==} + '@smithy/eventstream-serde-node@4.3.1': + resolution: {integrity: sha512-6Cn4xTNVxn9PWTHSbvf8zmcDhQW8lrLE1Xq5CJgmX6wEvdjS2S0KuE79Aiznv/jx51jpFJ98OuWyE+Bt+oG1MQ==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-browser@4.2.14': - resolution: {integrity: sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==} + '@smithy/fetch-http-handler@5.4.1': + resolution: {integrity: sha512-r7bN6spQ+caZC8AnyvSxkRUb57zt2jhhRw3Z+2Ez8hjq6coIikDBFUUI/+CQ1xx9K6eX1Gx6wUKo4ylU66TIqw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-config-resolver@4.3.14': - resolution: {integrity: sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==} + '@smithy/hash-blob-browser@4.3.1': + resolution: {integrity: sha512-2fbltQVQYmGd0OzPv2oDMRF0pxkzeIx8cbpx2x6W3UJWGaEyUzVPxF4d0sDXZ/r2obg+RbTyhTidXWlPDsKRKw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-node@4.2.14': - resolution: {integrity: sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==} + '@smithy/hash-node@4.3.1': + resolution: {integrity: sha512-u0/zo11mg7yNneoYgTkH4sXwSmcBpbl49o4UNCtQ7hYsXxynsN25KYHmXzqi7TPk5HQL5klGnpU5koOY0O+9hw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-universal@4.2.14': - resolution: {integrity: sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==} + '@smithy/hash-stream-node@4.3.1': + resolution: {integrity: sha512-4NOnngIoXngbJw9By3u8KXRgqt4vYATpAobNBnNWxOREP7JY3kB0bUmbBNhZ7dtZV/b4auO1eFMD4cLj9OauVg==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.17': - resolution: {integrity: sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-blob-browser@4.2.15': - resolution: {integrity: sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-node@4.2.14': - resolution: {integrity: sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-stream-node@4.2.14': - resolution: {integrity: sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==} - engines: {node: '>=18.0.0'} - - '@smithy/invalid-dependency@4.2.14': - resolution: {integrity: sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==} + '@smithy/invalid-dependency@4.3.1': + resolution: {integrity: sha512-cLmwtDoulyZvRepAfyV+3rx5oMvuh51dbE+6En3vGC09j3uVSRt1U4oguNu32ub3soGX0oYtBs8E7S2Q4SxTqg==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} - '@smithy/is-array-buffer@4.2.2': - resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + '@smithy/md5-js@4.3.1': + resolution: {integrity: sha512-98NalujRdzv6ggVQNYPWpL2K57UKeUB8roIr61u6+JiHd7KUlMQ+sn/vk6IG4XxEjw2vlC7eu/xjYXshUE4XXg==} engines: {node: '>=18.0.0'} - '@smithy/md5-js@4.2.14': - resolution: {integrity: sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==} + '@smithy/middleware-content-length@4.3.1': + resolution: {integrity: sha512-l4BUIP+wljW/Ar+0/QcGdmElI9lalrywfzNijXMBG34Z510FRzPyrDLx/blNTZOAm0C4Mvx5t/bf760CZo1ajg==} engines: {node: '>=18.0.0'} - '@smithy/middleware-content-length@4.2.14': - resolution: {integrity: sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==} + '@smithy/middleware-endpoint@4.5.1': + resolution: {integrity: sha512-qtqu5TS+8Y18ZDkJoiXN5AMW1G4JAg1+xytzpsUvIR5a4EUsgd5HQg12lekEHWpm2TDUmOgg+hBaHK7dvyWdkA==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.32': - resolution: {integrity: sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==} + '@smithy/middleware-retry@4.6.1': + resolution: {integrity: sha512-eTaQhxs0rfUuAkL2MSKrH8DTO7YCeAgrdN0B2/RAeuHmXQ+x52dk5qUBsi/jtcqe5LxItgq5AG5tI6Cp8c0sow==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.5.7': - resolution: {integrity: sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==} + '@smithy/middleware-serde@4.3.1': + resolution: {integrity: sha512-t7YtUe076zWVypVmy1rX91oKi2TFJCkpfFpfMhJFpEIRPP0iL9JxjeSyFQ+1bF45JUfDzOzslUJa150WcSrBug==} engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.2.20': - resolution: {integrity: sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==} + '@smithy/middleware-stack@4.3.1': + resolution: {integrity: sha512-1jKwiKZxCMQNqmp4uVPYA6r+MLGjEtH07gnOUdPgbnjuOIrl/0JY/ICdpQtFgeBsQ/Up01gnSv8GYEL0fb8yvg==} engines: {node: '>=18.0.0'} - '@smithy/middleware-stack@4.2.14': - resolution: {integrity: sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==} + '@smithy/node-config-provider@4.4.1': + resolution: {integrity: sha512-q7tDJEJXcaSG/8TVpu2f2l9bzxTzDM9geWmltbzsY6Hfh3yiuXXTpLIO8+zwYASPPVFaTJpdKwjSSjdoDoccgw==} engines: {node: '>=18.0.0'} - '@smithy/node-config-provider@4.3.14': - resolution: {integrity: sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==} + '@smithy/node-http-handler@4.7.1': + resolution: {integrity: sha512-BdEYko85f/ldp68uH8XEyIvo810xFk6eyPH81SRggTOApYHWA+Xu7B2EzLuHbe37WVLaUA7F1fWR3/zBeme2WA==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.6.1': - resolution: {integrity: sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==} + '@smithy/protocol-http@5.4.1': + resolution: {integrity: sha512-8irPNCQgYxcSFp1aGcnDNFkTwSA+xPUaFq9V/v1+JXWu8sKr5b3cFmg2kBTkjkvypDmGeNffuNu0x5iqw1NoAw==} engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.2.14': - resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==} + '@smithy/signature-v4@5.4.1': + resolution: {integrity: sha512-728lZZEWYWubBESrfntNslZQYDKRlJDY4dcDnYbL50+gu35pGPLblu4S0/RH/RDLF6me1M87ECHsHELGL7dA/Q==} engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.3.14': - resolution: {integrity: sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-builder@4.2.14': - resolution: {integrity: sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-parser@4.2.14': - resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==} - engines: {node: '>=18.0.0'} - - '@smithy/service-error-classification@4.3.1': - resolution: {integrity: sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==} - engines: {node: '>=18.0.0'} - - '@smithy/shared-ini-file-loader@4.4.9': - resolution: {integrity: sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.3.14': - resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==} - engines: {node: '>=18.0.0'} - - '@smithy/smithy-client@4.12.13': - resolution: {integrity: sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==} + '@smithy/smithy-client@4.13.1': + resolution: {integrity: sha512-IcznNM8Qd9u1X3oflp12tkzyOB4HbT+sfYWlWiyEysgNzSHoWcHUUsTT4y1jjDjtVuuVVQbYks+g1kVd7u1eGQ==} engines: {node: '>=18.0.0'} '@smithy/types@4.14.1': resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.2.14': - resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==} + '@smithy/url-parser@4.3.1': + resolution: {integrity: sha512-tuelFlF2PZR/wogFC58NIrPOv+Zna4N1+3kA161/33D1Gbwvl6Nh4WsAsW05ZyPp0O6CMGsdbb0S2b/qVjRMCw==} engines: {node: '>=18.0.0'} - '@smithy/util-base64@4.3.2': - resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + '@smithy/util-base64@4.4.1': + resolution: {integrity: sha512-fTHiwW2xbiRiWzfSk4IGAr3gNZCH4fuRYqt8+IuarsP/YON35576iVdePraZ6yJlFxlCL0eMec3/F7xYqoKzlg==} engines: {node: '>=18.0.0'} - '@smithy/util-body-length-browser@4.2.2': - resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + '@smithy/util-body-length-browser@4.3.1': + resolution: {integrity: sha512-1scg5t4nV3hV7CZs996/XHb80aDZ5YotH4NcvkW/w/rHj+cSz0aCIzwz8aUNKB4nCDPSHRCbrKoj+TvycYefmw==} engines: {node: '>=18.0.0'} - '@smithy/util-body-length-node@4.2.3': - resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + '@smithy/util-body-length-node@4.3.1': + resolution: {integrity: sha512-VRC8MKVPKrgUYThTA7ughcKMfjW6/X92H0wXGJoda0Apw4O5xbXL0GMLz40DTWlsb5hh2iItk6+XL72uJdxYcw==} engines: {node: '>=18.0.0'} '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} - '@smithy/util-buffer-from@4.2.2': - resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + '@smithy/util-defaults-mode-browser@4.4.1': + resolution: {integrity: sha512-1rA7w+LjK1WJClsffC81Z/ZtjFt22QsKhBjUYEnZsGVS2nOTfOENKBzdg4SxhdwFvBCjcbpjscUfXOPwE3UHWQ==} engines: {node: '>=18.0.0'} - '@smithy/util-config-provider@4.2.2': - resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + '@smithy/util-defaults-mode-node@4.3.1': + resolution: {integrity: sha512-1fk1wfQHBenQD5NitVKOFgW0wsISYAFPIXGyStJWAeCtMyRhgHYvtJxBk2rwGWA0L5QX6oM6yeHSLKPFMk59ww==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.49': - resolution: {integrity: sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==} + '@smithy/util-endpoints@3.5.1': + resolution: {integrity: sha512-yORYzJD5zoGbSDkAACr0dIjDiSEA3X8h8lggDENl1dkKpCG0TQIoItPBqtvuJHzFFjRXumcoH+/09xIuixGyCw==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.54': - resolution: {integrity: sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==} + '@smithy/util-middleware@4.3.1': + resolution: {integrity: sha512-SRRMDcIgVXVhVbxviBaSZbuWuVW3jD08wv4ESV0V2oiw0Mki8TPVQ5IxwD3MvSTPg52QYsRP+JoMw5WdUdeWAg==} engines: {node: '>=18.0.0'} - '@smithy/util-endpoints@3.4.2': - resolution: {integrity: sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==} + '@smithy/util-retry@4.4.1': + resolution: {integrity: sha512-qkgWgwn1xw0GoY9Ea/B6FrYSPfHA0zyOtJkokwxZuvucRf2+2lfTut6adi4e4Y7LEAaxsFG7r6i05mtDCxbHKA==} engines: {node: '>=18.0.0'} - '@smithy/util-hex-encoding@4.2.2': - resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-middleware@4.2.14': - resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-retry@4.3.6': - resolution: {integrity: sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==} - engines: {node: '>=18.0.0'} - deprecated: '@smithy/util-retry v4.3.6 contains a bug in Adaptive Retry, see https://github.com/smithy-lang/smithy-typescript/issues/1993. Upgrade to 4.3.7+' - - '@smithy/util-stream@4.5.25': - resolution: {integrity: sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-uri-escape@4.2.2': - resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + '@smithy/util-stream@4.6.1': + resolution: {integrity: sha512-GjZfEft0M0V3n2YM/LGkr5LeLd8gxHUIzW0rUz6VtTtlAq245GxHlJghvoPEjJHKTj255iHFAiA4IsIdK40Ueg==} engines: {node: '>=18.0.0'} '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@smithy/util-utf8@4.2.2': - resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + '@smithy/util-utf8@4.3.1': + resolution: {integrity: sha512-FtRrSnriXtOs4+J8/y9SbQ1xmN71hrOsN/YJr5PQQj5nR1l7YNkGS/TEk4gr0WN7gyrUqw8/RFaYVjI18732ZA==} engines: {node: '>=18.0.0'} - '@smithy/util-waiter@4.3.0': - resolution: {integrity: sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==} - engines: {node: '>=18.0.0'} - - '@smithy/uuid@1.1.2': - resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + '@smithy/util-waiter@4.4.1': + resolution: {integrity: sha512-G/gWDykZNL0NVcd1qXkoKm45jxJECp6q53DSomM5QKMsyAMEsGksVq+HwgonqYxfFJEzzHi6ljtWKXVS1pl0/Q==} engines: {node: '>=18.0.0'} '@socket.io/component-emitter@3.1.0': @@ -5871,9 +5795,6 @@ packages: chevrotain@11.0.3: resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} - chevrotain@11.1.2: - resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -6641,6 +6562,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + es6-promise-pool@2.5.0: resolution: {integrity: sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==} engines: {node: '>=0.10.0'} @@ -7166,8 +7090,8 @@ packages: highlightjs-sap-abap@0.3.0: resolution: {integrity: sha512-nSiUvEOCycjtFA3pHaTowrbAAk5+lciBHyoVkDsd6FTRBtW9sT2dt42o2jAKbXjZVUidtacdk+j0Y2xnd233Mw==} - hono@4.12.14: - resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} + hono@4.12.18: + resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} engines: {node: '>=16.9.0'} hookified@1.15.1: @@ -7299,8 +7223,8 @@ packages: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + ip-address@10.1.1: + resolution: {integrity: sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -7891,20 +7815,16 @@ packages: postgres: optional: true - kysely@0.28.14: - resolution: {integrity: sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==} - engines: {node: '>=20.0.0'} + kysely@0.29.0: + resolution: {integrity: sha512-LrQfPUeTW7MXbMvT62moEMnpMTuj9TO3lqjCeLKjM975PJ4Alrl/43f2tlDX7xOsNptKgH4LSNGwIbXwEkLg4g==} + engines: {node: '>=22.0.0'} langium@3.3.1: resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} engines: {node: '>=16.0.0'} - langium@4.2.1: - resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - - langsmith@0.5.19: - resolution: {integrity: sha512-5tFoETuFMvGkbPGsINNlIE4Ab86CsPhdPOQZCGwNt/NX0h5NDKQLKOWS/G2XcRUBOQl4mCNbrayUvUTWaIRsCg==} + langsmith@0.7.0: + resolution: {integrity: sha512-iiPAGHJZ3uIHGnnLSkgcYZ4+thzhsGp5U48pWuW3ETgCRtbYzoDxYJigiQ3iWkK8ovF7Vr37tYvbI1ZE0tB+6A==} peerDependencies: '@opentelemetry/api': '*' '@opentelemetry/exporter-trace-otlp-proto': '*' @@ -8226,8 +8146,8 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - mermaid@11.13.0: - resolution: {integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==} + mermaid@11.15.0: + resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} @@ -9018,8 +8938,8 @@ packages: prosemirror-view@1.40.0: resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==} - protobufjs@7.5.5: - resolution: {integrity: sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==} + protobufjs@7.5.6: + resolution: {integrity: sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -10288,9 +10208,6 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -10851,182 +10768,164 @@ snapshots: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.7 - '@aws-sdk/credential-provider-node': 3.972.38 - '@aws-sdk/middleware-bucket-endpoint': 3.972.10 - '@aws-sdk/middleware-expect-continue': 3.972.10 - '@aws-sdk/middleware-flexible-checksums': 3.974.15 - '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/core': 3.974.9 + '@aws-sdk/credential-provider-node': 3.972.40 + '@aws-sdk/middleware-bucket-endpoint': 3.972.11 + '@aws-sdk/middleware-expect-continue': 3.972.11 + '@aws-sdk/middleware-flexible-checksums': 3.974.17 + '@aws-sdk/middleware-host-header': 3.972.11 '@aws-sdk/middleware-location-constraint': 3.972.10 '@aws-sdk/middleware-logger': 3.972.10 - '@aws-sdk/middleware-recursion-detection': 3.972.11 - '@aws-sdk/middleware-sdk-s3': 3.972.36 + '@aws-sdk/middleware-recursion-detection': 3.972.12 + '@aws-sdk/middleware-sdk-s3': 3.972.38 '@aws-sdk/middleware-ssec': 3.972.10 - '@aws-sdk/middleware-user-agent': 3.972.37 - '@aws-sdk/region-config-resolver': 3.972.13 - '@aws-sdk/signature-v4-multi-region': 3.996.24 + '@aws-sdk/middleware-user-agent': 3.972.39 + '@aws-sdk/region-config-resolver': 3.972.14 + '@aws-sdk/signature-v4-multi-region': 3.996.26 '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-endpoints': 3.996.8 - '@aws-sdk/util-user-agent-browser': 3.972.10 - '@aws-sdk/util-user-agent-node': 3.973.23 - '@smithy/config-resolver': 4.4.17 - '@smithy/core': 3.23.17 - '@smithy/eventstream-serde-browser': 4.2.14 - '@smithy/eventstream-serde-config-resolver': 4.3.14 - '@smithy/eventstream-serde-node': 4.2.14 - '@smithy/fetch-http-handler': 5.3.17 - '@smithy/hash-blob-browser': 4.2.15 - '@smithy/hash-node': 4.2.14 - '@smithy/hash-stream-node': 4.2.14 - '@smithy/invalid-dependency': 4.2.14 - '@smithy/md5-js': 4.2.14 - '@smithy/middleware-content-length': 4.2.14 - '@smithy/middleware-endpoint': 4.4.32 - '@smithy/middleware-retry': 4.5.7 - '@smithy/middleware-serde': 4.2.20 - '@smithy/middleware-stack': 4.2.14 - '@smithy/node-config-provider': 4.3.14 - '@smithy/node-http-handler': 4.6.1 - '@smithy/protocol-http': 5.3.14 - '@smithy/smithy-client': 4.12.13 + '@aws-sdk/util-endpoints': 3.996.9 + '@aws-sdk/util-user-agent-browser': 3.972.11 + '@aws-sdk/util-user-agent-node': 3.973.25 + '@smithy/config-resolver': 4.5.1 + '@smithy/core': 3.24.1 + '@smithy/eventstream-serde-browser': 4.3.1 + '@smithy/eventstream-serde-config-resolver': 4.4.1 + '@smithy/eventstream-serde-node': 4.3.1 + '@smithy/fetch-http-handler': 5.4.1 + '@smithy/hash-blob-browser': 4.3.1 + '@smithy/hash-node': 4.3.1 + '@smithy/hash-stream-node': 4.3.1 + '@smithy/invalid-dependency': 4.3.1 + '@smithy/md5-js': 4.3.1 + '@smithy/middleware-content-length': 4.3.1 + '@smithy/middleware-endpoint': 4.5.1 + '@smithy/middleware-retry': 4.6.1 + '@smithy/middleware-serde': 4.3.1 + '@smithy/middleware-stack': 4.3.1 + '@smithy/node-config-provider': 4.4.1 + '@smithy/node-http-handler': 4.7.1 + '@smithy/protocol-http': 5.4.1 + '@smithy/smithy-client': 4.13.1 '@smithy/types': 4.14.1 - '@smithy/url-parser': 4.2.14 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.49 - '@smithy/util-defaults-mode-node': 4.2.54 - '@smithy/util-endpoints': 3.4.2 - '@smithy/util-middleware': 4.2.14 - '@smithy/util-retry': 4.3.6 - '@smithy/util-stream': 4.5.25 - '@smithy/util-utf8': 4.2.2 - '@smithy/util-waiter': 4.3.0 + '@smithy/url-parser': 4.3.1 + '@smithy/util-base64': 4.4.1 + '@smithy/util-body-length-browser': 4.3.1 + '@smithy/util-body-length-node': 4.3.1 + '@smithy/util-defaults-mode-browser': 4.4.1 + '@smithy/util-defaults-mode-node': 4.3.1 + '@smithy/util-endpoints': 3.5.1 + '@smithy/util-middleware': 4.3.1 + '@smithy/util-retry': 4.4.1 + '@smithy/util-stream': 4.6.1 + '@smithy/util-utf8': 4.3.1 + '@smithy/util-waiter': 4.4.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.974.7': + '@aws-sdk/core@3.974.9': dependencies: '@aws-sdk/types': 3.973.8 - '@aws-sdk/xml-builder': 3.972.22 - '@smithy/core': 3.23.17 - '@smithy/node-config-provider': 4.3.14 - '@smithy/property-provider': 4.2.14 - '@smithy/protocol-http': 5.3.14 - '@smithy/signature-v4': 5.3.14 - '@smithy/smithy-client': 4.12.13 + '@aws-sdk/xml-builder': 3.972.23 + '@smithy/core': 3.24.1 + '@smithy/signature-v4': 5.4.1 '@smithy/types': 4.14.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-middleware': 4.2.14 - '@smithy/util-retry': 4.3.6 - '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/crc64-nvme@3.972.7': + '@aws-sdk/crc64-nvme@3.972.8': dependencies: '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.33': + '@aws-sdk/credential-provider-env@3.972.35': dependencies: - '@aws-sdk/core': 3.974.7 + '@aws-sdk/core': 3.974.9 '@aws-sdk/types': 3.973.8 - '@smithy/property-provider': 4.2.14 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.35': + '@aws-sdk/credential-provider-http@3.972.37': dependencies: - '@aws-sdk/core': 3.974.7 + '@aws-sdk/core': 3.974.9 '@aws-sdk/types': 3.973.8 - '@smithy/fetch-http-handler': 5.3.17 - '@smithy/node-http-handler': 4.6.1 - '@smithy/property-provider': 4.2.14 - '@smithy/protocol-http': 5.3.14 - '@smithy/smithy-client': 4.12.13 + '@smithy/core': 3.24.1 + '@smithy/fetch-http-handler': 5.4.1 + '@smithy/node-http-handler': 4.7.1 '@smithy/types': 4.14.1 - '@smithy/util-stream': 4.5.25 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.972.37': + '@aws-sdk/credential-provider-ini@3.972.39': dependencies: - '@aws-sdk/core': 3.974.7 - '@aws-sdk/credential-provider-env': 3.972.33 - '@aws-sdk/credential-provider-http': 3.972.35 - '@aws-sdk/credential-provider-login': 3.972.37 - '@aws-sdk/credential-provider-process': 3.972.33 - '@aws-sdk/credential-provider-sso': 3.972.37 - '@aws-sdk/credential-provider-web-identity': 3.972.37 - '@aws-sdk/nested-clients': 3.997.5 + '@aws-sdk/core': 3.974.9 + '@aws-sdk/credential-provider-env': 3.972.35 + '@aws-sdk/credential-provider-http': 3.972.37 + '@aws-sdk/credential-provider-login': 3.972.39 + '@aws-sdk/credential-provider-process': 3.972.35 + '@aws-sdk/credential-provider-sso': 3.972.39 + '@aws-sdk/credential-provider-web-identity': 3.972.39 + '@aws-sdk/nested-clients': 3.997.7 '@aws-sdk/types': 3.973.8 - '@smithy/credential-provider-imds': 4.2.14 - '@smithy/property-provider': 4.2.14 - '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/core': 3.24.1 + '@smithy/credential-provider-imds': 4.3.1 '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.972.37': + '@aws-sdk/credential-provider-login@3.972.39': dependencies: - '@aws-sdk/core': 3.974.7 - '@aws-sdk/nested-clients': 3.997.5 + '@aws-sdk/core': 3.974.9 + '@aws-sdk/nested-clients': 3.997.7 '@aws-sdk/types': 3.973.8 - '@smithy/property-provider': 4.2.14 - '@smithy/protocol-http': 5.3.14 - '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.972.38': + '@aws-sdk/credential-provider-node@3.972.40': dependencies: - '@aws-sdk/credential-provider-env': 3.972.33 - '@aws-sdk/credential-provider-http': 3.972.35 - '@aws-sdk/credential-provider-ini': 3.972.37 - '@aws-sdk/credential-provider-process': 3.972.33 - '@aws-sdk/credential-provider-sso': 3.972.37 - '@aws-sdk/credential-provider-web-identity': 3.972.37 + '@aws-sdk/credential-provider-env': 3.972.35 + '@aws-sdk/credential-provider-http': 3.972.37 + '@aws-sdk/credential-provider-ini': 3.972.39 + '@aws-sdk/credential-provider-process': 3.972.35 + '@aws-sdk/credential-provider-sso': 3.972.39 + '@aws-sdk/credential-provider-web-identity': 3.972.39 '@aws-sdk/types': 3.973.8 - '@smithy/credential-provider-imds': 4.2.14 - '@smithy/property-provider': 4.2.14 - '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/core': 3.24.1 + '@smithy/credential-provider-imds': 4.3.1 '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.972.33': + '@aws-sdk/credential-provider-process@3.972.35': dependencies: - '@aws-sdk/core': 3.974.7 + '@aws-sdk/core': 3.974.9 '@aws-sdk/types': 3.973.8 - '@smithy/property-provider': 4.2.14 - '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.37': + '@aws-sdk/credential-provider-sso@3.972.39': dependencies: - '@aws-sdk/core': 3.974.7 - '@aws-sdk/nested-clients': 3.997.5 - '@aws-sdk/token-providers': 3.1039.0 + '@aws-sdk/core': 3.974.9 + '@aws-sdk/nested-clients': 3.997.7 + '@aws-sdk/token-providers': 3.1046.0 '@aws-sdk/types': 3.973.8 - '@smithy/property-provider': 4.2.14 - '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.972.37': + '@aws-sdk/credential-provider-web-identity@3.972.39': dependencies: - '@aws-sdk/core': 3.974.7 - '@aws-sdk/nested-clients': 3.997.5 + '@aws-sdk/core': 3.974.9 + '@aws-sdk/nested-clients': 3.997.7 '@aws-sdk/types': 3.973.8 - '@smithy/property-provider': 4.2.14 - '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: @@ -11035,53 +10934,46 @@ snapshots: '@aws-sdk/lib-storage@3.1040.0(@aws-sdk/client-s3@3.1040.0)': dependencies: '@aws-sdk/client-s3': 3.1040.0 - '@smithy/middleware-endpoint': 4.4.32 - '@smithy/protocol-http': 5.3.14 - '@smithy/smithy-client': 4.12.13 + '@smithy/middleware-endpoint': 4.5.1 + '@smithy/protocol-http': 5.4.1 + '@smithy/smithy-client': 4.13.1 '@smithy/types': 4.14.1 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: 2.8.1 - '@aws-sdk/middleware-bucket-endpoint@3.972.10': + '@aws-sdk/middleware-bucket-endpoint@3.972.11': dependencies: + '@aws-sdk/core': 3.974.9 '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-arn-parser': 3.972.3 - '@smithy/node-config-provider': 4.3.14 - '@smithy/protocol-http': 5.3.14 - '@smithy/types': 4.14.1 - '@smithy/util-config-provider': 4.2.2 - tslib: 2.8.1 - - '@aws-sdk/middleware-expect-continue@3.972.10': - dependencies: - '@aws-sdk/types': 3.973.8 - '@smithy/protocol-http': 5.3.14 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.974.15': + '@aws-sdk/middleware-expect-continue@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.1 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.17': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.974.7 - '@aws-sdk/crc64-nvme': 3.972.7 + '@aws-sdk/core': 3.974.9 + '@aws-sdk/crc64-nvme': 3.972.8 '@aws-sdk/types': 3.973.8 - '@smithy/is-array-buffer': 4.2.2 - '@smithy/node-config-provider': 4.3.14 - '@smithy/protocol-http': 5.3.14 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 - '@smithy/util-middleware': 4.2.14 - '@smithy/util-stream': 4.5.25 - '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.972.10': + '@aws-sdk/middleware-host-header@3.972.11': dependencies: '@aws-sdk/types': 3.973.8 - '@smithy/protocol-http': 5.3.14 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 @@ -11097,29 +10989,22 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.972.11': + '@aws-sdk/middleware-recursion-detection@3.972.12': dependencies: '@aws-sdk/types': 3.973.8 '@aws/lambda-invoke-store': 0.2.3 - '@smithy/protocol-http': 5.3.14 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.972.36': + '@aws-sdk/middleware-sdk-s3@3.972.38': dependencies: - '@aws-sdk/core': 3.974.7 + '@aws-sdk/core': 3.974.9 + '@aws-sdk/signature-v4-multi-region': 3.996.26 '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-arn-parser': 3.972.3 - '@smithy/core': 3.23.17 - '@smithy/node-config-provider': 4.3.14 - '@smithy/protocol-http': 5.3.14 - '@smithy/signature-v4': 5.3.14 - '@smithy/smithy-client': 4.12.13 + '@smithy/core': 3.24.1 + '@smithy/signature-v4': 5.4.1 '@smithy/types': 4.14.1 - '@smithy/util-config-provider': 4.2.2 - '@smithy/util-middleware': 4.2.14 - '@smithy/util-stream': 4.5.25 - '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 '@aws-sdk/middleware-ssec@3.972.10': @@ -11128,96 +11013,70 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.972.37': + '@aws-sdk/middleware-user-agent@3.972.39': dependencies: - '@aws-sdk/core': 3.974.7 + '@aws-sdk/core': 3.974.9 '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-endpoints': 3.996.8 - '@smithy/core': 3.23.17 - '@smithy/protocol-http': 5.3.14 + '@aws-sdk/util-endpoints': 3.996.9 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 - '@smithy/util-retry': 4.3.6 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.997.5': + '@aws-sdk/nested-clients@3.997.7': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.7 - '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/core': 3.974.9 + '@aws-sdk/middleware-host-header': 3.972.11 '@aws-sdk/middleware-logger': 3.972.10 - '@aws-sdk/middleware-recursion-detection': 3.972.11 - '@aws-sdk/middleware-user-agent': 3.972.37 - '@aws-sdk/region-config-resolver': 3.972.13 - '@aws-sdk/signature-v4-multi-region': 3.996.24 + '@aws-sdk/middleware-recursion-detection': 3.972.12 + '@aws-sdk/middleware-user-agent': 3.972.39 + '@aws-sdk/region-config-resolver': 3.972.14 + '@aws-sdk/signature-v4-multi-region': 3.996.26 '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-endpoints': 3.996.8 - '@aws-sdk/util-user-agent-browser': 3.972.10 - '@aws-sdk/util-user-agent-node': 3.973.23 - '@smithy/config-resolver': 4.4.17 - '@smithy/core': 3.23.17 - '@smithy/fetch-http-handler': 5.3.17 - '@smithy/hash-node': 4.2.14 - '@smithy/invalid-dependency': 4.2.14 - '@smithy/middleware-content-length': 4.2.14 - '@smithy/middleware-endpoint': 4.4.32 - '@smithy/middleware-retry': 4.5.7 - '@smithy/middleware-serde': 4.2.20 - '@smithy/middleware-stack': 4.2.14 - '@smithy/node-config-provider': 4.3.14 - '@smithy/node-http-handler': 4.6.1 - '@smithy/protocol-http': 5.3.14 - '@smithy/smithy-client': 4.12.13 + '@aws-sdk/util-endpoints': 3.996.9 + '@aws-sdk/util-user-agent-browser': 3.972.11 + '@aws-sdk/util-user-agent-node': 3.973.25 + '@smithy/core': 3.24.1 + '@smithy/fetch-http-handler': 5.4.1 + '@smithy/node-http-handler': 4.7.1 '@smithy/types': 4.14.1 - '@smithy/url-parser': 4.2.14 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.49 - '@smithy/util-defaults-mode-node': 4.2.54 - '@smithy/util-endpoints': 3.4.2 - '@smithy/util-middleware': 4.2.14 - '@smithy/util-retry': 4.3.6 - '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.972.13': + '@aws-sdk/region-config-resolver@3.972.14': dependencies: '@aws-sdk/types': 3.973.8 - '@smithy/config-resolver': 4.4.17 - '@smithy/node-config-provider': 4.3.14 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 '@aws-sdk/s3-request-presigner@3.1040.0': dependencies: - '@aws-sdk/signature-v4-multi-region': 3.996.24 + '@aws-sdk/signature-v4-multi-region': 3.996.26 '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-format-url': 3.972.10 - '@smithy/middleware-endpoint': 4.4.32 - '@smithy/protocol-http': 5.3.14 - '@smithy/smithy-client': 4.12.13 + '@aws-sdk/util-format-url': 3.972.11 + '@smithy/middleware-endpoint': 4.5.1 + '@smithy/protocol-http': 5.4.1 + '@smithy/smithy-client': 4.13.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.24': + '@aws-sdk/signature-v4-multi-region@3.996.26': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.972.36 '@aws-sdk/types': 3.973.8 - '@smithy/protocol-http': 5.3.14 - '@smithy/signature-v4': 5.3.14 + '@smithy/core': 3.24.1 + '@smithy/signature-v4': 5.4.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1039.0': + '@aws-sdk/token-providers@3.1046.0': dependencies: - '@aws-sdk/core': 3.974.7 - '@aws-sdk/nested-clients': 3.997.5 + '@aws-sdk/core': 3.974.9 + '@aws-sdk/nested-clients': 3.997.7 '@aws-sdk/types': 3.973.8 - '@smithy/property-provider': 4.2.14 - '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: @@ -11228,46 +11087,38 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/util-arn-parser@3.972.3': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-endpoints@3.996.8': + '@aws-sdk/util-endpoints@3.996.9': dependencies: '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 - '@smithy/url-parser': 4.2.14 - '@smithy/util-endpoints': 3.4.2 tslib: 2.8.1 - '@aws-sdk/util-format-url@3.972.10': + '@aws-sdk/util-format-url@3.972.11': dependencies: - '@aws-sdk/types': 3.973.8 - '@smithy/querystring-builder': 4.2.14 - '@smithy/types': 4.14.1 + '@aws-sdk/core': 3.974.9 tslib: 2.8.1 '@aws-sdk/util-locate-window@3.535.0': dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.972.10': + '@aws-sdk/util-user-agent-browser@3.972.11': dependencies: '@aws-sdk/types': 3.973.8 '@smithy/types': 4.14.1 bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.973.23': + '@aws-sdk/util-user-agent-node@3.973.25': dependencies: - '@aws-sdk/middleware-user-agent': 3.972.37 + '@aws-sdk/middleware-user-agent': 3.972.39 '@aws-sdk/types': 3.973.8 - '@smithy/node-config-provider': 4.3.14 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 - '@smithy/util-config-provider': 4.2.2 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.22': + '@aws-sdk/xml-builder@3.972.23': dependencies: '@nodable/entities': 2.1.0 '@smithy/types': 4.14.1 @@ -12090,34 +11941,19 @@ snapshots: '@chevrotain/types': 11.0.3 lodash-es: 4.18.1 - '@chevrotain/cst-dts-gen@11.1.2': - dependencies: - '@chevrotain/gast': 11.1.2 - '@chevrotain/types': 11.1.2 - lodash-es: 4.18.1 - '@chevrotain/gast@11.0.3': dependencies: '@chevrotain/types': 11.0.3 lodash-es: 4.18.1 - '@chevrotain/gast@11.1.2': - dependencies: - '@chevrotain/types': 11.1.2 - lodash-es: 4.18.1 - '@chevrotain/regexp-to-ast@11.0.3': {} - '@chevrotain/regexp-to-ast@11.1.2': {} - '@chevrotain/types@11.0.3': {} '@chevrotain/types@11.1.2': {} '@chevrotain/utils@11.0.3': {} - '@chevrotain/utils@11.1.2': {} - '@clickhouse/client-common@1.18.2': {} '@clickhouse/client@1.18.2': @@ -12476,7 +12312,7 @@ snapshots: dependencies: '@excalidraw/markdown-to-text': 0.1.2 '@mermaid-js/parser': 0.6.3 - mermaid: 11.13.0 + mermaid: 11.15.0 nanoid: 4.0.2 '@excalidraw/random-username@1.1.0': {} @@ -12618,9 +12454,9 @@ snapshots: y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30) yjs: 13.6.30 - '@hono/node-server@1.19.13(hono@4.12.14)': + '@hono/node-server@1.19.13(hono@4.12.18)': dependencies: - hono: 4.12.14 + hono: 4.12.18 '@humanfs/core@0.19.1': {} @@ -13043,18 +12879,14 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)': + '@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.5.19(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + langsmith: 0.7.0(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) mustache: 4.2.0 p-queue: 6.6.2 - uuid: 11.1.0 zod: 4.3.6 transitivePeerDependencies: - '@opentelemetry/api' @@ -13063,9 +12895,9 @@ snapshots: - openai - ws - '@langchain/textsplitters@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))': + '@langchain/textsplitters@1.0.1(@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + '@langchain/core': 1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) js-tiktoken: 1.0.21 '@lifeomic/attempt@3.0.3': {} @@ -13141,13 +12973,13 @@ snapshots: dependencies: langium: 3.3.1 - '@mermaid-js/parser@1.0.1': + '@mermaid-js/parser@1.1.1': dependencies: - langium: 4.2.1 + '@chevrotain/types': 11.1.2 '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)': dependencies: - '@hono/node-server': 1.19.13(hono@4.12.14) + '@hono/node-server': 1.19.13(hono@4.12.18) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -13157,7 +12989,7 @@ snapshots: eventsource-parser: 3.0.6 express: 5.2.1 express-rate-limit: 8.2.2(express@5.2.1) - hono: 4.12.14 + hono: 4.12.18 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -13585,7 +13417,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - protobufjs: 7.5.5 + protobufjs: 7.5.6 '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': dependencies: @@ -13641,24 +13473,24 @@ snapshots: '@protobufjs/base64@1.1.2': {} - '@protobufjs/codegen@2.0.4': {} + '@protobufjs/codegen@2.0.5': {} '@protobufjs/eventemitter@1.1.0': {} '@protobufjs/fetch@1.1.0': dependencies: '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 + '@protobufjs/inquire': 1.1.1 '@protobufjs/float@1.0.2': {} - '@protobufjs/inquire@1.1.0': {} + '@protobufjs/inquire@1.1.1': {} '@protobufjs/path@1.1.2': {} '@protobufjs/pool@1.1.0': {} - '@protobufjs/utf8@1.1.0': {} + '@protobufjs/utf8@1.1.1': {} '@radix-ui/number@1.1.1': {} @@ -14502,251 +14334,148 @@ snapshots: '@slidoapp/emoji-mart@5.8.7': {} - '@smithy/chunked-blob-reader-native@4.2.3': + '@smithy/config-resolver@4.5.1': dependencies: - '@smithy/util-base64': 4.3.2 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/chunked-blob-reader@5.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/config-resolver@4.4.17': - dependencies: - '@smithy/node-config-provider': 4.3.14 - '@smithy/types': 4.14.1 - '@smithy/util-config-provider': 4.2.2 - '@smithy/util-endpoints': 3.4.2 - '@smithy/util-middleware': 4.2.14 - tslib: 2.8.1 - - '@smithy/core@3.23.17': - dependencies: - '@smithy/protocol-http': 5.3.14 - '@smithy/types': 4.14.1 - '@smithy/url-parser': 4.2.14 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-middleware': 4.2.14 - '@smithy/util-stream': 4.5.25 - '@smithy/util-utf8': 4.2.2 - '@smithy/uuid': 1.1.2 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.2.14': - dependencies: - '@smithy/node-config-provider': 4.3.14 - '@smithy/property-provider': 4.2.14 - '@smithy/types': 4.14.1 - '@smithy/url-parser': 4.2.14 - tslib: 2.8.1 - - '@smithy/eventstream-codec@4.2.14': + '@smithy/core@3.24.1': dependencies: '@aws-crypto/crc32': 5.2.0 '@smithy/types': 4.14.1 - '@smithy/util-hex-encoding': 4.2.2 tslib: 2.8.1 - '@smithy/eventstream-serde-browser@4.2.14': + '@smithy/credential-provider-imds@4.3.1': dependencies: - '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/eventstream-serde-config-resolver@4.3.14': + '@smithy/eventstream-serde-browser@4.3.1': dependencies: + '@smithy/core': 3.24.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.4.1': + dependencies: + '@smithy/core': 3.24.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.3.1': + dependencies: + '@smithy/core': 3.24.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.4.1': + dependencies: + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/eventstream-serde-node@4.2.14': + '@smithy/hash-blob-browser@4.3.1': dependencies: - '@smithy/eventstream-serde-universal': 4.2.14 - '@smithy/types': 4.14.1 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/eventstream-serde-universal@4.2.14': + '@smithy/hash-node@4.3.1': dependencies: - '@smithy/eventstream-codec': 4.2.14 - '@smithy/types': 4.14.1 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.3.17': + '@smithy/hash-stream-node@4.3.1': dependencies: - '@smithy/protocol-http': 5.3.14 - '@smithy/querystring-builder': 4.2.14 - '@smithy/types': 4.14.1 - '@smithy/util-base64': 4.3.2 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/hash-blob-browser@4.2.15': + '@smithy/invalid-dependency@4.3.1': dependencies: - '@smithy/chunked-blob-reader': 5.2.2 - '@smithy/chunked-blob-reader-native': 4.2.3 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/hash-node@4.2.14': - dependencies: - '@smithy/types': 4.14.1 - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/hash-stream-node@4.2.14': - dependencies: - '@smithy/types': 4.14.1 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/invalid-dependency@4.2.14': - dependencies: - '@smithy/types': 4.14.1 + '@smithy/core': 3.24.1 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': dependencies: tslib: 2.8.1 - '@smithy/is-array-buffer@4.2.2': + '@smithy/md5-js@4.3.1': dependencies: + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/md5-js@4.2.14': + '@smithy/middleware-content-length@4.3.1': dependencies: - '@smithy/types': 4.14.1 - '@smithy/util-utf8': 4.2.2 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/middleware-content-length@4.2.14': + '@smithy/middleware-endpoint@4.5.1': dependencies: - '@smithy/protocol-http': 5.3.14 + '@smithy/core': 3.24.1 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.6.1': + dependencies: + '@smithy/core': 3.24.1 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.3.1': + dependencies: + '@smithy/core': 3.24.1 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.3.1': + dependencies: + '@smithy/core': 3.24.1 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.4.1': + dependencies: + '@smithy/core': 3.24.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.1': + dependencies: + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.32': + '@smithy/protocol-http@5.4.1': dependencies: - '@smithy/core': 3.23.17 - '@smithy/middleware-serde': 4.2.20 - '@smithy/node-config-provider': 4.3.14 - '@smithy/shared-ini-file-loader': 4.4.9 - '@smithy/types': 4.14.1 - '@smithy/url-parser': 4.2.14 - '@smithy/util-middleware': 4.2.14 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/middleware-retry@4.5.7': + '@smithy/signature-v4@5.4.1': dependencies: - '@smithy/core': 3.23.17 - '@smithy/node-config-provider': 4.3.14 - '@smithy/protocol-http': 5.3.14 - '@smithy/service-error-classification': 4.3.1 - '@smithy/smithy-client': 4.12.13 - '@smithy/types': 4.14.1 - '@smithy/util-middleware': 4.2.14 - '@smithy/util-retry': 4.3.6 - '@smithy/uuid': 1.1.2 - tslib: 2.8.1 - - '@smithy/middleware-serde@4.2.20': - dependencies: - '@smithy/core': 3.23.17 - '@smithy/protocol-http': 5.3.14 + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/middleware-stack@4.2.14': + '@smithy/smithy-client@4.13.1': dependencies: + '@smithy/core': 3.24.1 '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/node-config-provider@4.3.14': - dependencies: - '@smithy/property-provider': 4.2.14 - '@smithy/shared-ini-file-loader': 4.4.9 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.6.1': - dependencies: - '@smithy/protocol-http': 5.3.14 - '@smithy/querystring-builder': 4.2.14 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/property-provider@4.2.14': - dependencies: - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/protocol-http@5.3.14': - dependencies: - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/querystring-builder@4.2.14': - dependencies: - '@smithy/types': 4.14.1 - '@smithy/util-uri-escape': 4.2.2 - tslib: 2.8.1 - - '@smithy/querystring-parser@4.2.14': - dependencies: - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/service-error-classification@4.3.1': - dependencies: - '@smithy/types': 4.14.1 - - '@smithy/shared-ini-file-loader@4.4.9': - dependencies: - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/signature-v4@5.3.14': - dependencies: - '@smithy/is-array-buffer': 4.2.2 - '@smithy/protocol-http': 5.3.14 - '@smithy/types': 4.14.1 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-middleware': 4.2.14 - '@smithy/util-uri-escape': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/smithy-client@4.12.13': - dependencies: - '@smithy/core': 3.23.17 - '@smithy/middleware-endpoint': 4.4.32 - '@smithy/middleware-stack': 4.2.14 - '@smithy/protocol-http': 5.3.14 - '@smithy/types': 4.14.1 - '@smithy/util-stream': 4.5.25 - tslib: 2.8.1 - '@smithy/types@4.14.1': dependencies: tslib: 2.8.1 - '@smithy/url-parser@4.2.14': + '@smithy/url-parser@4.3.1': dependencies: - '@smithy/querystring-parser': 4.2.14 - '@smithy/types': 4.14.1 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/util-base64@4.3.2': + '@smithy/util-base64@4.4.1': dependencies: - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-utf8': 4.2.2 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/util-body-length-browser@4.2.2': + '@smithy/util-body-length-browser@4.3.1': dependencies: + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/util-body-length-node@4.2.3': + '@smithy/util-body-length-node@4.3.1': dependencies: + '@smithy/core': 3.24.1 tslib: 2.8.1 '@smithy/util-buffer-from@2.2.0': @@ -14754,66 +14483,34 @@ snapshots: '@smithy/is-array-buffer': 2.2.0 tslib: 2.8.1 - '@smithy/util-buffer-from@4.2.2': + '@smithy/util-defaults-mode-browser@4.4.1': dependencies: - '@smithy/is-array-buffer': 4.2.2 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/util-config-provider@4.2.2': + '@smithy/util-defaults-mode-node@4.3.1': dependencies: + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.49': + '@smithy/util-endpoints@3.5.1': dependencies: - '@smithy/property-provider': 4.2.14 - '@smithy/smithy-client': 4.12.13 - '@smithy/types': 4.14.1 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.54': + '@smithy/util-middleware@4.3.1': dependencies: - '@smithy/config-resolver': 4.4.17 - '@smithy/credential-provider-imds': 4.2.14 - '@smithy/node-config-provider': 4.3.14 - '@smithy/property-provider': 4.2.14 - '@smithy/smithy-client': 4.12.13 - '@smithy/types': 4.14.1 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/util-endpoints@3.4.2': + '@smithy/util-retry@4.4.1': dependencies: - '@smithy/node-config-provider': 4.3.14 - '@smithy/types': 4.14.1 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/util-hex-encoding@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-middleware@4.2.14': - dependencies: - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/util-retry@4.3.6': - dependencies: - '@smithy/service-error-classification': 4.3.1 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/util-stream@4.5.25': - dependencies: - '@smithy/fetch-http-handler': 5.3.17 - '@smithy/node-http-handler': 4.6.1 - '@smithy/types': 4.14.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-uri-escape@4.2.2': + '@smithy/util-stream@4.6.1': dependencies: + '@smithy/core': 3.24.1 tslib: 2.8.1 '@smithy/util-utf8@2.3.0': @@ -14821,18 +14518,14 @@ snapshots: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@smithy/util-utf8@4.2.2': + '@smithy/util-utf8@4.3.1': dependencies: - '@smithy/util-buffer-from': 4.2.2 + '@smithy/core': 3.24.1 tslib: 2.8.1 - '@smithy/util-waiter@4.3.0': - dependencies: - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/uuid@1.1.2': + '@smithy/util-waiter@4.4.1': dependencies: + '@smithy/core': 3.24.1 tslib: 2.8.1 '@socket.io/component-emitter@3.1.0': {} @@ -16575,11 +16268,6 @@ snapshots: chevrotain: 11.0.3 lodash-es: 4.18.1 - chevrotain-allstar@0.3.1(chevrotain@11.1.2): - dependencies: - chevrotain: 11.1.2 - lodash-es: 4.18.1 - chevrotain@11.0.3: dependencies: '@chevrotain/cst-dts-gen': 11.0.3 @@ -16589,15 +16277,6 @@ snapshots: '@chevrotain/utils': 11.0.3 lodash-es: 4.18.1 - chevrotain@11.1.2: - dependencies: - '@chevrotain/cst-dts-gen': 11.1.2 - '@chevrotain/gast': 11.1.2 - '@chevrotain/regexp-to-ast': 11.1.2 - '@chevrotain/types': 11.1.2 - '@chevrotain/utils': 11.1.2 - lodash-es: 4.18.1 - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -17423,6 +17102,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.46.1: {} + es6-promise-pool@2.5.0: {} esbuild@0.27.4: @@ -17712,7 +17393,7 @@ snapshots: express-rate-limit@8.2.2(express@5.2.1): dependencies: express: 5.2.1 - ip-address: 10.1.0 + ip-address: 10.1.1 express@5.2.1: dependencies: @@ -18112,7 +17793,7 @@ snapshots: highlightjs-sap-abap@0.3.0: {} - hono@4.12.14: {} + hono@4.12.18: {} hookified@1.15.1: {} @@ -18257,7 +17938,7 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.1.0: {} + ip-address@10.1.1: {} ipaddr.js@1.9.1: {} @@ -19005,14 +18686,14 @@ snapshots: klona@2.0.6: {} - kysely-codegen@0.20.0(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3): + kysely-codegen@0.20.0(kysely@0.29.0)(pg@8.16.3)(typescript@5.9.3): dependencies: chalk: 4.1.2 cosmiconfig: 9.0.0(typescript@5.9.3) diff: 8.0.3 dotenv: 17.2.4 dotenv-expand: 12.0.3 - kysely: 0.28.14 + kysely: 0.29.0 micromatch: 4.0.8 minimist: 1.2.8 pluralize: 8.0.0 @@ -19027,13 +18708,13 @@ snapshots: '@commander-js/extra-typings': 11.1.0(commander@11.1.0) commander: 11.1.0 - kysely-postgres-js@3.0.0(kysely@0.28.14)(postgres@3.4.8): + kysely-postgres-js@3.0.0(kysely@0.29.0)(postgres@3.4.8): dependencies: - kysely: 0.28.14 + kysely: 0.29.0 optionalDependencies: postgres: 3.4.8 - kysely@0.28.14: {} + kysely@0.29.0: {} langium@3.3.1: dependencies: @@ -19043,18 +18724,9 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 - langium@4.2.1: - dependencies: - chevrotain: 11.1.2 - chevrotain-allstar: 0.3.1(chevrotain@11.1.2) - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - langsmith@0.5.19(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0): + langsmith@0.7.0(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0): dependencies: p-queue: 6.6.2 - uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) @@ -19325,11 +18997,11 @@ snapshots: merge-stream@2.0.0: {} - mermaid@11.13.0: + mermaid@11.15.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.1 + '@mermaid-js/parser': 1.1.1 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.1 @@ -19340,14 +19012,14 @@ snapshots: dagre-d3-es: 7.0.14 dayjs: 1.11.19 dompurify: 3.4.1 + es-toolkit: 1.46.1 katex: 0.16.40 khroma: 2.1.0 - lodash-es: 4.18.1 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.3.6 ts-dedent: 2.2.0 - uuid: 11.1.0 + uuid: 14.0.0 methods@1.1.2: {} @@ -19470,11 +19142,11 @@ snapshots: reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.28.14)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.29.0)(reflect-metadata@0.2.2): dependencies: '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) - kysely: 0.28.14 + kysely: 0.29.0 reflect-metadata: 0.2.2 nestjs-pino@4.6.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): @@ -20214,18 +19886,18 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 - protobufjs@7.5.5: + protobufjs@7.5.6: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 + '@protobufjs/codegen': 2.0.5 '@protobufjs/eventemitter': 1.1.0 '@protobufjs/fetch': 1.1.0 '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 + '@protobufjs/inquire': 1.1.1 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 + '@protobufjs/utf8': 1.1.1 '@types/node': 25.5.0 long: 5.3.2 @@ -21607,8 +21279,6 @@ snapshots: vscode-uri@3.0.8: {} - vscode-uri@3.1.0: {} - w3c-keyname@2.2.8: {} w3c-xmlserializer@5.0.0: From 3b983a27f62f0341c449773ee8372c6124bfb95d Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 14 May 2026 03:01:55 +0100 Subject: [PATCH 04/12] sync --- apps/server/package.json | 2 +- apps/server/src/ee | 2 +- pnpm-lock.yaml | 30 +++++++++++++++--------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index f3e14770d..17d763fb8 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -81,7 +81,7 @@ "ioredis": "^5.10.1", "js-tiktoken": "^1.0.21", "jsonwebtoken": "^9.0.3", - "kysely": "^0.29.0", + "kysely": "^0.28.17", "kysely-migration-cli": "^0.4.2", "kysely-postgres-js": "^3.0.0", "ldapts": "^8.1.7", diff --git a/apps/server/src/ee b/apps/server/src/ee index 71844c097..b30e92f6a 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 71844c097213ee3d861bf5b0e91eb6766b09d215 +Subproject commit b30e92f6a024b2b1106a8243e5a313122c4b0712 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58423036b..e7980205a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -637,14 +637,14 @@ importers: specifier: ^9.0.3 version: 9.0.3 kysely: - specifier: ^0.29.0 - version: 0.29.0 + specifier: ^0.28.17 + version: 0.28.17 kysely-migration-cli: specifier: ^0.4.2 version: 0.4.2 kysely-postgres-js: specifier: ^3.0.0 - version: 3.0.0(kysely@0.29.0)(postgres@3.4.8) + version: 3.0.0(kysely@0.28.17)(postgres@3.4.8) ldapts: specifier: ^8.1.7 version: 8.1.7 @@ -668,7 +668,7 @@ importers: version: 6.2.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: ^3.1.2 - version: 3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.29.0)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.28.17)(reflect-metadata@0.2.2) nestjs-pino: specifier: ^4.6.1 version: 4.6.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) @@ -819,7 +819,7 @@ importers: version: 30.3.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)(typescript@5.9.3)) kysely-codegen: specifier: ^0.20.0 - version: 0.20.0(kysely@0.29.0)(pg@8.16.3)(typescript@5.9.3) + version: 0.20.0(kysely@0.28.17)(pg@8.16.3)(typescript@5.9.3) prettier: specifier: ^3.8.1 version: 3.8.1 @@ -7815,9 +7815,9 @@ packages: postgres: optional: true - kysely@0.29.0: - resolution: {integrity: sha512-LrQfPUeTW7MXbMvT62moEMnpMTuj9TO3lqjCeLKjM975PJ4Alrl/43f2tlDX7xOsNptKgH4LSNGwIbXwEkLg4g==} - engines: {node: '>=22.0.0'} + kysely@0.28.17: + resolution: {integrity: sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==} + engines: {node: '>=20.0.0'} langium@3.3.1: resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} @@ -18686,14 +18686,14 @@ snapshots: klona@2.0.6: {} - kysely-codegen@0.20.0(kysely@0.29.0)(pg@8.16.3)(typescript@5.9.3): + kysely-codegen@0.20.0(kysely@0.28.17)(pg@8.16.3)(typescript@5.9.3): dependencies: chalk: 4.1.2 cosmiconfig: 9.0.0(typescript@5.9.3) diff: 8.0.3 dotenv: 17.2.4 dotenv-expand: 12.0.3 - kysely: 0.29.0 + kysely: 0.28.17 micromatch: 4.0.8 minimist: 1.2.8 pluralize: 8.0.0 @@ -18708,13 +18708,13 @@ snapshots: '@commander-js/extra-typings': 11.1.0(commander@11.1.0) commander: 11.1.0 - kysely-postgres-js@3.0.0(kysely@0.29.0)(postgres@3.4.8): + kysely-postgres-js@3.0.0(kysely@0.28.17)(postgres@3.4.8): dependencies: - kysely: 0.29.0 + kysely: 0.28.17 optionalDependencies: postgres: 3.4.8 - kysely@0.29.0: {} + kysely@0.28.17: {} langium@3.3.1: dependencies: @@ -19142,11 +19142,11 @@ snapshots: reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.29.0)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.28.17)(reflect-metadata@0.2.2): dependencies: '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) - kysely: 0.29.0 + kysely: 0.28.17 reflect-metadata: 0.2.2 nestjs-pino@4.6.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): From f4af4c3fc0957f7cc7005182a09f41ccba0ec032 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Thu, 14 May 2026 03:48:13 +0100 Subject: [PATCH 05/12] feat(editor): add page break node (#2202) --- .../public/locales/en-US/translation.json | 2 + .../fixed-toolbar/groups/block-type-group.tsx | 7 +++ .../components/slash-menu/menu-items.ts | 9 +++ .../features/editor/extensions/extensions.ts | 2 + .../src/features/editor/styles/index.css | 1 + .../src/features/editor/styles/page-break.css | 50 ++++++++++++++++ .../src/collaboration/collaboration.util.ts | 2 + packages/editor-ext/src/index.ts | 1 + .../editor-ext/src/lib/page-break/index.ts | 1 + .../src/lib/page-break/page-break.ts | 60 +++++++++++++++++++ 10 files changed, 135 insertions(+) create mode 100644 apps/client/src/features/editor/styles/page-break.css create mode 100644 packages/editor-ext/src/lib/page-break/index.ts create mode 100644 packages/editor-ext/src/lib/page-break/page-break.ts diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 268d696c8..e85f2af23 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -361,6 +361,8 @@ "Create block quote.": "Create block quote.", "Insert code snippet.": "Insert code snippet.", "Insert horizontal rule divider": "Insert horizontal rule divider", + "Page break": "Page break", + "Insert a page break for printing.": "Insert a page break for printing.", "Upload any image from your device.": "Upload any image from your device.", "Upload any video from your device.": "Upload any video from your device.", "Upload any audio from your device.": "Upload any audio from your device.", diff --git a/apps/client/src/features/editor/components/fixed-toolbar/groups/block-type-group.tsx b/apps/client/src/features/editor/components/fixed-toolbar/groups/block-type-group.tsx index 3edb28eda..69911f7cb 100644 --- a/apps/client/src/features/editor/components/fixed-toolbar/groups/block-type-group.tsx +++ b/apps/client/src/features/editor/components/fixed-toolbar/groups/block-type-group.tsx @@ -10,6 +10,7 @@ import { IconH2, IconH3, IconMenu4, + IconPageBreak, IconTypography, } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; @@ -102,6 +103,12 @@ export const BlockTypeGroup: FC = ({ editor }) => { > {t("Divider")} + } + onClick={() => editor.chain().focus().setPageBreak().run()} + > + {t("Page break")} + ); diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts index 4a0532fe3..cddddc35f 100644 --- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts @@ -19,6 +19,7 @@ import { IconTable, IconTypography, IconMenu4, + IconPageBreak, IconCalendar, IconAppWindow, IconSitemap, @@ -164,6 +165,14 @@ const CommandGroups: SlashMenuGroupedItemsType = { command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).setHorizontalRule().run(), }, + { + title: "Page break", + description: "Insert a page break for printing.", + searchTerms: ["page", "break", "pagebreak", "print"], + icon: IconPageBreak, + command: ({ editor, range }: CommandProps) => + editor.chain().focus().deleteRange(range).setPageBreak().run(), + }, { title: "Image", description: "Upload any image from your device.", diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 1f09bef37..91411daef 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -42,6 +42,7 @@ import { Excalidraw, Embed, TiptapPdf, + PageBreak, SearchAndReplace, Mention, TableDndExtension, @@ -366,6 +367,7 @@ export const mainExtensions = [ TiptapPdf.configure({ view: PdfView, }), + PageBreak, Subpages.configure({ view: SubpagesView, }), diff --git a/apps/client/src/features/editor/styles/index.css b/apps/client/src/features/editor/styles/index.css index 7abfe1086..52d9268e1 100644 --- a/apps/client/src/features/editor/styles/index.css +++ b/apps/client/src/features/editor/styles/index.css @@ -9,6 +9,7 @@ @import "./media.css"; @import "./code.css"; @import "./print.css"; +@import "./page-break.css"; @import "./find.css"; @import "./mention.css"; @import "./ordered-list.css"; diff --git a/apps/client/src/features/editor/styles/page-break.css b/apps/client/src/features/editor/styles/page-break.css new file mode 100644 index 000000000..6dc97c738 --- /dev/null +++ b/apps/client/src/features/editor/styles/page-break.css @@ -0,0 +1,50 @@ +.ProseMirror .page-break { + position: relative; + margin: 1.5rem 0; + border-top: 1px dashed var(--mantine-color-default-border); + height: 0; + user-select: none; +} + +.ProseMirror[contenteditable="false"] .page-break { + margin: 0; + border: none; + height: 0; +} + +.ProseMirror[contenteditable="false"] .page-break::after { + content: none; +} + +.ProseMirror .page-break::after { + content: "Page break"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 0 0.5rem; + background: var(--mantine-color-body); + color: var(--mantine-color-dimmed); + font-size: 0.75rem; + line-height: 1; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.ProseMirror .page-break.ProseMirror-selectednode { + border-top-color: var(--mantine-primary-color-filled); +} + +@media print { + .ProseMirror .page-break { + break-before: always; + page-break-before: always; + visibility: hidden; + border: none; + margin: 0; + } + + .ProseMirror .page-break::after { + content: none; + } +} diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index 5787e2e3a..554aa43bd 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -26,6 +26,7 @@ import { TiptapVideo, TiptapAudio, TiptapPdf, + PageBreak, TrailingNode, Attachment, Drawio, @@ -94,6 +95,7 @@ export const tiptapExtensions = [ TiptapVideo, TiptapAudio, TiptapPdf, + PageBreak, Callout, Attachment, CustomCodeBlock, diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts index 354b1a617..003d22886 100644 --- a/packages/editor-ext/src/index.ts +++ b/packages/editor-ext/src/index.ts @@ -31,5 +31,6 @@ export * from "./lib/recreate-transform"; export * from "./lib/columns"; export * from "./lib/status"; export * from "./lib/pdf"; +export * from "./lib/page-break"; export * from "./lib/resizable-nodeview"; diff --git a/packages/editor-ext/src/lib/page-break/index.ts b/packages/editor-ext/src/lib/page-break/index.ts new file mode 100644 index 000000000..701b20b78 --- /dev/null +++ b/packages/editor-ext/src/lib/page-break/index.ts @@ -0,0 +1 @@ +export * from "./page-break"; diff --git a/packages/editor-ext/src/lib/page-break/page-break.ts b/packages/editor-ext/src/lib/page-break/page-break.ts new file mode 100644 index 000000000..f8991b266 --- /dev/null +++ b/packages/editor-ext/src/lib/page-break/page-break.ts @@ -0,0 +1,60 @@ +import { mergeAttributes, Node } from "@tiptap/core"; + +export interface PageBreakOptions { + HTMLAttributes: Record; +} + +declare module "@tiptap/core" { + interface Commands { + pageBreak: { + setPageBreak: () => ReturnType; + }; + } +} + +export const PageBreak = Node.create({ + name: "pageBreak", + + group: "block", + + atom: true, + + selectable: true, + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + parseHTML() { + return [ + { + tag: `div[data-type="${this.name}"]`, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes( + { "data-type": this.name, class: "page-break" }, + this.options.HTMLAttributes, + HTMLAttributes, + ), + ]; + }, + + addCommands() { + return { + setPageBreak: + () => + ({ chain }) => + chain() + .insertContent({ type: this.name }) + .focus() + .run(), + }; + }, +}); From f758091b2af8341758e05ef2a82b27ee32f09672 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Thu, 14 May 2026 13:11:28 +0100 Subject: [PATCH 06/12] perf(permissions): cache space role and page edit lookups (#2208) --- apps/server/src/common/helpers/cache-keys.ts | 8 ++ apps/server/src/common/helpers/with-cache.ts | 27 ++++ .../repos/page/page-permission.repo.ts | 123 ++++++++---------- .../database/repos/space/space-member.repo.ts | 55 +++++--- 4 files changed, 125 insertions(+), 88 deletions(-) create mode 100644 apps/server/src/common/helpers/with-cache.ts diff --git a/apps/server/src/common/helpers/cache-keys.ts b/apps/server/src/common/helpers/cache-keys.ts index 570c96d88..38b24d20e 100644 --- a/apps/server/src/common/helpers/cache-keys.ts +++ b/apps/server/src/common/helpers/cache-keys.ts @@ -1,3 +1,11 @@ export const CacheKey = { LICENSE_VALID: (workspaceId: string) => `license:valid:${workspaceId}`, + SPACE_ROLES: (userId: string, spaceId: string) => + `perm:space-roles:${userId}:${spaceId}`, + PAGE_CAN_EDIT: (userId: string, pageId: string) => + `perm:can-edit:${userId}:${pageId}`, }; + +// Permission caches dedupe repeated checks within and across short request bursts. +// 5s keeps staleness on revocations bounded. +export const PERMISSION_CACHE_TTL_MS = 5_000; diff --git a/apps/server/src/common/helpers/with-cache.ts b/apps/server/src/common/helpers/with-cache.ts new file mode 100644 index 000000000..2db1d3c02 --- /dev/null +++ b/apps/server/src/common/helpers/with-cache.ts @@ -0,0 +1,27 @@ +import { Cache } from 'cache-manager'; + +export async function withCache( + cacheManager: Cache, + key: string, + ttlMs: number, + fn: () => Promise, +): Promise { + try { + const cached = await cacheManager.get<{ v: T }>(key); + if (cached !== undefined && cached !== null) { + return cached.v; + } + } catch (err) { + console.warn(`[withCache] get failed for "${key}", falling back to source`, err); + } + + const value = await fn(); + + try { + await cacheManager.set(key, { v: value }, ttlMs); + } catch (err) { + console.warn(`[withCache] set failed for "${key}"`, err); + } + + return value; +} diff --git a/apps/server/src/database/repos/page/page-permission.repo.ts b/apps/server/src/database/repos/page/page-permission.repo.ts index fbd7423ce..f753526cd 100644 --- a/apps/server/src/database/repos/page/page-permission.repo.ts +++ b/apps/server/src/database/repos/page/page-permission.repo.ts @@ -1,4 +1,6 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; import { dbOrTx } from '@docmost/db/utils'; @@ -17,6 +19,11 @@ import { executeWithCursorPagination, } from '@docmost/db/pagination/cursor-pagination'; import { PagePermissionMember } from './types/page-permission.types'; +import { withCache } from '../../../common/helpers/with-cache'; +import { + CacheKey, + PERMISSION_CACHE_TTL_MS, +} from '../../../common/helpers/cache-keys'; export { PagePermissionMember } from './types/page-permission.types'; @@ -25,6 +32,7 @@ export class PagePermissionRepo { constructor( @InjectKysely() private readonly db: KyselyDB, private readonly groupRepo: GroupRepo, + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, ) {} async findPageAccessByPageId( @@ -361,40 +369,8 @@ export class PagePermissionRepo { * Check if user can access a page by verifying they have permission on ALL restricted ancestors. */ async canUserAccessPage(userId: string, pageId: string): Promise { - const deniedAncestor = await this.db - .withRecursive('ancestors', (qb) => - qb - .selectFrom('pages') - .select(['pages.id as ancestorId', 'pages.parentPageId']) - .where('pages.id', '=', pageId) - .unionAll((eb) => - eb - .selectFrom('pages') - .innerJoin('ancestors', 'ancestors.parentPageId', 'pages.id') - .select(['pages.id as ancestorId', 'pages.parentPageId']), - ), - ) - .selectFrom('ancestors') - .innerJoin('pageAccess', 'pageAccess.pageId', 'ancestors.ancestorId') - .leftJoin('pagePermissions', (join) => - join - .onRef('pagePermissions.pageAccessId', '=', 'pageAccess.id') - .on((eb) => - eb.or([ - eb('pagePermissions.userId', '=', userId), - eb( - 'pagePermissions.groupId', - 'in', - this.userGroupIdsSubquery(eb, userId), - ), - ]), - ), - ) - .select('pageAccess.pageId') - .where('pagePermissions.id', 'is', null) - .executeTakeFirst(); - - return !deniedAncestor; + const { canAccess } = await this.canUserEditPage(userId, pageId); + return canAccess; } /** @@ -412,43 +388,50 @@ export class PagePermissionRepo { canAccess: boolean; canEdit: boolean; }> { - const result = await sql<{ - canAccess: boolean | null; - canEdit: boolean | null; - }>` - WITH RECURSIVE ancestors AS ( - SELECT id AS ancestor_id, parent_page_id, 0 AS depth - FROM pages - WHERE id = ${pageId}::uuid - UNION ALL - SELECT p.id, p.parent_page_id, a.depth + 1 - FROM pages p - JOIN ancestors a ON a.parent_page_id = p.id - ) - SELECT - bool_and(pp.id IS NOT NULL) AS "canAccess", - -- nearest restricted ancestor's highest role wins (DESC: 'writer' > 'reader', NULLS LAST: no-permission after real roles) - (array_agg(pp.role ORDER BY a.depth ASC, pp.role DESC NULLS LAST))[1] = 'writer' AS "canEdit" - FROM ancestors a - JOIN page_access pa ON pa.page_id = a.ancestor_id - LEFT JOIN page_permissions pp ON pp.page_access_id = pa.id - AND ( - pp.user_id = ${userId}::uuid - OR pp.group_id IN ( - SELECT gu.group_id FROM group_users gu WHERE gu.user_id = ${userId}::uuid + return withCache( + this.cacheManager, + CacheKey.PAGE_CAN_EDIT(userId, pageId), + PERMISSION_CACHE_TTL_MS, + async () => { + const result = await sql<{ + canAccess: boolean | null; + canEdit: boolean | null; + }>` + WITH RECURSIVE ancestors AS ( + SELECT id AS ancestor_id, parent_page_id, 0 AS depth + FROM pages + WHERE id = ${pageId}::uuid + UNION ALL + SELECT p.id, p.parent_page_id, a.depth + 1 + FROM pages p + JOIN ancestors a ON a.parent_page_id = p.id ) - ) - `.execute(this.db); + SELECT + bool_and(pp.id IS NOT NULL) AS "canAccess", + -- nearest restricted ancestor's highest role wins (DESC: 'writer' > 'reader', NULLS LAST: no-permission after real roles) + (array_agg(pp.role ORDER BY a.depth ASC, pp.role DESC NULLS LAST))[1] = 'writer' AS "canEdit" + FROM ancestors a + JOIN page_access pa ON pa.page_id = a.ancestor_id + LEFT JOIN page_permissions pp ON pp.page_access_id = pa.id + AND ( + pp.user_id = ${userId}::uuid + OR pp.group_id IN ( + SELECT gu.group_id FROM group_users gu WHERE gu.user_id = ${userId}::uuid + ) + ) + `.execute(this.db); - const row = result.rows[0]; - if (!row || row.canAccess === null) { - return { hasAnyRestriction: false, canAccess: true, canEdit: true }; - } - return { - hasAnyRestriction: true, - canAccess: row.canAccess, - canEdit: row.canAccess && (row.canEdit ?? false), - }; + const row = result.rows[0]; + if (!row || row.canAccess === null) { + return { hasAnyRestriction: false, canAccess: true, canEdit: true }; + } + return { + hasAnyRestriction: true, + canAccess: row.canAccess, + canEdit: row.canAccess && (row.canEdit ?? false), + }; + }, + ); } /** diff --git a/apps/server/src/database/repos/space/space-member.repo.ts b/apps/server/src/database/repos/space/space-member.repo.ts index 50961802f..6711c30c7 100644 --- a/apps/server/src/database/repos/space/space-member.repo.ts +++ b/apps/server/src/database/repos/space/space-member.repo.ts @@ -1,4 +1,6 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; import { dbOrTx } from '@docmost/db/utils'; @@ -13,6 +15,11 @@ import { MemberInfo, UserSpaceRole } from './types'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination'; import { GroupRepo } from '@docmost/db/repos/group/group.repo'; import { SpaceRepo } from '@docmost/db/repos/space/space.repo'; +import { withCache } from '../../../common/helpers/with-cache'; +import { + CacheKey, + PERMISSION_CACHE_TTL_MS, +} from '../../../common/helpers/cache-keys'; @Injectable() export class SpaceMemberRepo { @@ -20,6 +27,7 @@ export class SpaceMemberRepo { @InjectKysely() private readonly db: KyselyDB, private readonly groupRepo: GroupRepo, private readonly spaceRepo: SpaceRepo, + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, ) {} async insertSpaceMember( @@ -214,25 +222,36 @@ export class SpaceMemberRepo { userId: string, spaceId: string, ): Promise { - const roles = await this.db - .selectFrom('spaceMembers') - .select(['userId', 'role']) - .where('userId', '=', userId) - .where('spaceId', '=', spaceId) - .unionAll( - this.db + return withCache( + this.cacheManager, + CacheKey.SPACE_ROLES(userId, spaceId), + PERMISSION_CACHE_TTL_MS, + async () => { + const roles = await this.db .selectFrom('spaceMembers') - .innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId') - .select(['groupUsers.userId', 'spaceMembers.role']) - .where('groupUsers.userId', '=', userId) - .where('spaceMembers.spaceId', '=', spaceId), - ) - .execute(); + .select(['userId', 'role']) + .where('userId', '=', userId) + .where('spaceId', '=', spaceId) + .unionAll( + this.db + .selectFrom('spaceMembers') + .innerJoin( + 'groupUsers', + 'groupUsers.groupId', + 'spaceMembers.groupId', + ) + .select(['groupUsers.userId', 'spaceMembers.role']) + .where('groupUsers.userId', '=', userId) + .where('spaceMembers.spaceId', '=', spaceId), + ) + .execute(); - if (!roles || roles.length === 0) { - return undefined; - } - return roles; + if (!roles || roles.length === 0) { + return undefined; + } + return roles; + }, + ); } async getUserIdsWithSpaceAccess( From 82d065669dfb2617449aa664f34fae4fadd68f7d Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Thu, 14 May 2026 14:15:03 +0200 Subject: [PATCH 07/12] fix: page mode toggle no longer overwrites default preference (#1996) The header edit/read toggle now controls only the current session's mode without saving it as the user's preference. The saved preference (set in profile settings) is applied once on initial load and sticks across page navigations within the session, so navigating to a new page no longer resets the mode mid-session. Fixes #1693 --- .../src/features/editor/atoms/editor-atoms.ts | 5 +++++ .../src/features/editor/full-editor.tsx | 19 ++++++++++++++-- .../src/features/editor/page-editor.tsx | 22 +++++-------------- .../src/features/editor/title-editor.tsx | 21 +++++------------- .../components/header/page-header-menu.tsx | 4 ++-- .../user/components/page-state-pref.tsx | 22 +++++++++++++++++++ 6 files changed, 57 insertions(+), 36 deletions(-) diff --git a/apps/client/src/features/editor/atoms/editor-atoms.ts b/apps/client/src/features/editor/atoms/editor-atoms.ts index 8982765e4..c0873adfa 100644 --- a/apps/client/src/features/editor/atoms/editor-atoms.ts +++ b/apps/client/src/features/editor/atoms/editor-atoms.ts @@ -1,5 +1,6 @@ import { atom } from "jotai"; import { Editor } from "@tiptap/core"; +import { PageEditMode } from "@/features/user/types/user.types.ts"; export const pageEditorAtom = atom(null); @@ -12,3 +13,7 @@ export const yjsConnectionStatusAtom = atom(""); export const showAiMenuAtom = atom(false); export const showLinkMenuAtom = atom(false); + +// Current page's edit mode — initialized from the user's saved preference on +// first load, can be toggled locally without persisting to the server. +export const currentPageEditModeAtom = atom(PageEditMode.Edit); diff --git a/apps/client/src/features/editor/full-editor.tsx b/apps/client/src/features/editor/full-editor.tsx index 69bf2628f..57595b288 100644 --- a/apps/client/src/features/editor/full-editor.tsx +++ b/apps/client/src/features/editor/full-editor.tsx @@ -1,5 +1,5 @@ import classes from "@/features/editor/styles/editor.module.css"; -import React from "react"; +import React, { useEffect } from "react"; import { TitleEditor } from "@/features/editor/title-editor"; import PageEditor from "@/features/editor/page-editor"; import { @@ -24,6 +24,7 @@ import { FixedToolbar } from "@/features/editor/components/fixed-toolbar/fixed-t import { PageEditMode } from "@/features/user/types/user.types.ts"; import useToggleAside from "@/hooks/use-toggle-aside.tsx"; import clsx from "clsx"; +import { currentPageEditModeAtom } from "@/features/editor/atoms/editor-atoms.ts"; const MemoizedTitleEditor = React.memo(TitleEditor); const MemoizedPageEditor = React.memo(PageEditor); @@ -34,6 +35,10 @@ type PageCreator = { avatarUrl: string; }; +// Module-level flag: survives component unmount/remount on page navigation, +// reset only on full page reload (i.e. a new app session). +let defaultEditModeApplied = false; + export interface FullEditorProps { pageId: string; slugId: string; @@ -61,9 +66,19 @@ export function FullEditor({ const fullPageWidth = user.settings?.preferences?.fullPageWidth; const editorToolbarEnabled = user.settings?.preferences?.editorToolbar ?? false; + const [currentPageEditMode, setCurrentPageEditMode] = useAtom(currentPageEditModeAtom); const userPageEditMode = user.settings?.preferences?.pageEditMode ?? PageEditMode.Edit; - const isEditMode = userPageEditMode === PageEditMode.Edit; + const isEditMode = currentPageEditMode === PageEditMode.Edit; + + // Apply the user's saved preference only once on initial load, not on every + // page navigation — so the mode sticks across navigations within a session. + useEffect(() => { + if (!defaultEditModeApplied) { + setCurrentPageEditMode(userPageEditMode); + defaultEditModeApplied = true; + } + }, [userPageEditMode, setCurrentPageEditMode]); return ( Boolean(isComponentMounted.current && editorRef.current), [isComponentMounted], @@ -373,19 +373,9 @@ export default function PageEditor({ return () => clearTimeout(timeout); }, [yjsConnectionStatus, isSynced]); useEffect(() => { - // Only honor user default page edit mode preference and permissions - if (editor) { - if (userPageEditMode && editable) { - if (userPageEditMode === PageEditMode.Edit) { - editor.setEditable(true); - } else if (userPageEditMode === PageEditMode.Read) { - editor.setEditable(false); - } - } else { - editor.setEditable(false); - } - } - }, [userPageEditMode, editor, editable]); + if (!editor) return; + editor.setEditable(editable && currentPageEditMode === PageEditMode.Edit); + }, [currentPageEditMode, editor, editable]); const hasConnectedOnceRef = useRef(false); const [showStatic, setShowStatic] = useState(true); diff --git a/apps/client/src/features/editor/title-editor.tsx b/apps/client/src/features/editor/title-editor.tsx index e61d8c042..3ff2d7614 100644 --- a/apps/client/src/features/editor/title-editor.tsx +++ b/apps/client/src/features/editor/title-editor.tsx @@ -7,6 +7,7 @@ import { Text } from "@tiptap/extension-text"; import { Placeholder } from "@tiptap/extension-placeholder"; import { useAtomValue } from "jotai"; import { + currentPageEditModeAtom, pageEditorAtom, titleEditorAtom, } from "@/features/editor/atoms/editor-atoms"; @@ -24,7 +25,6 @@ import { useTranslation } from "react-i18next"; import EmojiCommand from "@/features/editor/extensions/emoji-command.ts"; import { UpdateEvent } from "@/features/websocket/types"; import localEmitter from "@/lib/local-emitter.ts"; -import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts"; import { PageEditMode } from "@/features/user/types/user.types.ts"; import { searchSpotlight } from "@/features/search/constants.ts"; import { platformModifierKey } from "@/lib"; @@ -52,9 +52,7 @@ export function TitleEditor({ const emit = useQueryEmit(); const navigate = useNavigate(); const [activePageId, setActivePageId] = useState(pageId); - const [currentUser] = useAtom(currentUserAtom); - const userPageEditMode = - currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit; + const currentPageEditMode = useAtomValue(currentPageEditModeAtom); const titleEditor = useEditor({ extensions: [ @@ -172,18 +170,9 @@ export function TitleEditor({ }, [pageId]); useEffect(() => { - if (titleEditor) { - if (userPageEditMode && editable) { - if (userPageEditMode === PageEditMode.Edit) { - titleEditor.setEditable(true); - } else if (userPageEditMode === PageEditMode.Read) { - titleEditor.setEditable(false); - } - } else { - titleEditor.setEditable(false); - } - } - }, [userPageEditMode, titleEditor, editable]); + if (!titleEditor) return; + titleEditor.setEditable(editable && currentPageEditMode === PageEditMode.Edit); + }, [currentPageEditMode, titleEditor, editable]); const openSearchDialog = () => { const event = new CustomEvent("openFindDialogFromEditor", {}); diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx index 6e481b7aa..aaf23d6fd 100644 --- a/apps/client/src/features/page/components/header/page-header-menu.tsx +++ b/apps/client/src/features/page/components/header/page-header-menu.tsx @@ -40,7 +40,7 @@ import { yjsConnectionStatusAtom, } from "@/features/editor/atoms/editor-atoms.ts"; import { formattedDate } from "@/lib/time.ts"; -import { PageStateSegmentedControl } from "@/features/user/components/page-state-pref.tsx"; +import { PageEditModeToggle } from "@/features/user/components/page-state-pref.tsx"; import MovePageModal from "@/features/page/components/move-page-modal.tsx"; import { useTimeAgo } from "@/hooks/use-time-ago.tsx"; import { PageShareModal } from "@/ee/page-permission"; @@ -91,7 +91,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { <> - {!readOnly && } + {!readOnly && } diff --git a/apps/client/src/features/user/components/page-state-pref.tsx b/apps/client/src/features/user/components/page-state-pref.tsx index 712f5152f..c78a2e04b 100644 --- a/apps/client/src/features/user/components/page-state-pref.tsx +++ b/apps/client/src/features/user/components/page-state-pref.tsx @@ -6,6 +6,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { PageEditMode } from "@/features/user/types/user.types.ts"; import { ResponsiveSettingsRow, ResponsiveSettingsContent, ResponsiveSettingsControl } from "@/components/ui/responsive-settings-row"; +import { currentPageEditModeAtom } from "@/features/editor/atoms/editor-atoms.ts"; export default function PageStatePref() { const { t } = useTranslation(); @@ -71,3 +72,24 @@ export function PageStateSegmentedControl({ /> ); } + +// Header variant: updates the current page's mode locally without persisting +// the preference to the server. +export function PageEditModeToggle({ size }: { size?: MantineSize }) { + const { t } = useTranslation(); + const [currentPageEditMode, setCurrentPageEditMode] = useAtom( + currentPageEditModeAtom, + ); + + return ( + setCurrentPageEditMode(v as PageEditMode)} + data={[ + { label: t("Edit"), value: PageEditMode.Edit }, + { label: t("Read"), value: PageEditMode.Read }, + ]} + /> + ); +} From 932c1ad5b73b14b9ea5a1f3ff1db0fea05a380b1 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 14 May 2026 09:41:10 -0400 Subject: [PATCH 08/12] Better trash (#2190) * Better trash I recently lost a bunch of time editing and searching for pages that were actually in the Trash. Docmost intentionally tries to not link to Trashed pages, but the url of that Trashed page and any inbound links still work. This makes it clearer when a page you are interacting with is in the Trash. - /trash - Refactored banner into `trash-banner.tsx` - Refactored "Restore" modal into `use-restore-page-modal.tsx` - Page (when isDeleted) - Add: `trash-banner.tsx` - Add breadcrumbs: `Parent / Child / Page (Deleted)` - Change: Deleted Pages are read-only - Replace "Move to Trash" with "Restore" in page menu (invokes `use-restore-page-modal`) I tried very hard to keep this simple and re-use existing translation strings wherever possible. * cleanup --------- Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com> --- .../public/locales/en-US/translation.json | 3 + .../src/components/layouts/global/aside.tsx | 22 ++- .../src/features/editor/full-editor.tsx | 12 +- .../components/header/page-header-menu.tsx | 9 ++ .../page/hooks/use-restore-page-modal.tsx | 30 ++++ .../src/features/page/queries/page-query.ts | 26 +++- .../trash/components/deleted-page-banner.tsx | 140 ++++++++++++++++++ .../page/trash/components/trash-banner.tsx | 21 +++ .../features/page/trash/components/trash.tsx | 36 +---- apps/client/src/pages/page/page.tsx | 2 +- apps/server/src/core/page/page.controller.ts | 1 + .../src/database/repos/page/page.repo.ts | 5 + 12 files changed, 265 insertions(+), 42 deletions(-) create mode 100644 apps/client/src/features/page/hooks/use-restore-page-modal.tsx create mode 100644 apps/client/src/features/page/trash/components/deleted-page-banner.tsx create mode 100644 apps/client/src/features/page/trash/components/trash-banner.tsx diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index e85f2af23..d59aa94b9 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -71,6 +71,7 @@ "Export": "Export", "Failed to create page": "Failed to create page", "Failed to delete page": "Failed to delete page", + "Failed to restore page": "Failed to restore page", "Failed to fetch recent pages": "Failed to fetch recent pages", "Failed to import pages": "Failed to import pages", "Failed to load page. An error occurred.": "Failed to load page. An error occurred.", @@ -581,6 +582,8 @@ "Move to trash": "Move to trash", "Move this page to trash?": "Move this page to trash?", "Restore page": "Restore page", + "Permanently delete": "Permanently delete", + "{{name}} moved this page to Trash {{time}}.": "{{name}} moved this page to Trash {{time}}.", "Page moved to trash": "Page moved to trash", "Page restored successfully": "Page restored successfully", "Deleted by": "Deleted by", diff --git a/apps/client/src/components/layouts/global/aside.tsx b/apps/client/src/components/layouts/global/aside.tsx index 73e6a381d..23ebe7b7c 100644 --- a/apps/client/src/components/layouts/global/aside.tsx +++ b/apps/client/src/components/layouts/global/aside.tsx @@ -1,4 +1,5 @@ -import { Box, ScrollArea, Text } from "@mantine/core"; +import { ActionIcon, Box, Group, ScrollArea, Text, Tooltip } from "@mantine/core"; +import { IconX } from "@tabler/icons-react"; import CommentListWithTabs from "@/features/comment/components/comment-list-with-tabs.tsx"; import { useAtom } from "jotai"; import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; @@ -11,9 +12,10 @@ import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel"; import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx"; export default function Aside() { - const [{ tab }] = useAtom(asideStateAtom); + const [{ tab }, setAsideState] = useAtom(asideStateAtom); const { t } = useTranslation(); const pageEditor = useAtomValue(pageEditorAtom); + const closeAside = () => setAsideState((s) => ({ ...s, isAsideOpen: false })); let title: string; let component: ReactNode; @@ -45,9 +47,19 @@ export default function Aside() { {component && ( <> {tab !== "chat" && ( - - {t(title)} - + + {t(title)} + + + + + + )} {tab === "comments" || tab === "chat" ? ( diff --git a/apps/client/src/features/editor/full-editor.tsx b/apps/client/src/features/editor/full-editor.tsx index 57595b288..23a506448 100644 --- a/apps/client/src/features/editor/full-editor.tsx +++ b/apps/client/src/features/editor/full-editor.tsx @@ -23,13 +23,16 @@ import { IContributor } from "@/features/page/types/page.types.ts"; import { FixedToolbar } from "@/features/editor/components/fixed-toolbar/fixed-toolbar"; import { PageEditMode } from "@/features/user/types/user.types.ts"; import useToggleAside from "@/hooks/use-toggle-aside.tsx"; +import { DeletedPageBanner } from "@/features/page/trash/components/deleted-page-banner.tsx"; import clsx from "clsx"; import { currentPageEditModeAtom } from "@/features/editor/atoms/editor-atoms.ts"; const MemoizedTitleEditor = React.memo(TitleEditor); const MemoizedPageEditor = React.memo(PageEditor); +const MemoizedFixedToolbar = React.memo(FixedToolbar); +const MemoizedDeletedPageBanner = React.memo(DeletedPageBanner); -type PageCreator = { +type PageUser = { id: string; name: string; avatarUrl: string; @@ -46,7 +49,7 @@ export interface FullEditorProps { content: string; spaceSlug: string; editable: boolean; - creator?: PageCreator; + creator?: PageUser; contributors?: IContributor[]; canComment?: boolean; } @@ -86,7 +89,8 @@ export function FullEditor({ size={!fullPageWidth && 900} className={classes.editor} > - {editorToolbarEnabled && editable && isEditMode && } + {editorToolbarEnabled && editable && isEditMode && } + diff --git a/apps/client/src/features/page/hooks/use-restore-page-modal.tsx b/apps/client/src/features/page/hooks/use-restore-page-modal.tsx new file mode 100644 index 000000000..f2089f37f --- /dev/null +++ b/apps/client/src/features/page/hooks/use-restore-page-modal.tsx @@ -0,0 +1,30 @@ +import { modals } from "@mantine/modals"; +import { Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; + +type UseRestoreModalProps = { + title?: string | null; + onConfirm: () => void; +}; + +export function useRestorePageModal() { + const { t } = useTranslation(); + const openRestoreModal = ({ title, onConfirm }: UseRestoreModalProps) => { + modals.openConfirmModal({ + title: t("Restore page"), + children: ( + + {t("Restore '{{title}}' and its sub-pages?", { + title: title || t("Untitled"), + })} + + ), + centered: true, + labels: { confirm: t("Restore"), cancel: t("Cancel") }, + confirmProps: { color: "blue" }, + onConfirm, + }); + }; + + return { openRestoreModal } as const; +} diff --git a/apps/client/src/features/page/queries/page-query.ts b/apps/client/src/features/page/queries/page-query.ts index 1ed704ce1..11ba7f32d 100644 --- a/apps/client/src/features/page/queries/page-query.ts +++ b/apps/client/src/features/page/queries/page-query.ts @@ -117,10 +117,20 @@ export function useUpdatePageMutation() { } export function useRemovePageMutation() { + const { t } = useTranslation(); return useMutation({ mutationFn: (pageId: string) => deletePage(pageId, false), onSuccess: (_, pageId) => { - notifications.show({ message: "Page moved to trash" }); + notifications.show({ message: t("Page moved to trash") }); + + // Stamp deletedAt so a re-visit shows the trash banner, not stale state. + const cached = queryClient.getQueryData(["pages", pageId]); + if (cached) { + const stamped = { ...cached, deletedAt: new Date() }; + queryClient.setQueryData(["pages", cached.id], stamped); + queryClient.setQueryData(["pages", cached.slugId], stamped); + } + invalidateOnDeletePage(pageId); queryClient.invalidateQueries({ predicate: (item) => @@ -128,7 +138,7 @@ export function useRemovePageMutation() { }); }, onError: (error) => { - notifications.show({ message: "Failed to delete page", color: "red" }); + notifications.show({ message: t("Failed to delete page"), color: "red" }); }, }); } @@ -162,13 +172,14 @@ export function useMovePageMutation() { } export function useRestorePageMutation() { + const { t } = useTranslation(); const [treeData, setTreeData] = useAtom(treeDataAtom); const emit = useQueryEmit(); return useMutation({ mutationFn: (pageId: string) => restorePage(pageId), onSuccess: async (restoredPage) => { - notifications.show({ message: "Page restored successfully" }); + notifications.show({ message: t("Page restored successfully") }); // Check if the page already exists in the tree (it shouldn't) if (!treeModel.find(treeData, restoredPage.id)) { @@ -222,9 +233,16 @@ export function useRestorePageMutation() { await queryClient.invalidateQueries({ queryKey: ["trash-list", restoredPage.spaceId], }); + + // Merge — restore endpoint returns a skinny page; + // Replace would strip space/permissions/content and break the editor. + const merge = (cached: IPage | undefined) => + cached ? { ...cached, ...restoredPage } : cached; + queryClient.setQueryData(["pages", restoredPage.id], merge); + queryClient.setQueryData(["pages", restoredPage.slugId], merge); }, onError: (error) => { - notifications.show({ message: "Failed to restore page", color: "red" }); + notifications.show({ message: t("Failed to restore page"), color: "red" }); }, }); } diff --git a/apps/client/src/features/page/trash/components/deleted-page-banner.tsx b/apps/client/src/features/page/trash/components/deleted-page-banner.tsx new file mode 100644 index 000000000..f01a6ab49 --- /dev/null +++ b/apps/client/src/features/page/trash/components/deleted-page-banner.tsx @@ -0,0 +1,140 @@ +import { ActionIcon, Button, Group, Paper, Text, Tooltip } from "@mantine/core"; +import { IconRestore, IconTrash } from "@tabler/icons-react"; +import { useNavigate } from "react-router-dom"; +import { Trans, useTranslation } from "react-i18next"; +import { useTimeAgo } from "@/hooks/use-time-ago.tsx"; +import { useRestorePageModal } from "@/features/page/hooks/use-restore-page-modal.tsx"; +import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx"; +import { + useDeletePageMutation, + usePageQuery, + useRestorePageMutation, +} from "@/features/page/queries/page-query.ts"; +import { getSpaceUrl } from "@/lib/config.ts"; +import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts"; +import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts"; +import { + SpaceCaslAction, + SpaceCaslSubject, +} from "@/features/space/permissions/permissions.type.ts"; + +type DeletedPageBannerProps = { + slugId: string; +}; + +export function DeletedPageBanner({ slugId }: DeletedPageBannerProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { data: page } = usePageQuery({ pageId: slugId }); + const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug); + const spaceAbility = useSpaceAbility(space?.membership?.permissions); + const deletedTimeAgo = useTimeAgo(page?.deletedAt); + const restorePageMutation = useRestorePageMutation(); + const deletePageMutation = useDeletePageMutation(); + const { openRestoreModal } = useRestorePageModal(); + const { openDeleteModal } = useDeletePageModal(); + + if (!page?.deletedAt) return null; + + const canRestore = spaceAbility.can( + SpaceCaslAction.Edit, + SpaceCaslSubject.Page, + ); + const canPermanentlyDelete = spaceAbility.can( + SpaceCaslAction.Manage, + SpaceCaslSubject.Settings, + ); + const actorName = page.deletedBy?.name ?? t("Someone"); + + const handleRestore = () => { + openRestoreModal({ + title: page.title, + onConfirm: () => restorePageMutation.mutate(page.id), + }); + }; + + const handlePermanentDelete = () => { + openDeleteModal({ + isPermanent: true, + onConfirm: async () => { + await deletePageMutation.mutateAsync(page.id); + navigate(getSpaceUrl(page.space?.slug)); + }, + }); + }; + + const hasAnyAction = canRestore || canPermanentlyDelete; + + return ( + + + + }} + /> + + {hasAnyAction && ( + <> + + {canRestore && ( + + )} + {canPermanentlyDelete && ( + + )} + + + {canRestore && ( + + + + + + )} + {canPermanentlyDelete && ( + + + + + + )} + + + )} + + + ); +} diff --git a/apps/client/src/features/page/trash/components/trash-banner.tsx b/apps/client/src/features/page/trash/components/trash-banner.tsx new file mode 100644 index 000000000..90ec4a066 --- /dev/null +++ b/apps/client/src/features/page/trash/components/trash-banner.tsx @@ -0,0 +1,21 @@ +import { Alert, Text } from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; +import { useAtomValue } from "jotai"; +import { useTranslation } from "react-i18next"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; + +export function TrashBanner() { + const { t } = useTranslation(); + const workspace = useAtomValue(workspaceAtom); + const retentionDays = workspace?.trashRetentionDays ?? 30; + + return ( + } variant="light" color="red"> + + {t("Pages in trash will be permanently deleted after {{count}} days.", { + count: retentionDays, + })} + + + ); +} diff --git a/apps/client/src/features/page/trash/components/trash.tsx b/apps/client/src/features/page/trash/components/trash.tsx index de7e7ca4a..da33d828f 100644 --- a/apps/client/src/features/page/trash/components/trash.tsx +++ b/apps/client/src/features/page/trash/components/trash.tsx @@ -7,17 +7,16 @@ import { Group, ActionIcon, Text, - Alert, Stack, Menu, } from "@mantine/core"; import { - IconInfoCircle, IconDots, IconRestore, IconTrash, IconFileDescription, } from "@tabler/icons-react"; +import { TrashBanner } from "@/features/page/trash/components/trash-banner.tsx"; import { useDeletedPagesQuery, useRestorePageMutation, @@ -31,12 +30,10 @@ import TrashPageContentModal from "@/features/page/trash/components/trash-page-c import { UserInfo } from "@/components/common/user-info.tsx"; import Paginate from "@/components/common/paginate.tsx"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; -import { useAtom } from "jotai"; -import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useRestorePageModal } from "@/features/page/hooks/use-restore-page-modal.tsx"; export default function Trash() { const { t } = useTranslation(); - const [workspace] = useAtom(workspaceAtom); const { spaceSlug } = useParams(); const { cursor, goNext, goPrev } = useCursorPaginate(); const { data: space } = useGetSpaceBySlugQuery(spaceSlug); @@ -45,6 +42,7 @@ export default function Trash() { }); const restorePageMutation = useRestorePageMutation(); const deletePageMutation = useDeletePageMutation(); + const { openRestoreModal } = useRestorePageModal(); const [selectedPage, setSelectedPage] = useState<{ title: string; @@ -78,23 +76,6 @@ export default function Trash() { }); }; - const openRestoreModal = (pageId: string, pageTitle: string) => { - modals.openConfirmModal({ - title: t("Restore page"), - children: ( - - {t("Restore '{{title}}' and its sub-pages?", { - title: pageTitle || "Untitled", - })} - - ), - centered: true, - labels: { confirm: t("Restore"), cancel: t("Cancel") }, - confirmProps: { color: "blue" }, - onConfirm: () => handleRestorePage(pageId), - }); - }; - const hasPages = deletedPages && deletedPages.items.length > 0; const handlePageClick = (page: any) => { @@ -109,11 +90,7 @@ export default function Trash() { {t("Trash")} - } variant="light" color="red"> - - {t("Pages in trash will be permanently deleted after {{count}} days.", { count: workspace?.trashRetentionDays ?? 30 })} - - + {isLoading || !deletedPages ? ( <> @@ -181,7 +158,10 @@ export default function Trash() { } onClick={() => - openRestoreModal(page.id, page.title) + openRestoreModal({ + title: page.title, + onConfirm: () => handleRestorePage(page.id), + }) } > {t("Restore")} diff --git a/apps/client/src/pages/page/page.tsx b/apps/client/src/pages/page/page.tsx index b4e3baa80..1eecd2512 100644 --- a/apps/client/src/pages/page/page.tsx +++ b/apps/client/src/pages/page/page.tsx @@ -52,7 +52,7 @@ function PageContent({ pageSlug }: { pageSlug: string | undefined }) { } = usePageQuery({ pageId: extractPageSlugId(pageSlug) }); const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug); - const canEdit = page?.permissions?.canEdit ?? false; + const canEdit = !page?.deletedAt && (page?.permissions?.canEdit ?? false); const canComment = canEdit || (space?.settings?.comments?.allowViewerComments === true); diff --git a/apps/server/src/core/page/page.controller.ts b/apps/server/src/core/page/page.controller.ts index 5ce077508..773774ea7 100644 --- a/apps/server/src/core/page/page.controller.ts +++ b/apps/server/src/core/page/page.controller.ts @@ -76,6 +76,7 @@ export class PageController { includeCreator: true, includeLastUpdatedBy: true, includeContributors: true, + includeDeletedBy: true, }); if (!page) { diff --git a/apps/server/src/database/repos/page/page.repo.ts b/apps/server/src/database/repos/page/page.repo.ts index 6b17e37f9..259e1bd33 100644 --- a/apps/server/src/database/repos/page/page.repo.ts +++ b/apps/server/src/database/repos/page/page.repo.ts @@ -54,6 +54,7 @@ export class PageRepo { includeCreator?: boolean; includeLastUpdatedBy?: boolean; includeContributors?: boolean; + includeDeletedBy?: boolean; includeHasChildren?: boolean; withLock?: boolean; trx?: KyselyTransaction; @@ -83,6 +84,10 @@ export class PageRepo { query = query.select((eb) => this.withContributors(eb)); } + if (opts?.includeDeletedBy) { + query = query.select((eb) => this.withDeletedBy(eb)); + } + if (opts?.includeSpace) { query = query.select((eb) => this.withSpace(eb)); } From e41518a93db536fd53bcf678a679dd6c983bc862 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 14 May 2026 14:49:02 +0100 Subject: [PATCH 09/12] fix type --- apps/client/src/features/editor/full-editor.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/client/src/features/editor/full-editor.tsx b/apps/client/src/features/editor/full-editor.tsx index 23a506448..412a3b3de 100644 --- a/apps/client/src/features/editor/full-editor.tsx +++ b/apps/client/src/features/editor/full-editor.tsx @@ -69,7 +69,9 @@ export function FullEditor({ const fullPageWidth = user.settings?.preferences?.fullPageWidth; const editorToolbarEnabled = user.settings?.preferences?.editorToolbar ?? false; - const [currentPageEditMode, setCurrentPageEditMode] = useAtom(currentPageEditModeAtom); + const [currentPageEditMode, setCurrentPageEditMode] = useAtom( + currentPageEditModeAtom, + ); const userPageEditMode = user.settings?.preferences?.pageEditMode ?? PageEditMode.Edit; const isEditMode = currentPageEditMode === PageEditMode.Edit; @@ -78,7 +80,7 @@ export function FullEditor({ // page navigation — so the mode sticks across navigations within a session. useEffect(() => { if (!defaultEditModeApplied) { - setCurrentPageEditMode(userPageEditMode); + setCurrentPageEditMode(userPageEditMode as PageEditMode); defaultEditModeApplied = true; } }, [userPageEditMode, setCurrentPageEditMode]); @@ -89,7 +91,9 @@ export function FullEditor({ size={!fullPageWidth && 900} className={classes.editor} > - {editorToolbarEnabled && editable && isEditMode && } + {editorToolbarEnabled && editable && isEditMode && ( + + )} Date: Thu, 14 May 2026 15:06:51 +0100 Subject: [PATCH 10/12] fix collab module --- .../src/collaboration/server/collab-app.module.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/server/src/collaboration/server/collab-app.module.ts b/apps/server/src/collaboration/server/collab-app.module.ts index f0e27d41a..85738d1cb 100644 --- a/apps/server/src/collaboration/server/collab-app.module.ts +++ b/apps/server/src/collaboration/server/collab-app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { AppController } from '../../app.controller'; import { AppService } from '../../app.service'; import { EnvironmentModule } from '../../integrations/environment/environment.module'; +import { EnvironmentService } from '../../integrations/environment/environment.service'; import { CollaborationModule } from '../collaboration.module'; import { DatabaseModule } from '@docmost/db/database.module'; import { QueueModule } from '../../integrations/queue/queue.module'; @@ -12,6 +13,8 @@ import { LoggerModule } from '../../common/logger/logger.module'; import { RedisModule } from '@nestjs-labs/nestjs-ioredis'; import { RedisConfigService } from '../../integrations/redis/redis-config.service'; import { CaslModule } from '../../core/casl/casl.module'; +import { CacheModule } from '@nestjs/cache-manager'; +import KeyvRedis from '@keyv/redis'; @Module({ imports: [ @@ -26,6 +29,18 @@ import { CaslModule } from '../../core/casl/casl.module'; RedisModule.forRootAsync({ useClass: RedisConfigService, }), + CacheModule.registerAsync({ + isGlobal: true, + useFactory: async (environmentService: EnvironmentService) => { + const redisUrl = environmentService.getRedisUrl(); + + return { + ttl: 5 * 1000, + stores: [new KeyvRedis(redisUrl)], + }; + }, + inject: [EnvironmentService], + }), ], controllers: [ AppController, From b7b99cb3b229196f7f00aaa02fe7a043c0a41134 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Fri, 15 May 2026 02:46:54 +0100 Subject: [PATCH 11/12] fix: code splitting and editor fixes (#2211) * fix table * fix code splitting * fix: editor ready check * fix codeblock/mermaid gap cursor * fix callout --- .../editor/components/audio/audio-menu.tsx | 3 +- .../components/callout/callout-menu.tsx | 4 +- .../components/columns/columns-menu.tsx | 6 +- .../editor/components/drawio/drawio-menu.tsx | 3 +- .../excalidraw/excalidraw-menu-lazy.tsx | 14 ++ .../components/excalidraw/excalidraw-menu.tsx | 3 +- .../excalidraw/excalidraw-view-lazy.tsx | 14 ++ .../editor/components/image/image-menu.tsx | 3 +- .../editor/components/pdf/pdf-menu.tsx | 8 +- .../components/subpages/subpages-menu.tsx | 2 + .../components/table/handle/column-handle.tsx | 7 +- .../components/table/handle/row-handle.tsx | 7 +- .../editor/components/table/table-menu.tsx | 3 +- .../editor/components/video/video-menu.tsx | 3 +- .../features/editor/extensions/extensions.ts | 2 +- .../src/features/editor/page-editor.tsx | 2 +- apps/client/vite.config.ts | 10 +- .../editor-ext/src/lib/callout/callout.ts | 72 +++++++++ .../custom-code-block/custom-code-block.ts | 139 ++++++++++++++++++ packages/editor-ext/src/lib/utils.ts | 9 ++ 20 files changed, 290 insertions(+), 24 deletions(-) create mode 100644 apps/client/src/features/editor/components/excalidraw/excalidraw-menu-lazy.tsx create mode 100644 apps/client/src/features/editor/components/excalidraw/excalidraw-view-lazy.tsx diff --git a/apps/client/src/features/editor/components/audio/audio-menu.tsx b/apps/client/src/features/editor/components/audio/audio-menu.tsx index 3ca1950da..eadc1afe5 100644 --- a/apps/client/src/features/editor/components/audio/audio-menu.tsx +++ b/apps/client/src/features/editor/components/audio/audio-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -46,7 +47,7 @@ export function AudioMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "audio"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/callout/callout-menu.tsx b/apps/client/src/features/editor/components/callout/callout-menu.tsx index 69c836934..3ce022dae 100644 --- a/apps/client/src/features/editor/components/callout/callout-menu.tsx +++ b/apps/client/src/features/editor/components/callout/callout-menu.tsx @@ -16,7 +16,7 @@ import { IconMoodSmile, IconNotes, } from "@tabler/icons-react"; -import { CalloutType, isTextSelected } from "@docmost/editor-ext"; +import { CalloutType, isEditorReady, isTextSelected } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import EmojiPicker from "@/components/ui/emoji-picker.tsx"; import classes from "../common/toolbar-menu.module.css"; @@ -55,7 +55,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { }); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "callout"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/columns/columns-menu.tsx b/apps/client/src/features/editor/components/columns/columns-menu.tsx index 0ee99508c..4a1f041eb 100644 --- a/apps/client/src/features/editor/components/columns/columns-menu.tsx +++ b/apps/client/src/features/editor/components/columns/columns-menu.tsx @@ -19,7 +19,7 @@ import { IconCopy, IconTrash, } from "@tabler/icons-react"; -import { isTextSelected } from "@docmost/editor-ext"; +import { isEditorReady, isTextSelected } from "@docmost/editor-ext"; import type { WidthMode, ColumnsLayout } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import classes from "../common/toolbar-menu.module.css"; @@ -82,7 +82,7 @@ export function ColumnsMenu({ editor }: EditorMenuProps) { const shouldShow = useCallback( ({ state }: ShouldShowProps) => { - if (!state) return false; + if (!state || !isEditorReady(editor)) return false; if (!editor.isActive("columns")) return false; if (isTextSelected(editor)) return false; if (nodesWithMenus.some((name) => editor.isActive(name))) return false; @@ -121,7 +121,7 @@ export function ColumnsMenu({ editor }: EditorMenuProps) { }); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "columns"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx index 869decd71..877911750 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -81,7 +82,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "drawio"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu-lazy.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu-lazy.tsx new file mode 100644 index 000000000..acdf5440d --- /dev/null +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu-lazy.tsx @@ -0,0 +1,14 @@ +import { lazy, Suspense } from "react"; +import { EditorMenuProps } from "@/features/editor/components/table/types/types.ts"; + +const ExcalidrawMenu = lazy( + () => import("@/features/editor/components/excalidraw/excalidraw-menu.tsx"), +); + +export default function ExcalidrawMenuLazy(props: EditorMenuProps) { + return ( + + + + ); +} diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx index fd3128062..823c2c213 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -94,7 +95,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "excalidraw"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-view-lazy.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-view-lazy.tsx new file mode 100644 index 000000000..573a25dbd --- /dev/null +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-view-lazy.tsx @@ -0,0 +1,14 @@ +import { lazy, Suspense } from "react"; +import { NodeViewProps } from "@tiptap/react"; + +const ExcalidrawView = lazy( + () => import("@/features/editor/components/excalidraw/excalidraw-view.tsx"), +); + +export default function ExcalidrawViewLazy(props: NodeViewProps) { + return ( + + + + ); +} diff --git a/apps/client/src/features/editor/components/image/image-menu.tsx b/apps/client/src/features/editor/components/image/image-menu.tsx index 666fab7dc..1b2d00e7e 100644 --- a/apps/client/src/features/editor/components/image/image-menu.tsx +++ b/apps/client/src/features/editor/components/image/image-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import React, { useCallback, useRef } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -56,7 +57,7 @@ export function ImageMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "image"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/pdf/pdf-menu.tsx b/apps/client/src/features/editor/components/pdf/pdf-menu.tsx index 2104bfbc6..3fc8b6fd1 100644 --- a/apps/client/src/features/editor/components/pdf/pdf-menu.tsx +++ b/apps/client/src/features/editor/components/pdf/pdf-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -37,9 +38,8 @@ export function PdfMenu({ editor }: EditorMenuProps) { const shouldShow = useCallback( ({ state }: ShouldShowProps) => { - if (!state || !editor.isActive("pdf")) { - return false; - } + if (!state || !isEditorReady(editor)) return false; + if (!editor.isActive("pdf")) return false; const { selection } = state; const dom = editor.view.nodeDOM(selection.from) as HTMLElement | null; @@ -51,7 +51,7 @@ export function PdfMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "pdf"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx index 9f0544e67..a626e1ee2 100644 --- a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx +++ b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx @@ -6,6 +6,7 @@ import { ActionIcon, Tooltip } from "@mantine/core"; import { IconTrash } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { Editor } from "@tiptap/core"; +import { isEditorReady } from "@docmost/editor-ext"; interface SubpagesMenuProps { editor: Editor; @@ -33,6 +34,7 @@ export const SubpagesMenu = React.memo( ); const getReferenceClientRect = useCallback(() => { + if (!isEditorReady(editor)) return new DOMRect(); const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "subpages"; const parent = findParentNode(predicate)(selection); 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 index ccc459740..a46ac50d5 100644 --- a/apps/client/src/features/editor/components/table/handle/column-handle.tsx +++ b/apps/client/src/features/editor/components/table/handle/column-handle.tsx @@ -31,7 +31,12 @@ export const ColumnHandle = React.memo(function ColumnHandle({ // (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; + // `nodeDOM` is typed as `Node | null` — when `anchorPos` goes stale (e.g. + // an external drop reflows the doc before the plugin re-emits + // hoveringCell), it can resolve to a Text node, on which `.closest` is + // undefined. Filter to HTMLElement so downstream consumers stay safe. + const lookupDom = editor.view.nodeDOM(anchorPos); + const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null; const [cellDom, setCellDom] = useState(lookupCellDom); const lastCellDomRef = useRef(lookupCellDom); useEffect(() => { 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 index 7a5483558..1f3e3cc51 100644 --- a/apps/client/src/features/editor/components/table/handle/row-handle.tsx +++ b/apps/client/src/features/editor/components/table/handle/row-handle.tsx @@ -29,7 +29,12 @@ export const RowHandle = React.memo(function RowHandle({ // 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; + // `nodeDOM` is typed as `Node | null` — when `anchorPos` goes stale (e.g. + // an external drop reflows the doc before the plugin re-emits + // hoveringCell), it can resolve to a Text node, on which `.closest` is + // undefined. Filter to HTMLElement so downstream consumers stay safe. + const lookupDom = editor.view.nodeDOM(anchorPos); + const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null; const [cellDom, setCellDom] = useState(lookupCellDom); const lastCellDomRef = useRef(lookupCellDom); useEffect(() => { 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 3be7ec539..92cc318e9 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -18,7 +18,7 @@ import { IconTrashX, } from "@tabler/icons-react"; import { BubbleMenu } from "@tiptap/react/menus"; -import { isCellSelection, isTextSelected } from "@docmost/editor-ext"; +import { isCellSelection, isEditorReady, isTextSelected } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import classes from "../common/toolbar-menu.module.css"; @@ -38,6 +38,7 @@ export const TableMenu = React.memo( ); const getReferencedVirtualElement = useCallback(() => { + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "table"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/video/video-menu.tsx b/apps/client/src/features/editor/components/video/video-menu.tsx index 3f232625f..429e02f87 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -53,7 +54,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "video"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 91411daef..9857b0551 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -85,7 +85,7 @@ import AudioView from "@/features/editor/components/audio/audio-view.tsx"; import AttachmentView from "@/features/editor/components/attachment/attachment-view.tsx"; import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx"; import DrawioView from "../components/drawio/drawio-view"; -import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx"; +import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view-lazy.tsx"; import EmbedView from "@/features/editor/components/embed/embed-view.tsx"; import PdfView from "@/features/editor/components/pdf/pdf-view.tsx"; import SubpagesView from "@/features/editor/components/subpages/subpages-view.tsx"; diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index 9d53eec31..8521356ec 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -55,7 +55,7 @@ import { handleFileDrop, handlePaste, } from "@/features/editor/components/common/editor-paste-handler.tsx"; -import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu"; +import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu-lazy"; import DrawioMenu from "./components/drawio/drawio-menu"; import { useCollabToken } from "@/features/auth/queries/auth-query.tsx"; import SearchAndReplaceDialog from "@/features/editor/components/search-and-replace/search-and-replace-dialog.tsx"; diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts index e6f9de48c..b230a29ff 100644 --- a/apps/client/vite.config.ts +++ b/apps/client/vite.config.ts @@ -38,12 +38,12 @@ export default defineConfig(({ mode }) => { build: { rolldownOptions: { output: { - codeSplitting: { + advancedChunks: { groups: [ - { name: "vendor-mantine", test: /@mantine/ }, - { name: "vendor-mermaid", test: /mermaid|cytoscape|elkjs/ }, - { name: "vendor-excalidraw", test: /excalidraw/ }, - { name: "vendor-katex", test: /katex/ }, + { + name: "vendor-mantine", + test: /[\\/]node_modules[\\/]@mantine[\\/]/, + }, ], }, }, diff --git a/packages/editor-ext/src/lib/callout/callout.ts b/packages/editor-ext/src/lib/callout/callout.ts index 898fc4152..a07a4a96c 100644 --- a/packages/editor-ext/src/lib/callout/callout.ts +++ b/packages/editor-ext/src/lib/callout/callout.ts @@ -162,6 +162,28 @@ export const Callout = Node.create({ return false; } + // Empty callout: delete the whole node so Backspace inside it isn't + // a no-op (isolating: true blocks the default join with the block + // above). + const calloutDepth = $from.depth - 1; + if (calloutDepth >= 0) { + const calloutNode = $from.node(calloutDepth); + if ( + calloutNode.type === this.type && + calloutNode.childCount === 1 && + calloutNode.firstChild?.content.size === 0 + ) { + const calloutPos = $from.before(calloutDepth); + const { tr } = state; + tr.delete(calloutPos, calloutPos + calloutNode.nodeSize); + tr.setSelection( + TextSelection.near(tr.doc.resolve(calloutPos), -1), + ); + view.dispatch(tr); + return true; + } + } + const previousPosition = $from.before($from.depth) - 1; // If nothing above to join with @@ -207,6 +229,56 @@ export const Callout = Node.create({ } return false; }, + + // Exit the callout into a fresh paragraph below when the cursor sits + // in an empty trailing child. An empty callout (single empty + // paragraph) exits on the first Enter and keeps the empty callout + // intact; a callout with content needs the double-Enter pattern + // (first Enter splits, second Enter on the new trailing empty exits + // and removes that trailing paragraph). + Enter: ({ editor }) => { + const { state, view } = editor; + const { selection } = state; + if (!selection.empty) return false; + + const { $from } = selection; + const calloutDepth = $from.depth - 1; + if (calloutDepth < 0) return false; + + const calloutNode = $from.node(calloutDepth); + if (calloutNode.type !== this.type) return false; + if ($from.parent.content.size !== 0) return false; + if ($from.index(calloutDepth) !== calloutNode.childCount - 1) { + return false; + } + + const paragraphType = state.schema.nodes.paragraph; + const containerDepth = calloutDepth - 1; + const container = $from.node(containerDepth); + const indexAfter = $from.indexAfter(containerDepth); + if ( + !container.canReplaceWith(indexAfter, indexAfter, paragraphType) + ) { + return false; + } + + const calloutEnd = $from.after(calloutDepth); + const paragraph = paragraphType.create(); + const { tr } = state; + + if (calloutNode.childCount === 1) { + tr.insert(calloutEnd, paragraph); + tr.setSelection(TextSelection.create(tr.doc, calloutEnd + 1)); + } else { + tr.delete($from.before(), $from.after()); + const insertPos = tr.mapping.map(calloutEnd); + tr.insert(insertPos, paragraph); + tr.setSelection(TextSelection.create(tr.doc, insertPos + 1)); + } + + view.dispatch(tr); + return true; + }, }; }, diff --git a/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts b/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts index 4c4b6ef77..5d67188a3 100644 --- a/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts +++ b/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts @@ -1,5 +1,7 @@ import type { CodeBlockOptions } from '@tiptap/extension-code-block'; import CodeBlock from '@tiptap/extension-code-block'; +import { Plugin, Selection, TextSelection } from '@tiptap/pm/state'; +import { GapCursor } from '@tiptap/pm/gapcursor'; import { LowlightPlugin } from './lowlight-plugin.js'; import { ReactNodeViewRenderer } from '@tiptap/react'; @@ -19,7 +21,11 @@ const TAB_CHAR = '\u00A0\u00A0'; * @see https://tiptap.dev/api/nodes/code-block-lowlight */ export const CustomCodeBlock = CodeBlock.extend({ + // Run ahead of Gapcursor (100) so the mermaid arrow-into-source plugin + // can intercept before gapcursor takes over. + priority: 101, selectable: true, + isolating: true, addOptions() { return { @@ -35,8 +41,86 @@ export const CustomCodeBlock = CodeBlock.extend({ }, addKeyboardShortcuts() { + const isMermaid = (node: any) => + node?.type === this.type && node.attrs.language === 'mermaid'; + return { ...this.parent?.(), + // Stop at the gap (or enter mermaid source) instead of jumping + // straight into the next block, so the user can place a cursor + // between two adjacent isolating blocks. + ArrowDown: ({ editor }) => { + const { state } = editor; + const { selection, doc } = state; + const { $from, empty } = selection; + + if (!empty || $from.parent.type !== this.type) return false; + if ($from.parentOffset !== $from.parent.nodeSize - 2) return false; + + const after = $from.after(); + if (after >= doc.content.size) { + return editor.commands.exitCode(); + } + + const $after = doc.resolve(after); + const nodeAfter = $after.nodeAfter; + + if (isMermaid(nodeAfter)) { + return editor.commands.command(({ tr }) => { + tr.setSelection(TextSelection.create(tr.doc, after + 1)); + return true; + }); + } + + if ( + nodeAfter?.type.spec.isolating && + !nodeAfter.type.spec.atom + ) { + return editor.commands.command(({ tr }) => { + tr.setSelection(new GapCursor(tr.doc.resolve(after))); + return true; + }); + } + + return editor.commands.command(({ tr }) => { + tr.setSelection(Selection.near(tr.doc.resolve(after))); + return true; + }); + }, + // Mirror of ArrowDown; upstream has no ArrowUp handler. + ArrowUp: ({ editor }) => { + const { state } = editor; + const { selection, doc } = state; + const { $from, empty } = selection; + + if (!empty || $from.parent.type !== this.type) return false; + if ($from.parentOffset !== 0) return false; + + const before = $from.before(); + if (before <= 0) return false; + + const $before = doc.resolve(before); + const nodeBefore = $before.nodeBefore; + + if (isMermaid(nodeBefore)) { + return editor.commands.command(({ tr }) => { + tr.setSelection(TextSelection.create(tr.doc, before - 1)); + return true; + }); + } + + if ( + nodeBefore?.type.spec.isolating && + !nodeBefore.type.spec.atom + ) { + return editor.commands.command(({ tr }) => { + tr.setSelection(new GapCursor(tr.doc.resolve(before))); + return true; + }); + } + + return false; + }, 'Mod-a': () => { if (this.editor.isActive('codeBlock')) { const { state } = this.editor; @@ -84,6 +168,7 @@ export const CustomCodeBlock = CodeBlock.extend({ }, addProseMirrorPlugins() { + const codeBlockType = this.type; return [ ...(this.parent?.() || []), LowlightPlugin({ @@ -91,6 +176,60 @@ export const CustomCodeBlock = CodeBlock.extend({ lowlight: this.options.lowlight, defaultLanguage: this.options.defaultLanguage, }), + // Mermaid hides its
     when unselected, so the browser's native
    +      // vertical caret movement skips past it. Land the cursor inside the
    +      // source explicitly.
    +      new Plugin({
    +        props: {
    +          handleKeyDown: (view, event) => {
    +            if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
    +              return false;
    +            }
    +            const { state } = view;
    +            const { selection } = state;
    +            if (
    +              !selection.empty ||
    +              !(selection instanceof TextSelection)
    +            ) {
    +              return false;
    +            }
    +            const { $from } = selection;
    +            if ($from.depth === 0 || $from.parent.type === codeBlockType) {
    +              return false;
    +            }
    +            const dir = event.key === 'ArrowUp' ? 'up' : 'down';
    +            if (!view.endOfTextblock(dir)) return false;
    +
    +            const isMermaid = (node: any) =>
    +              node?.type === codeBlockType && node.attrs.language === 'mermaid';
    +
    +            if (event.key === 'ArrowUp') {
    +              if ($from.parentOffset !== 0) return false;
    +              const beforePos = $from.before();
    +              const prev = state.doc.resolve(beforePos).nodeBefore;
    +              if (!isMermaid(prev)) return false;
    +              const endPos = beforePos - 1;
    +              view.dispatch(
    +                state.tr.setSelection(
    +                  TextSelection.create(state.doc, endPos),
    +                ),
    +              );
    +              return true;
    +            }
    +            if ($from.parentOffset !== $from.parent.nodeSize - 2) return false;
    +            const afterPos = $from.after();
    +            const next = state.doc.resolve(afterPos).nodeAfter;
    +            if (!isMermaid(next)) return false;
    +            const startPos = afterPos + 1;
    +            view.dispatch(
    +              state.tr.setSelection(
    +                TextSelection.create(state.doc, startPos),
    +              ),
    +            );
    +            return true;
    +          },
    +        },
    +      }),
         ];
       },
     });
    diff --git a/packages/editor-ext/src/lib/utils.ts b/packages/editor-ext/src/lib/utils.ts
    index 3bfd01778..8d03577c7 100644
    --- a/packages/editor-ext/src/lib/utils.ts
    +++ b/packages/editor-ext/src/lib/utils.ts
    @@ -338,6 +338,15 @@ export const isRowGripSelected = ({
       return !!gripRow;
     };
     
    +// TipTap's `editor.view` proxy throws if accessed before mount or after destroy.
    +// Guard floating-menu callbacks (getReferencedVirtualElement, shouldShow) with
    +// this before touching `editor.view.nodeDOM(...)`.
    +export function isEditorReady(
    +  editor: Editor | null | undefined,
    +): editor is Editor {
    +  return !!editor && editor.isInitialized;
    +}
    +
     export function isTextSelected(editor: Editor) {
       const {
         state: {
    
    From 0d6538ab1a38fe395827b737570130486f9e14c7 Mon Sep 17 00:00:00 2001
    From: Philipinho <16838612+Philipinho@users.noreply.github.com>
    Date: Mon, 18 May 2026 22:02:31 +0100
    Subject: [PATCH 12/12] feat: iframe configuration
    
    ---
     .env.example                                  |  7 ++++++
     apps/server/src/common/helpers/index.ts       |  1 +
     .../src/common/helpers/security-headers.ts    | 19 +++++++++++++++
     .../environment/environment.service.ts        | 15 ++++++++++++
     apps/server/src/main.ts                       | 24 +++++++++++++++++++
     5 files changed, 66 insertions(+)
     create mode 100644 apps/server/src/common/helpers/security-headers.ts
    
    diff --git a/.env.example b/.env.example
    index b218bdb84..cf2dafc1d 100644
    --- a/.env.example
    +++ b/.env.example
    @@ -48,6 +48,13 @@ GOTENBERG_URL=
     
     DISABLE_TELEMETRY=false
     
    +# Allow other sites to embed Docmost in an iframe.
    +IFRAME_EMBED_ALLOWED=false
    +
    +# Only used when IFRAME_EMBED_ALLOWED=true. When empty, any origin is allowed.
    +# Example: https://intranet.example.com,https://portal.example.com
    +IFRAME_ALLOWED_ORIGINS=
    +
     # Enable debug logging in production (default: false)
     DEBUG_MODE=false
     
    diff --git a/apps/server/src/common/helpers/index.ts b/apps/server/src/common/helpers/index.ts
    index 13e44d526..80e9a9028 100644
    --- a/apps/server/src/common/helpers/index.ts
    +++ b/apps/server/src/common/helpers/index.ts
    @@ -2,3 +2,4 @@ export * from './utils';
     export * from './nanoid.utils';
     export * from './file.helper';
     export * from './constants';
    +export * from './security-headers';
    diff --git a/apps/server/src/common/helpers/security-headers.ts b/apps/server/src/common/helpers/security-headers.ts
    new file mode 100644
    index 000000000..931300e2d
    --- /dev/null
    +++ b/apps/server/src/common/helpers/security-headers.ts
    @@ -0,0 +1,19 @@
    +export type SecurityHeader = { name: string; value: string };
    +
    +export function resolveFrameHeader(
    +  iframeEmbedAllowed: boolean,
    +  allowedOrigins: string[],
    +): SecurityHeader | null {
    +  if (!iframeEmbedAllowed) {
    +    return { name: 'X-Frame-Options', value: 'SAMEORIGIN' };
    +  }
    +
    +  if (allowedOrigins.length === 0) {
    +    return null;
    +  }
    +
    +  return {
    +    name: 'Content-Security-Policy',
    +    value: `frame-ancestors 'self' ${allowedOrigins.join(' ')}`,
    +  };
    +}
    diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts
    index abee1966f..20824d355 100644
    --- a/apps/server/src/integrations/environment/environment.service.ts
    +++ b/apps/server/src/integrations/environment/environment.service.ts
    @@ -325,4 +325,19 @@ export class EnvironmentService {
           .toLowerCase();
         return disabled === 'true';
       }
    +
    +  isIframeEmbedAllowed(): boolean {
    +    const allowed = this.configService
    +      .get('IFRAME_EMBED_ALLOWED', 'false')
    +      .toLowerCase();
    +    return allowed === 'true';
    +  }
    +
    +  getIframeAllowedOrigins(): string[] {
    +    const raw = this.configService.get('IFRAME_ALLOWED_ORIGINS', '');
    +    return raw
    +      .split(',')
    +      .map((o) => o.trim())
    +      .filter(Boolean);
    +  }
     }
    diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
    index 877411223..1c2ccebf1 100644
    --- a/apps/server/src/main.ts
    +++ b/apps/server/src/main.ts
    @@ -12,6 +12,8 @@ import fastifyMultipart from '@fastify/multipart';
     import fastifyCookie from '@fastify/cookie';
     import fastifyIp from 'fastify-ip';
     import { InternalLogFilter } from './common/logger/internal-log-filter';
    +import { EnvironmentService } from './integrations/environment/environment.service';
    +import { resolveFrameHeader } from './common/helpers';
     
     async function bootstrap() {
       const app = await NestFactory.create(
    @@ -50,6 +52,28 @@ async function bootstrap() {
       await app.register(fastifyMultipart);
       await app.register(fastifyCookie);
     
    +  const environmentService = app.get(EnvironmentService);
    +  const frameHeader = resolveFrameHeader(
    +    environmentService.isIframeEmbedAllowed(),
    +    environmentService.getIframeAllowedOrigins(),
    +  );
    +  if (frameHeader) {
    +    // Skipped routes:
    +    //   /api/files/ - attachment controller sets its own CSP we'd overwrite
    +    //   /share/     0 public share pages are safe to embed
    +    const frameHeaderSkippedPrefixes = ['/api/files/', '/share/'];
    +    app
    +      .getHttpAdapter()
    +      .getInstance()
    +      .addHook('onSend', (req, reply, payload, done) => {
    +        if (frameHeaderSkippedPrefixes.some((p) => req.url.startsWith(p))) {
    +          return done(null, payload);
    +        }
    +        reply.header(frameHeader.name, frameHeader.value);
    +        done(null, payload);
    +      });
    +  }
    +
       app
         .getHttpAdapter()
         .getInstance()