From ba6163b6d87b0c08499b87c245cd1b7ebc0a871e Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sun, 19 Oct 2025 08:16:56 -0700 Subject: [PATCH] Better code splitting and removed final instances of react-dnd --- package-lock.json | 148 ++------ src-web/components/DynamicForm.tsx | 2 +- src-web/components/GrpcEditor.tsx | 2 +- src-web/components/GrpcResponsePane.tsx | 2 +- src-web/components/HeadersEditor.tsx | 3 - src-web/components/HttpRequestPane.tsx | 23 +- src-web/components/HttpResponsePane.tsx | 88 +++-- src-web/components/MarkdownEditor.tsx | 2 +- src-web/components/Overlay.tsx | 93 ++--- src-web/components/Settings/SettingsTheme.tsx | 29 +- src-web/components/WebsocketRequestPane.tsx | 2 +- src-web/components/WebsocketResponsePane.tsx | 2 +- src-web/components/core/BulkPairEditor.tsx | 2 +- src-web/components/core/Editor/LazyEditor.tsx | 13 + src-web/components/core/Icon.tsx | 354 ++++++++++++------ src-web/components/core/Input.tsx | 8 +- src-web/components/core/PairEditor.tsx | 266 +++++++------ src-web/components/core/Tabs/Tabs.tsx | 1 - src-web/components/core/Tooltip.tsx | 21 +- .../core/tree/AutoScrollWhileDragging.tsx | 63 ---- src-web/components/core/tree/Tree.tsx | 5 +- src-web/components/core/tree/TreeItem.tsx | 4 +- src-web/components/core/tree/common.ts | 25 +- src-web/components/graphql/GraphQLEditor.tsx | 2 +- .../responseViewers/EventStreamViewer.tsx | 2 +- .../components/responseViewers/PdfViewer.tsx | 13 +- .../components/responseViewers/TextViewer.tsx | 2 +- src-web/lib/dnd.ts | 23 ++ src-web/main.tsx | 7 - src-web/package.json | 10 +- src-web/routes/__root.tsx | 29 +- src-web/vite.config.ts | 13 +- 32 files changed, 654 insertions(+), 605 deletions(-) create mode 100644 src-web/components/core/Editor/LazyEditor.tsx delete mode 100644 src-web/components/core/tree/AutoScrollWhileDragging.tsx create mode 100644 src-web/lib/dnd.ts diff --git a/package-lock.json b/package-lock.json index 19e912e9..3be3c9a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1969,24 +1969,6 @@ "node": ">=14" } }, - "node_modules/@react-dnd/asap": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", - "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", - "license": "MIT" - }, - "node_modules/@react-dnd/invariant": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", - "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", - "license": "MIT" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", - "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", - "license": "MIT" - }, "node_modules/@replit/codemirror-emacs": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz", @@ -2854,6 +2836,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", + "dev": true, "license": "MIT", "peerDependencies": { "tailwindcss": ">=3.2.0" @@ -2873,9 +2856,9 @@ } }, "node_modules/@tanstack/history": { - "version": "1.121.34", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.121.34.tgz", - "integrity": "sha512-YL8dGi5ZU+xvtav2boRlw4zrRghkY6hvdcmHhA0RGSJ/CBgzv+cbADW9eYJLx74XMZvIQ1pp6VMbrpXnnM5gHA==", + "version": "1.133.3", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.133.3.tgz", + "integrity": "sha512-zFQnGdX0S4g5xRuS+95iiEXM+qlGvYG7ksmOKx7LaMv60lDWa0imR8/24WwXXvBWJT1KnwVdZcjvhCwz9IiJCw==", "license": "MIT", "engines": { "node": ">=12" @@ -2886,9 +2869,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.83.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", - "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==", + "version": "5.90.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.5.tgz", + "integrity": "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==", "license": "MIT", "funding": { "type": "github", @@ -2896,12 +2879,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.83.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz", - "integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==", + "version": "5.90.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.5.tgz", + "integrity": "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.83.0" + "@tanstack/query-core": "5.90.5" }, "funding": { "type": "github", @@ -2912,14 +2895,14 @@ } }, "node_modules/@tanstack/react-router": { - "version": "1.127.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.127.3.tgz", - "integrity": "sha512-QprmWHJrGbEKXJiP7WZ+dilTJRc7nMbsFCUnfAUw8PsOYanhgvBkBwAU05YEo8WTIZ9atCc1R90hyzqbiBFkdA==", + "version": "1.133.13", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.133.13.tgz", + "integrity": "sha512-mVAj70mPOH/a60Hjlha3gHEWLFuE4kHeKau/AL5Xp6e5GtNk1JTRwN4sJ9QlSyLcClOUUtGfED1FoLj0D2W0Eg==", "license": "MIT", "dependencies": { - "@tanstack/history": "1.121.34", + "@tanstack/history": "1.133.3", "@tanstack/react-store": "^0.7.0", - "@tanstack/router-core": "1.127.3", + "@tanstack/router-core": "1.133.13", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" @@ -2972,14 +2955,14 @@ } }, "node_modules/@tanstack/router-core": { - "version": "1.127.3", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.127.3.tgz", - "integrity": "sha512-08JlfwsMIDkMyCQsRviMVBn0cVUzlNzkll4pZgf6QRSO1RASBsci5hMojcsdH0d/yXLH0FBJ6fINbj0ctBm63Q==", + "version": "1.133.13", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.133.13.tgz", + "integrity": "sha512-zZptdlS/wSkqozb07Y3zX5gas2OapJdjEG6/Id0e/twNefVdR4EY2TK/mgvyhHtKIpCxIcnZz/3opypgeQi9bg==", "license": "MIT", "dependencies": { - "@tanstack/history": "1.121.34", + "@tanstack/history": "1.133.3", "@tanstack/store": "^0.7.0", - "cookie-es": "^1.2.2", + "cookie-es": "^2.0.0", "seroval": "^1.3.2", "seroval-plugins": "^1.3.2", "tiny-invariant": "^1.3.3", @@ -5915,9 +5898,9 @@ "license": "MIT" }, "node_modules/cookie-es": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", - "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz", + "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==", "license": "MIT" }, "node_modules/copy-descriptor": { @@ -6707,17 +6690,6 @@ "dev": true, "license": "MIT" }, - "node_modules/dnd-core": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", - "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", - "license": "MIT", - "dependencies": { - "@react-dnd/asap": "^5.0.1", - "@react-dnd/invariant": "^4.0.1", - "redux": "^4.2.0" - } - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -9273,15 +9245,6 @@ "@babel/runtime": "^7.7.6" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -14421,46 +14384,6 @@ "react-dom": ">=16.8.0" } }, - "node_modules/react-dnd": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", - "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", - "license": "MIT", - "dependencies": { - "@react-dnd/invariant": "^4.0.1", - "@react-dnd/shallowequal": "^4.0.1", - "dnd-core": "^16.0.1", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "@types/hoist-non-react-statics": ">= 3.3.1", - "@types/node": ">= 12", - "@types/react": ">= 16", - "react": ">= 16.14" - }, - "peerDependenciesMeta": { - "@types/hoist-non-react-statics": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-dnd-touch-backend": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz", - "integrity": "sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==", - "license": "MIT", - "dependencies": { - "@react-dnd/invariant": "^4.0.1", - "dnd-core": "^16.0.1" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -14479,6 +14402,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/react-markdown": { @@ -14847,15 +14771,6 @@ "node": ">=8" } }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -16907,6 +16822,7 @@ "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "dev": true, "license": "MIT", "peer": true }, @@ -18864,7 +18780,7 @@ }, "packages/plugin-runtime-types": { "name": "@yaakapp/api", - "version": "0.6.6", + "version": "0.7.0", "dependencies": { "@types/node": "^24.0.13" }, @@ -19136,10 +19052,9 @@ "@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", - "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^5.76.1", - "@tanstack/react-router": "^1.120.3", - "@tanstack/react-virtual": "^3.13.8", + "@tanstack/react-query": "^5.90.5", + "@tanstack/react-router": "^1.133.13", + "@tanstack/react-virtual": "^3.13.12", "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.4.0", @@ -19169,8 +19084,6 @@ "parse-color": "^1.0.0", "react": "^19.1.0", "react-colorful": "^5.6.1", - "react-dnd": "^16.0.1", - "react-dnd-touch-backend": "^16.0.1", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", "react-pdf": "^10.0.1", @@ -19187,6 +19100,7 @@ }, "devDependencies": { "@lezer/generator": "^1.8.0", + "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/nesting": "^0.0.0-insiders.565cd3e", "@tanstack/router-plugin": "^1.127.5", "@types/node": "^24.0.13", diff --git a/src-web/components/DynamicForm.tsx b/src-web/components/DynamicForm.tsx index 651ba7cc..73e98862 100644 --- a/src-web/components/DynamicForm.tsx +++ b/src-web/components/DynamicForm.tsx @@ -21,7 +21,7 @@ import { resolvedModelName } from '../lib/resolvedModelName'; import { Banner } from './core/Banner'; import { Checkbox } from './core/Checkbox'; import { DetailsBanner } from './core/DetailsBanner'; -import { Editor } from './core/Editor/Editor'; +import { Editor } from './core/Editor/LazyEditor'; import { IconButton } from './core/IconButton'; import { Input } from './core/Input'; import { Label } from './core/Label'; diff --git a/src-web/components/GrpcEditor.tsx b/src-web/components/GrpcEditor.tsx index 13139fa5..90288d5d 100644 --- a/src-web/components/GrpcEditor.tsx +++ b/src-web/components/GrpcEditor.tsx @@ -17,7 +17,7 @@ import { showDialog } from '../lib/dialog'; import { pluralizeCount } from '../lib/pluralize'; import { Button } from './core/Button'; import type { EditorProps } from './core/Editor/Editor'; -import { Editor } from './core/Editor/Editor'; +import { Editor } from './core/Editor/LazyEditor'; import { FormattedError } from './core/FormattedError'; import { InlineCode } from './core/InlineCode'; import { VStack } from './core/Stacks'; diff --git a/src-web/components/GrpcResponsePane.tsx b/src-web/components/GrpcResponsePane.tsx index 2566cb32..6cfda48f 100644 --- a/src-web/components/GrpcResponsePane.tsx +++ b/src-web/components/GrpcResponsePane.tsx @@ -15,7 +15,7 @@ import { copyToClipboard } from '../lib/copy'; import { AutoScroller } from './core/AutoScroller'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; -import { Editor } from './core/Editor/Editor'; +import { Editor } from './core/Editor/LazyEditor'; import { HotKeyList } from './core/HotKeyList'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; diff --git a/src-web/components/HeadersEditor.tsx b/src-web/components/HeadersEditor.tsx index c18dcfdc..bb8d0fc7 100644 --- a/src-web/components/HeadersEditor.tsx +++ b/src-web/components/HeadersEditor.tsx @@ -53,9 +53,6 @@ export function HeadersEditor({ disabled disableDrag className="py-1" - onChange={() => {}} - onEnd={() => {}} - onMove={() => {}} pair={ensurePairId(pair)} stateKey={null} nameAutocompleteFunctions diff --git a/src-web/components/HttpRequestPane.tsx b/src-web/components/HttpRequestPane.tsx index ccabdb27..7221181b 100644 --- a/src-web/components/HttpRequestPane.tsx +++ b/src-web/components/HttpRequestPane.tsx @@ -4,7 +4,7 @@ import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import classNames from 'classnames'; import { atom, useAtomValue } from 'jotai'; import type { CSSProperties } from 'react'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { lazy, Suspense, useCallback, useMemo, useState } from 'react'; import { activeRequestIdAtom } from '../hooks/useActiveRequestId'; import { allRequestsAtom } from '../hooks/useAllRequests'; import { useAuthTab } from '../hooks/useAuthTab'; @@ -37,7 +37,7 @@ import { showToast } from '../lib/toast'; import { BinaryFileEditor } from './BinaryFileEditor'; import { ConfirmLargeRequestBody } from './ConfirmLargeRequestBody'; import { CountBadge } from './core/CountBadge'; -import { Editor } from './core/Editor/Editor'; +import { Editor } from './core/Editor/LazyEditor'; import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; import { InlineCode } from './core/InlineCode'; import type { Pair } from './core/PairEditor'; @@ -53,7 +53,10 @@ import { MarkdownEditor } from './MarkdownEditor'; import { RequestMethodDropdown } from './RequestMethodDropdown'; import { UrlBar } from './UrlBar'; import { UrlParametersEditor } from './UrlParameterEditor'; -import { GraphQLEditor } from './graphql/GraphQLEditor'; + +const GraphQLEditor = lazy(() => + import('./graphql/GraphQLEditor').then((m) => ({ default: m.GraphQLEditor })), +); interface Props { style: CSSProperties; @@ -405,12 +408,14 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }: stateKey={`xml.${activeRequest.id}`} /> ) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? ( - + + + ) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? ( + import('./responseViewers/PdfViewer').then((m) => ({ default: m.PdfViewer })), +); + interface Props { style?: CSSProperties; className?: string; @@ -106,9 +109,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) { )} > {activeResponse == null ? ( - + ) : (
- - {activeResponse.state === 'initialized' ? ( - - - - - Sending Request - - - - - ) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? ( - Empty - ) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? ( - - ) : mimeType?.match(/^image\/svg/) ? ( - - ) : mimeType?.match(/^image/i) ? ( - - ) : mimeType?.match(/^audio/i) ? ( - - ) : mimeType?.match(/^video/i) ? ( - - ) : mimeType?.match(/pdf/i) ? ( - - ) : mimeType?.match(/csv|tab-separated/i) ? ( - - ) : ( - - )} - + + + {activeResponse.state === 'initialized' ? ( + + + + + Sending Request + + + + + ) : activeResponse.state === 'closed' && + activeResponse.contentLength === 0 ? ( + Empty + ) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? ( + + ) : mimeType?.match(/^image\/svg/) ? ( + + ) : mimeType?.match(/^image/i) ? ( + + ) : mimeType?.match(/^audio/i) ? ( + + ) : mimeType?.match(/^video/i) ? ( + + ) : mimeType?.match(/pdf/i) ? ( + + ) : mimeType?.match(/csv|tab-separated/i) ? ( + + ) : ( + + )} + + diff --git a/src-web/components/MarkdownEditor.tsx b/src-web/components/MarkdownEditor.tsx index 1edce0c9..8b068e20 100644 --- a/src-web/components/MarkdownEditor.tsx +++ b/src-web/components/MarkdownEditor.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import { useRef, useState } from 'react'; import type { EditorProps } from './core/Editor/Editor'; -import { Editor } from './core/Editor/Editor'; +import { Editor } from './core/Editor/LazyEditor'; import { SegmentedControl } from './core/SegmentedControl'; import { Markdown } from './Markdown'; diff --git a/src-web/components/Overlay.tsx b/src-web/components/Overlay.tsx index f60eac44..91e174f1 100644 --- a/src-web/components/Overlay.tsx +++ b/src-web/components/Overlay.tsx @@ -1,10 +1,11 @@ import classNames from 'classnames'; -import { FocusTrap } from 'focus-trap-react'; import * as m from 'motion/react-m'; -import type { ReactNode } from 'react'; -import React, { useRef } from 'react'; +import type { ReactNode} from 'react'; +import React, { Suspense , lazy, useRef } from 'react'; import { Portal } from './Portal'; +const FocusTrap = lazy(() => import('focus-trap-react')); + interface Props { children: ReactNode; portalName: string; @@ -50,50 +51,52 @@ export function Overlay({ return ( {open && ( - containerRef.current!, // always have a target - initialFocus: () => - // Doing this explicitly seems to work better than the default behavior for some reason - containerRef.current?.querySelector( - [ - 'a[href]', - 'input:not([disabled])', - 'select:not([disabled])', - 'textarea:not([disabled])', - 'button:not([disabled])', - '[tabindex]:not([tabindex="-1"])', - '[contenteditable]:not([contenteditable="false"])', - ].join(', '), - ) ?? undefined, - }} - > - + containerRef.current!, // always have a target + initialFocus: () => + // Doing this explicitly seems to work better than the default behavior for some reason + containerRef.current?.querySelector( + [ + 'a[href]', + 'input:not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])', + 'button:not([disabled])', + '[tabindex]:not([tabindex="-1"])', + '[contenteditable]:not([contenteditable="false"])', + ].join(', '), + ) ?? undefined, + }} > -
+ +
- {/* Show the draggable region at the top */} - {/* TODO: Figure out tauri drag region and also make clickable still */} - {variant === 'default' && ( -
- )} - {children} - - + {/* Show the draggable region at the top */} + {/* TODO: Figure out tauri drag region and also make clickable still */} + {variant === 'default' && ( +
+ )} + {children} + + + )} ); diff --git a/src-web/components/Settings/SettingsTheme.tsx b/src-web/components/Settings/SettingsTheme.tsx index a95be536..9c2a1901 100644 --- a/src-web/components/Settings/SettingsTheme.tsx +++ b/src-web/components/Settings/SettingsTheme.tsx @@ -1,11 +1,10 @@ import { patchModel, settingsAtom } from '@yaakapp-internal/models'; import { useAtomValue } from 'jotai'; -import React from 'react'; +import React, { lazy, Suspense } from 'react'; import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace'; import { useResolvedAppearance } from '../../hooks/useResolvedAppearance'; import { useResolvedTheme } from '../../hooks/useResolvedTheme'; import type { ButtonProps } from '../core/Button'; -import { Editor } from '../core/Editor/Editor'; import type { IconProps } from '../core/Icon'; import { Icon } from '../core/Icon'; import { IconButton } from '../core/IconButton'; @@ -13,6 +12,8 @@ import type { SelectProps } from '../core/Select'; import { Select } from '../core/Select'; import { HStack, VStack } from '../core/Stacks'; +const Editor = lazy(() => import('../core/Editor/Editor').then((m) => ({ default: m.Editor }))); + const buttonColors: ButtonProps['color'][] = [ 'primary', 'info', @@ -144,17 +145,19 @@ export function SettingsTheme() { /> ))} - + + + ); diff --git a/src-web/components/WebsocketRequestPane.tsx b/src-web/components/WebsocketRequestPane.tsx index b2feaada..63028265 100644 --- a/src-web/components/WebsocketRequestPane.tsx +++ b/src-web/components/WebsocketRequestPane.tsx @@ -25,7 +25,7 @@ import { generateId } from '../lib/generateId'; import { prepareImportQuerystring } from '../lib/prepareImportQuerystring'; import { resolvedModelName } from '../lib/resolvedModelName'; import { CountBadge } from './core/CountBadge'; -import { Editor } from './core/Editor/Editor'; +import { Editor } from './core/Editor/LazyEditor'; import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; import { IconButton } from './core/IconButton'; import type { Pair } from './core/PairEditor'; diff --git a/src-web/components/WebsocketResponsePane.tsx b/src-web/components/WebsocketResponsePane.tsx index c11b1397..b2dcd3f7 100644 --- a/src-web/components/WebsocketResponsePane.tsx +++ b/src-web/components/WebsocketResponsePane.tsx @@ -17,7 +17,7 @@ import { copyToClipboard } from '../lib/copy'; import { AutoScroller } from './core/AutoScroller'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; -import { Editor } from './core/Editor/Editor'; +import { Editor } from './core/Editor/LazyEditor'; import { HotKeyList } from './core/HotKeyList'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; diff --git a/src-web/components/core/BulkPairEditor.tsx b/src-web/components/core/BulkPairEditor.tsx index 9c67bcf4..f6d17a13 100644 --- a/src-web/components/core/BulkPairEditor.tsx +++ b/src-web/components/core/BulkPairEditor.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react'; import { generateId } from '../../lib/generateId'; -import { Editor } from './Editor/Editor'; +import { Editor } from './Editor/LazyEditor'; import type { Pair, PairEditorProps, PairWithId } from './PairEditor'; type Props = PairEditorProps; diff --git a/src-web/components/core/Editor/LazyEditor.tsx b/src-web/components/core/Editor/LazyEditor.tsx new file mode 100644 index 00000000..5b6adf31 --- /dev/null +++ b/src-web/components/core/Editor/LazyEditor.tsx @@ -0,0 +1,13 @@ +import type { EditorView } from '@codemirror/view'; +import { forwardRef, lazy, Suspense } from 'react'; +import type { EditorProps } from './Editor'; + +const Editor_ = lazy(() => import('./Editor').then((m) => ({ default: m.Editor }))); + +export const Editor = forwardRef(function LazyEditor(props, ref) { + return ( + + + + ); +}); diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index 8b8de601..c3d0c506 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -1,127 +1,245 @@ import type { Color } from '@yaakapp-internal/plugins'; import classNames from 'classnames'; -import * as lucide from 'lucide-react'; +import { + AlertTriangleIcon, + ArchiveIcon, + ArrowBigDownDashIcon, + ArrowBigLeftDashIcon, + ArrowBigRightDashIcon, + ArrowBigRightIcon, + ArrowBigUpDashIcon, + ArrowDownIcon, + ArrowDownToDotIcon, + ArrowDownToLineIcon, + ArrowRightCircleIcon, + ArrowUpDownIcon, + ArrowUpFromDotIcon, + ArrowUpFromLineIcon, + ArrowUpIcon, + BadgeCheckIcon, + BookOpenText, + BoxIcon, + CakeIcon, + CheckCircleIcon, + CheckIcon, + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, + CircleAlertIcon, + CircleDashedIcon, + CircleDollarSignIcon, + CircleFadingArrowUpIcon, + CircleHelpIcon, + ClipboardPasteIcon, + ClockIcon, + CodeIcon, + Columns2Icon, + CommandIcon, + CookieIcon, + CopyCheck, + CopyIcon, + CornerRightUpIcon, + CreditCardIcon, + DotIcon, + DownloadIcon, + EllipsisIcon, + ExpandIcon, + ExternalLinkIcon, + EyeIcon, + EyeOffIcon, + FileCodeIcon, + FileTextIcon, + FilterIcon, + FlameIcon, + FlaskConicalIcon, + FolderCodeIcon, + FolderCogIcon, + FolderGitIcon, + FolderIcon, + FolderInputIcon, + FolderOpenIcon, + FolderOutputIcon, + FolderSymlinkIcon, + FolderSyncIcon, + FolderUpIcon, + GitBranchIcon, + GitBranchPlusIcon, + GitCommitIcon, + GitCommitVerticalIcon, + GitForkIcon, + GitPullRequestIcon, + GripVerticalIcon, + HandIcon, + HistoryIcon, + HomeIcon, + ImportIcon, + InfoIcon, + KeyboardIcon, + KeyRoundIcon, + LockIcon, + LockOpenIcon, + MergeIcon, + MessageSquare, + MinusCircleIcon, + MinusIcon, + MoonIcon, + MoreVerticalIcon, + PaletteIcon, + PanelLeftCloseIcon, + PanelLeftOpenIcon, + PencilIcon, + PinIcon, + PinOffIcon, + Plug, + PlusCircleIcon, + PlusIcon, + PuzzleIcon, + RefreshCcwIcon, + RefreshCwIcon, + RocketIcon, + Rows2Icon, + SaveIcon, + SearchIcon, + SendHorizonalIcon, + SettingsIcon, + ShieldAlertIcon, + ShieldCheckIcon, + ShieldIcon, + ShieldOffIcon, + SparklesIcon, + SquareCheckIcon, + SquareIcon, + SquareTerminalIcon, + SunIcon, + TableIcon, + Trash2Icon, + UploadIcon, + VariableIcon, + Wand2Icon, + WrenchIcon, + XIcon, +} from 'lucide-react'; import type { CSSProperties, HTMLAttributes } from 'react'; import { memo } from 'react'; const icons = { - alert_triangle: lucide.AlertTriangleIcon, - archive: lucide.ArchiveIcon, - arrow_big_down_dash: lucide.ArrowBigDownDashIcon, - arrow_big_left_dash: lucide.ArrowBigLeftDashIcon, - arrow_big_right: lucide.ArrowBigRightIcon, - arrow_big_right_dash: lucide.ArrowBigRightDashIcon, - arrow_big_up_dash: lucide.ArrowBigUpDashIcon, - arrow_down: lucide.ArrowDownIcon, - arrow_down_to_dot: lucide.ArrowDownToDotIcon, - arrow_down_to_line: lucide.ArrowDownToLineIcon, - arrow_right_circle: lucide.ArrowRightCircleIcon, - arrow_up: lucide.ArrowUpIcon, - arrow_up_down: lucide.ArrowUpDownIcon, - arrow_up_from_dot: lucide.ArrowUpFromDotIcon, - arrow_up_from_line: lucide.ArrowUpFromLineIcon, - badge_check: lucide.BadgeCheckIcon, - book_open_text: lucide.BookOpenText, - box: lucide.BoxIcon, - cake: lucide.CakeIcon, - chat: lucide.MessageSquare, - check: lucide.CheckIcon, - check_circle: lucide.CheckCircleIcon, - check_square_checked: lucide.SquareCheckIcon, - check_square_unchecked: lucide.SquareIcon, - chevron_down: lucide.ChevronDownIcon, - chevron_left: lucide.ChevronLeftIcon, - chevron_right: lucide.ChevronRightIcon, - circle_alert: lucide.CircleAlertIcon, - circle_dashed: lucide.CircleDashedIcon, - circle_dollar_sign: lucide.CircleDollarSignIcon, - circle_fading_arrow_up: lucide.CircleFadingArrowUpIcon, - clock: lucide.ClockIcon, - code: lucide.CodeIcon, - columns_2: lucide.Columns2Icon, - command: lucide.CommandIcon, - cookie: lucide.CookieIcon, - copy: lucide.CopyIcon, - copy_check: lucide.CopyCheck, - corner_right_up: lucide.CornerRightUpIcon, - credit_card: lucide.CreditCardIcon, - dot: lucide.DotIcon, - download: lucide.DownloadIcon, - ellipsis: lucide.EllipsisIcon, - expand: lucide.ExpandIcon, - external_link: lucide.ExternalLinkIcon, - eye: lucide.EyeIcon, - eye_closed: lucide.EyeOffIcon, - file_code: lucide.FileCodeIcon, - filter: lucide.FilterIcon, - flame: lucide.FlameIcon, - flask: lucide.FlaskConicalIcon, - folder: lucide.FolderIcon, - folder_code: lucide.FolderCodeIcon, - folder_cog: lucide.FolderCogIcon, - folder_git: lucide.FolderGitIcon, - folder_input: lucide.FolderInputIcon, - folder_open: lucide.FolderOpenIcon, - folder_output: lucide.FolderOutputIcon, - folder_symlink: lucide.FolderSymlinkIcon, - folder_sync: lucide.FolderSyncIcon, - folder_up: lucide.FolderUpIcon, - git_branch: lucide.GitBranchIcon, - git_branch_plus: lucide.GitBranchPlusIcon, - git_commit: lucide.GitCommitIcon, - git_commit_vertical: lucide.GitCommitVerticalIcon, - git_fork: lucide.GitForkIcon, - git_pull_request: lucide.GitPullRequestIcon, - grip_vertical: lucide.GripVerticalIcon, - hand: lucide.HandIcon, - help: lucide.CircleHelpIcon, - history: lucide.HistoryIcon, - house: lucide.HomeIcon, - import: lucide.ImportIcon, - info: lucide.InfoIcon, - key_round: lucide.KeyRoundIcon, - keyboard: lucide.KeyboardIcon, - left_panel_hidden: lucide.PanelLeftOpenIcon, - left_panel_visible: lucide.PanelLeftCloseIcon, - lock: lucide.LockIcon, - lock_open: lucide.LockOpenIcon, - magic_wand: lucide.Wand2Icon, - merge: lucide.MergeIcon, - minus: lucide.MinusIcon, - minus_circle: lucide.MinusCircleIcon, - moon: lucide.MoonIcon, - more_vertical: lucide.MoreVerticalIcon, - palette: lucide.PaletteIcon, - paste: lucide.ClipboardPasteIcon, - pencil: lucide.PencilIcon, - pin: lucide.PinIcon, - plug: lucide.Plug, - plus: lucide.PlusIcon, - plus_circle: lucide.PlusCircleIcon, - puzzle: lucide.PuzzleIcon, - refresh: lucide.RefreshCwIcon, - rocket: lucide.RocketIcon, - rows_2: lucide.Rows2Icon, - save: lucide.SaveIcon, - search: lucide.SearchIcon, - send_horizontal: lucide.SendHorizonalIcon, - settings: lucide.SettingsIcon, - shield: lucide.ShieldIcon, - shield_check: lucide.ShieldCheckIcon, - shield_off: lucide.ShieldOffIcon, - sparkles: lucide.SparklesIcon, - square_terminal: lucide.SquareTerminalIcon, - sun: lucide.SunIcon, - table: lucide.TableIcon, - text: lucide.FileTextIcon, - trash: lucide.Trash2Icon, - unpin: lucide.PinOffIcon, - update: lucide.RefreshCcwIcon, - upload: lucide.UploadIcon, - variable: lucide.VariableIcon, - wrench: lucide.WrenchIcon, - x: lucide.XIcon, - _unknown: lucide.ShieldAlertIcon, + alert_triangle: AlertTriangleIcon, + archive: ArchiveIcon, + arrow_big_down_dash: ArrowBigDownDashIcon, + arrow_big_left_dash: ArrowBigLeftDashIcon, + arrow_big_right: ArrowBigRightIcon, + arrow_big_right_dash: ArrowBigRightDashIcon, + arrow_big_up_dash: ArrowBigUpDashIcon, + arrow_down: ArrowDownIcon, + arrow_down_to_dot: ArrowDownToDotIcon, + arrow_down_to_line: ArrowDownToLineIcon, + arrow_right_circle: ArrowRightCircleIcon, + arrow_up: ArrowUpIcon, + arrow_up_down: ArrowUpDownIcon, + arrow_up_from_dot: ArrowUpFromDotIcon, + arrow_up_from_line: ArrowUpFromLineIcon, + badge_check: BadgeCheckIcon, + book_open_text: BookOpenText, + box: BoxIcon, + cake: CakeIcon, + chat: MessageSquare, + check: CheckIcon, + check_circle: CheckCircleIcon, + check_square_checked: SquareCheckIcon, + check_square_unchecked: SquareIcon, + chevron_down: ChevronDownIcon, + chevron_left: ChevronLeftIcon, + chevron_right: ChevronRightIcon, + circle_alert: CircleAlertIcon, + circle_dashed: CircleDashedIcon, + circle_dollar_sign: CircleDollarSignIcon, + circle_fading_arrow_up: CircleFadingArrowUpIcon, + clock: ClockIcon, + code: CodeIcon, + columns_2: Columns2Icon, + command: CommandIcon, + cookie: CookieIcon, + copy: CopyIcon, + copy_check: CopyCheck, + corner_right_up: CornerRightUpIcon, + credit_card: CreditCardIcon, + dot: DotIcon, + download: DownloadIcon, + ellipsis: EllipsisIcon, + expand: ExpandIcon, + external_link: ExternalLinkIcon, + eye: EyeIcon, + eye_closed: EyeOffIcon, + file_code: FileCodeIcon, + filter: FilterIcon, + flame: FlameIcon, + flask: FlaskConicalIcon, + folder: FolderIcon, + folder_code: FolderCodeIcon, + folder_cog: FolderCogIcon, + folder_git: FolderGitIcon, + folder_input: FolderInputIcon, + folder_open: FolderOpenIcon, + folder_output: FolderOutputIcon, + folder_symlink: FolderSymlinkIcon, + folder_sync: FolderSyncIcon, + folder_up: FolderUpIcon, + git_branch: GitBranchIcon, + git_branch_plus: GitBranchPlusIcon, + git_commit: GitCommitIcon, + git_commit_vertical: GitCommitVerticalIcon, + git_fork: GitForkIcon, + git_pull_request: GitPullRequestIcon, + grip_vertical: GripVerticalIcon, + hand: HandIcon, + help: CircleHelpIcon, + history: HistoryIcon, + house: HomeIcon, + import: ImportIcon, + info: InfoIcon, + key_round: KeyRoundIcon, + keyboard: KeyboardIcon, + left_panel_hidden: PanelLeftOpenIcon, + left_panel_visible: PanelLeftCloseIcon, + lock: LockIcon, + lock_open: LockOpenIcon, + magic_wand: Wand2Icon, + merge: MergeIcon, + minus: MinusIcon, + minus_circle: MinusCircleIcon, + moon: MoonIcon, + more_vertical: MoreVerticalIcon, + palette: PaletteIcon, + paste: ClipboardPasteIcon, + pencil: PencilIcon, + pin: PinIcon, + plug: Plug, + plus: PlusIcon, + plus_circle: PlusCircleIcon, + puzzle: PuzzleIcon, + refresh: RefreshCwIcon, + rocket: RocketIcon, + rows_2: Rows2Icon, + save: SaveIcon, + search: SearchIcon, + send_horizontal: SendHorizonalIcon, + settings: SettingsIcon, + shield: ShieldIcon, + shield_check: ShieldCheckIcon, + shield_off: ShieldOffIcon, + sparkles: SparklesIcon, + square_terminal: SquareTerminalIcon, + sun: SunIcon, + table: TableIcon, + text: FileTextIcon, + trash: Trash2Icon, + unpin: PinOffIcon, + update: RefreshCcwIcon, + upload: UploadIcon, + variable: VariableIcon, + wrench: WrenchIcon, + x: XIcon, + _unknown: ShieldAlertIcon, empty: (props: HTMLAttributes) =>
, }; diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index 41f1c121..da218984 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -1,4 +1,3 @@ -import { EditorSelection } from '@codemirror/state'; import type { EditorView } from '@codemirror/view'; import type { Color } from '@yaakapp-internal/plugins'; import classNames from 'classnames'; @@ -30,7 +29,7 @@ import { Button } from './Button'; import type { DropdownItem } from './Dropdown'; import { Dropdown } from './Dropdown'; import type { EditorProps } from './Editor/Editor'; -import { Editor } from './Editor/Editor'; +import { Editor } from './Editor/LazyEditor'; import type { IconProps } from './Icon'; import { Icon } from './Icon'; import { IconButton } from './IconButton'; @@ -161,11 +160,12 @@ const BaseInput = forwardRef(function InputBase( onFocus?.(); }, [onFocus, readOnly]); - const handleBlur = useCallback(() => { + const handleBlur = useCallback(async () => { setFocused(false); // Move selection to the end on blur + const anchor = editorRef.current?.state.doc.length ?? 0; editorRef.current?.dispatch({ - selection: EditorSelection.single(editorRef.current.state.doc.length ), + selection: { anchor, head: anchor }, }); onBlur?.(); }, [onBlur]); diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx index a4a045a2..c90d8614 100644 --- a/src-web/components/core/PairEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -1,4 +1,16 @@ import type { EditorView } from '@codemirror/view'; +import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core'; +import { + DndContext, + DragOverlay, + PointerSensor, + pointerWithin, + useDraggable, + useDroppable, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { arrayMove } from '@dnd-kit/sortable'; import classNames from 'classnames'; import { forwardRef, @@ -10,13 +22,12 @@ import { useRef, useState, } from 'react'; -import type { XYCoord } from 'react-dnd'; -import { useDrag, useDrop } from 'react-dnd'; import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables'; import { useRandomKey } from '../../hooks/useRandomKey'; import { useToggle } from '../../hooks/useToggle'; import { languageFromContentType } from '../../lib/contentType'; import { showDialog } from '../../lib/dialog'; +import { computeSideForDragMove } from '../../lib/dnd'; import { showPrompt } from '../../lib/prompt'; import { DropMarker } from '../DropMarker'; import { SelectFile } from '../SelectFile'; @@ -25,8 +36,8 @@ import { Checkbox } from './Checkbox'; import type { DropdownItem } from './Dropdown'; import { Dropdown } from './Dropdown'; import type { EditorProps } from './Editor/Editor'; -import { Editor } from './Editor/Editor'; import type { GenericCompletionConfig } from './Editor/genericCompletion'; +import { Editor } from './Editor/LazyEditor'; import { Icon } from './Icon'; import { IconButton } from './IconButton'; import type { InputProps } from './Input'; @@ -108,6 +119,7 @@ export const PairEditor = forwardRef(function Pa const [forceFocusNamePairId, setForceFocusNamePairId] = useState(null); const [forceFocusValuePairId, setForceFocusValuePairId] = useState(null); const [hoveredIndex, setHoveredIndex] = useState(null); + const [isDragging, setIsDragging] = useState(null); const [pairs, setPairs] = useState([]); const [showAll, toggleShowAll] = useToggle(false); // NOTE: Use local force update key because we trigger an effect on forceUpdateKey change. If @@ -158,33 +170,6 @@ export const PairEditor = forwardRef(function Pa [onChange], ); - const handleMove = useCallback( - (id, side) => { - const dragIndex = pairs.findIndex((r) => r.id === id); - setHoveredIndex(side === 'above' ? dragIndex : dragIndex + 1); - }, - [pairs], - ); - - const handleEnd = useCallback( - (id: string) => { - if (hoveredIndex === null) return; - setHoveredIndex(null); - - setPairsAndSave((pairs) => { - const index = pairs.findIndex((p) => p.id === id); - const pair = pairs[index]; - if (pair === undefined) return pairs; - - const newPairs = pairs.filter((p) => p.id !== id); - if (hoveredIndex > index) newPairs.splice(hoveredIndex - 1, 0, pair); - else newPairs.splice(hoveredIndex, 0, pair); - return newPairs; - }); - }, - [hoveredIndex, setPairsAndSave], - ); - const handleChange = useCallback( (pair: PairWithId) => setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))), @@ -233,6 +218,55 @@ export const PairEditor = forwardRef(function Pa }); }, []); + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } })); + + // dnd-kit: show the “between rows” marker while hovering + const onDragMove = useCallback( + (e: DragMoveEvent) => { + const overId = e.over?.id as string | undefined; + if (!overId) return setHoveredIndex(null); + + const overPair = pairs.find((p) => p.id === overId); + if (overPair == null) return setHoveredIndex(null); + + const side = computeSideForDragMove(overPair.id, e); + const overIndex = pairs.findIndex((p) => p.id === overId); + const hoveredIndex = overIndex + (side === 'above' ? 0 : 1); + + setHoveredIndex(hoveredIndex); + }, + [pairs], + ); + + const onDragStart = useCallback( + (e: DragStartEvent) => { + const pair = pairs.find((p) => p.id === e.active.id); + setIsDragging(pair ?? null); + }, + [pairs], + ); + + const onDragCancel = useCallback(() => setIsDragging(null), []); + + const onDragEnd = useCallback( + (e: DragEndEvent) => { + setIsDragging(null); + setHoveredIndex(null); + const activeId = e.active.id as string | undefined; + const overId = e.over?.id as string | undefined; + if (!activeId || !overId) return; + + const from = pairs.findIndex((p) => p.id === activeId); + const baseTo = pairs.findIndex((p) => p.id === overId); + const to = hoveredIndex ?? (baseTo === -1 ? from : baseTo); + + if (from !== -1 && to !== -1 && from !== to) { + setPairsAndSave((ps) => arrayMove(ps, from, to > from ? to - 1 : to)); + } + }, + [pairs, hoveredIndex, setPairsAndSave], + ); + return (
(function Pa 'pt-0.5', )} > - {pairs.map((p, i) => { - if (!showAll && i > MAX_INITIAL_PAIRS) return null; + + {pairs.map((p, i) => { + if (!showAll && i > MAX_INITIAL_PAIRS) return null; - const isLast = i === pairs.length - 1; - return ( - - {hoveredIndex === i && } + const isLast = i === pairs.length - 1; + return ( + + {hoveredIndex === i && } + + + ); + })} + {!showAll && pairs.length > MAX_INITIAL_PAIRS && ( + + )} + + {isDragging && ( - - ); - })} - {!showAll && pairs.length > MAX_INITIAL_PAIRS && ( - - )} + )} + +
); }); -enum ItemTypes { - ROW = 'pair-row', -} - type PairEditorRowProps = { className?: string; pair: PairWithId; forceFocusNamePairId?: string | null; forceFocusValuePairId?: string | null; - onMove: (id: string, side: 'above' | 'below') => void; - onEnd: (id: string) => void; - onChange: (pair: PairWithId) => void; + onChange?: (pair: PairWithId) => void; onDelete?: (pair: PairWithId, focusPrevious: boolean) => void; onFocusName?: (pair: PairWithId) => void; onFocusValue?: (pair: PairWithId) => void; @@ -315,6 +364,7 @@ type PairEditorRowProps = { disabled?: boolean; disableDrag?: boolean; index: number; + isDraggingGlobal?: boolean; } & Pick< PairEditorProps, | 'allowFileValues' @@ -352,12 +402,11 @@ export function PairEditorRow({ nameAutocompleteVariables, namePlaceholder, nameValidate, + isDraggingGlobal, onChange, onDelete, - onEnd, onFocusName, onFocusValue, - onMove, pair, stateKey, valueAutocomplete, @@ -367,7 +416,6 @@ export function PairEditorRow({ valueType, valueValidate, }: PairEditorRowProps) { - const ref = useRef(null); const nameInputRef = useRef(null); const valueInputRef = useRef(null); @@ -388,29 +436,29 @@ export function PairEditorRow({ const handleDelete = useCallback(() => onDelete?.(pair, false), [onDelete, pair]); const handleChangeEnabled = useMemo( - () => (enabled: boolean) => onChange({ ...pair, enabled }), + () => (enabled: boolean) => onChange?.({ ...pair, enabled }), [onChange, pair], ); const handleChangeName = useMemo( - () => (name: string) => onChange({ ...pair, name }), + () => (name: string) => onChange?.({ ...pair, name }), [onChange, pair], ); const handleChangeValueText = useMemo( - () => (value: string) => onChange({ ...pair, value, isFile: false }), + () => (value: string) => onChange?.({ ...pair, value, isFile: false }), [onChange, pair], ); const handleChangeValueFile = useMemo( () => ({ filePath }: { filePath: string | null }) => - onChange({ ...pair, value: filePath ?? '', isFile: true }), + onChange?.({ ...pair, value: filePath ?? '', isFile: true }), [onChange, pair], ); const handleChangeValueContentType = useMemo( - () => (contentType: string) => onChange({ ...pair, contentType }), + () => (contentType: string) => onChange?.({ ...pair, contentType }), [onChange, pair], ); @@ -448,30 +496,8 @@ export function PairEditorRow({ [allowMultilineValues, handleDelete, handleEditMultiLineValue], ); - const [, connectDrop] = useDrop( - { - accept: ItemTypes.ROW, - hover: (_, monitor) => { - if (!ref.current) return; - const hoverBoundingRect = ref.current?.getBoundingClientRect(); - const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; - const clientOffset = monitor.getClientOffset(); - const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top; - onMove(pair.id, hoverClientY < hoverMiddleY ? 'above' : 'below'); - }, - }, - [onMove], - ); - - const [, connectDrag] = useDrag( - { - type: ItemTypes.ROW, - item: () => pair, - collect: (m) => ({ isDragging: m.isDragging() }), - end: () => onEnd(pair.id), - }, - [pair, onEnd], - ); + const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({ id: pair.id }); + const { setNodeRef: setDroppableRef } = useDroppable({ id: pair.id }); // Filter out the current pair name const valueAutocompleteVariablesFiltered = useMemo(() => { @@ -482,12 +508,17 @@ export function PairEditorRow({ } }, [pair.name, valueAutocompleteVariables]); - connectDrag(ref); - connectDrop(ref); + const handleSetRef = useCallback( + (n: HTMLDivElement | null) => { + setDraggableRef(n); + setDroppableRef(n); + }, + [setDraggableRef, setDroppableRef], + ); return (
{!isLast && !disableDrag ? (
@@ -599,7 +634,8 @@ export function PairEditorRow({ wrapLines={false} size="sm" disabled={disabled} - containerClassName={classNames(isLast && 'border-dashed')} + readOnly={isDraggingGlobal} + containerClassName={classNames('bg-surface', isLast && 'border-dashed')} validate={valueValidate} forcedEnvironmentId={forcedEnvironmentId} forceUpdateKey={forceUpdateKey} diff --git a/src-web/components/core/Tabs/Tabs.tsx b/src-web/components/core/Tabs/Tabs.tsx index 77d78198..9028a7cf 100644 --- a/src-web/components/core/Tabs/Tabs.tsx +++ b/src-web/components/core/Tabs/Tabs.tsx @@ -73,7 +73,6 @@ export function Tabs({ className={classNames( className, 'tabs-container', - 'transform-gpu', 'h-full grid', layout === 'horizontal' && 'grid-rows-1 grid-cols-[auto_minmax(0,1fr)]', layout === 'vertical' && 'grid-rows-[auto_minmax(0,1fr)] grid-cols-1', diff --git a/src-web/components/core/Tooltip.tsx b/src-web/components/core/Tooltip.tsx index 239ffdc4..0d3954ab 100644 --- a/src-web/components/core/Tooltip.tsx +++ b/src-web/components/core/Tooltip.tsx @@ -1,13 +1,22 @@ import classNames from 'classnames'; -import type { CSSProperties, KeyboardEvent, ReactNode } from 'react'; -import React, { useRef, useState } from 'react'; +import type { + CSSProperties, + KeyboardEvent, + ReactNode} from 'react'; +import React, { + lazy, + Suspense, + useRef, + useState, +} from 'react'; import { generateId } from '../../lib/generateId'; -import { Portal } from '../Portal'; + +const Portal = lazy(() => import('../Portal').then((m) => ({ default: m.Portal }))); export interface TooltipProps { children: ReactNode; content: ReactNode; - tabIndex?: number, + tabIndex?: number; size?: 'md' | 'lg'; } @@ -66,7 +75,7 @@ export function Tooltip({ children, content, tabIndex, size = 'md' }: TooltipPro const id = useRef(`tooltip-${generateId()}`); return ( - <> +
{children} - + ); } diff --git a/src-web/components/core/tree/AutoScrollWhileDragging.tsx b/src-web/components/core/tree/AutoScrollWhileDragging.tsx deleted file mode 100644 index 04a7ce0b..00000000 --- a/src-web/components/core/tree/AutoScrollWhileDragging.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// AutoScrollWhileDragging.tsx -import { useEffect, useRef } from 'react'; -import { useDragLayer } from 'react-dnd'; - -type Props = { - container: HTMLElement | null | undefined; - edgeDistance?: number; - maxSpeedPerFrame?: number; -}; - -export function AutoScrollWhileDragging({ - container, - edgeDistance = 30, - maxSpeedPerFrame = 6, -}: Props) { - const rafId = useRef(null); - - const { isDragging, pointer } = useDragLayer((monitor) => ({ - isDragging: monitor.isDragging(), - pointer: monitor.getClientOffset(), // { x, y } | null - })); - - useEffect(() => { - if (!container || !isDragging) { - if (rafId.current != null) cancelAnimationFrame(rafId.current); - rafId.current = null; - return; - } - - const tick = () => { - if (!container || !isDragging || !pointer) return; - - const rect = container.getBoundingClientRect(); - const y = pointer.y; - - // Compute vertical speed based on proximity to edges - let dy = 0; - if (y < rect.top + edgeDistance) { - const t = (rect.top + edgeDistance - y) / edgeDistance; // 0..1 - dy = -Math.min(maxSpeedPerFrame, Math.ceil(t * maxSpeedPerFrame)); - } else if (y > rect.bottom - edgeDistance) { - const t = (y - (rect.bottom - edgeDistance)) / edgeDistance; // 0..1 - dy = Math.min(maxSpeedPerFrame, Math.ceil(t * maxSpeedPerFrame)); - } - - if (dy !== 0) { - // Only scroll if there’s more content in that direction - const prev = container.scrollTop; - container.scrollTop = prev + dy; - } - - rafId.current = requestAnimationFrame(tick); - }; - - rafId.current = requestAnimationFrame(tick); - return () => { - if (rafId.current != null) cancelAnimationFrame(rafId.current); - rafId.current = null; - }; - }, [container, isDragging, pointer, edgeDistance, maxSpeedPerFrame]); - - return null; -} diff --git a/src-web/components/core/tree/Tree.tsx b/src-web/components/core/tree/Tree.tsx index 307864cb..d47882b0 100644 --- a/src-web/components/core/tree/Tree.tsx +++ b/src-web/components/core/tree/Tree.tsx @@ -14,6 +14,7 @@ import { forwardRef, memo, useCallback, useImperativeHandle, useMemo, useRef } f import { useKey, useKeyPressEvent } from 'react-use'; import type { HotkeyAction, HotKeyOptions } from '../../../hooks/useHotKey'; import { useHotKey } from '../../../hooks/useHotKey'; +import { computeSideForDragMove } from '../../../lib/dnd'; import { jotaiStore } from '../../../lib/jotai'; import type { ContextMenuProps } from '../Dropdown'; import { @@ -24,7 +25,7 @@ import { selectedIdsFamily, } from './atoms'; import type { SelectableTreeNode, TreeNode } from './common'; -import { computeSideForDragMove, equalSubtree, getSelectedItems, hasAncestor } from './common'; +import { equalSubtree, getSelectedItems, hasAncestor } from './common'; import { TreeDragOverlay } from './TreeDragOverlay'; import type { TreeItemProps } from './TreeItem'; import type { TreeItemListProps } from './TreeItemList'; @@ -255,7 +256,7 @@ function TreeInner( } const node = selectableItem.node; - const side = computeSideForDragMove(node, e); + const side = computeSideForDragMove(node.item.id, e); const item = node.item; let hoveredParent = node.parent; diff --git a/src-web/components/core/tree/TreeItem.tsx b/src-web/components/core/tree/TreeItem.tsx index 5cb11a17..4a833e79 100644 --- a/src-web/components/core/tree/TreeItem.tsx +++ b/src-web/components/core/tree/TreeItem.tsx @@ -5,13 +5,13 @@ import { useAtomValue } from 'jotai'; import { selectAtom } from 'jotai/utils'; import type { MouseEvent, PointerEvent } from 'react'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { computeSideForDragMove } from '../../../lib/dnd'; import { jotaiStore } from '../../../lib/jotai'; import type { ContextMenuProps, DropdownItem } from '../Dropdown'; import { ContextMenu } from '../Dropdown'; import { Icon } from '../Icon'; import { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily } from './atoms'; import type { TreeNode } from './common'; -import { computeSideForDragMove } from './common'; import type { TreeProps } from './Tree'; import { TreeIndentGuide } from './TreeIndentGuide'; @@ -161,7 +161,7 @@ function TreeItem_({ clearDropHover(); }, onDragMove(e: DragMoveEvent) { - const side = computeSideForDragMove(node, e); + const side = computeSideForDragMove(node.item.id, e); const isFolder = node.children != null; const hasChildren = (node.children?.length ?? 0) > 0; const isCollapsed = jotaiStore.get(isCollapsedFamily({ treeId, itemId: node.item.id })); diff --git a/src-web/components/core/tree/common.ts b/src-web/components/core/tree/common.ts index d2056dda..9cf6569d 100644 --- a/src-web/components/core/tree/common.ts +++ b/src-web/components/core/tree/common.ts @@ -1,8 +1,7 @@ -import type { DragMoveEvent } from '@dnd-kit/core'; import { jotaiStore } from '../../../lib/jotai'; import { selectedIdsFamily } from './atoms'; -export interface TreeNode { +export interface TreeNode { children?: TreeNode[]; item: T; parent: TreeNode | null; @@ -48,25 +47,3 @@ export function hasAncestor(node: TreeNode, ancesto // Check parents recursively return hasAncestor(node.parent, ancestorId); } - -export function computeSideForDragMove( - node: TreeNode, - e: DragMoveEvent, -): 'above' | 'below' | null { - if (e.over == null || e.over.id !== node.item.id) { - return null; - } - if (e.active.rect.current.initial == null) return null; - - const overRect = e.over.rect; - const activeTop = - e.active.rect.current.translated?.top ?? e.active.rect.current.initial.top + e.delta.y; - const pointerY = activeTop + e.active.rect.current.initial.height / 2; - - const hoverTop = overRect.top; - const hoverBottom = overRect.bottom; - const hoverMiddleY = (hoverBottom - hoverTop) / 2; - const hoverClientY = pointerY - hoverTop; - - return hoverClientY < hoverMiddleY ? 'above' : 'below'; -} diff --git a/src-web/components/graphql/GraphQLEditor.tsx b/src-web/components/graphql/GraphQLEditor.tsx index c0c19786..35ed37ea 100644 --- a/src-web/components/graphql/GraphQLEditor.tsx +++ b/src-web/components/graphql/GraphQLEditor.tsx @@ -13,7 +13,7 @@ import { Button } from '../core/Button'; import type { DropdownItem } from '../core/Dropdown'; import { Dropdown } from '../core/Dropdown'; import type { EditorProps } from '../core/Editor/Editor'; -import { Editor } from '../core/Editor/Editor'; +import { Editor } from '../core/Editor/LazyEditor'; import { FormattedError } from '../core/FormattedError'; import { Icon } from '../core/Icon'; import { Separator } from '../core/Separator'; diff --git a/src-web/components/responseViewers/EventStreamViewer.tsx b/src-web/components/responseViewers/EventStreamViewer.tsx index 266f0499..d993c403 100644 --- a/src-web/components/responseViewers/EventStreamViewer.tsx +++ b/src-web/components/responseViewers/EventStreamViewer.tsx @@ -9,7 +9,7 @@ import { AutoScroller } from '../core/AutoScroller'; import { Banner } from '../core/Banner'; import { Button } from '../core/Button'; import type { EditorProps } from '../core/Editor/Editor'; -import { Editor } from '../core/Editor/Editor'; +import { Editor } from '../core/Editor/LazyEditor'; import { Icon } from '../core/Icon'; import { InlineCode } from '../core/InlineCode'; import { Separator } from '../core/Separator'; diff --git a/src-web/components/responseViewers/PdfViewer.tsx b/src-web/components/responseViewers/PdfViewer.tsx index 0763165a..165db843 100644 --- a/src-web/components/responseViewers/PdfViewer.tsx +++ b/src-web/components/responseViewers/PdfViewer.tsx @@ -3,10 +3,19 @@ import 'react-pdf/dist/Page/AnnotationLayer.css'; import { convertFileSrc } from '@tauri-apps/api/core'; import './PdfViewer.css'; import type { PDFDocumentProxy } from 'pdfjs-dist'; -import React, { useRef, useState } from 'react'; -import { Document, Page } from 'react-pdf'; +import React, { lazy, useRef, useState } from 'react'; import { useContainerSize } from '../../hooks/useContainerQuery'; +const Document = lazy(() => import('react-pdf').then((m) => ({ default: m.Document }))); +const Page = lazy(() => import('react-pdf').then((m) => ({ default: m.Page }))); + +import('react-pdf').then(({ pdfjs }) => { + pdfjs.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url, + ).toString(); +}); + interface Props { bodyPath: string; } diff --git a/src-web/components/responseViewers/TextViewer.tsx b/src-web/components/responseViewers/TextViewer.tsx index d3ee59cb..ea918b23 100644 --- a/src-web/components/responseViewers/TextViewer.tsx +++ b/src-web/components/responseViewers/TextViewer.tsx @@ -7,7 +7,7 @@ import { useDebouncedValue } from '../../hooks/useDebouncedValue'; import { useFormatText } from '../../hooks/useFormatText'; import { useResponseBodyText } from '../../hooks/useResponseBodyText'; import type { EditorProps } from '../core/Editor/Editor'; -import { Editor } from '../core/Editor/Editor'; +import { Editor } from '../core/Editor/LazyEditor'; import { hyperlink } from '../core/Editor/hyperlink/extension'; import { IconButton } from '../core/IconButton'; import { Input } from '../core/Input'; diff --git a/src-web/lib/dnd.ts b/src-web/lib/dnd.ts new file mode 100644 index 00000000..61e1749e --- /dev/null +++ b/src-web/lib/dnd.ts @@ -0,0 +1,23 @@ +import type { DragMoveEvent } from '@dnd-kit/core'; + +export function computeSideForDragMove( + id: string, + e: DragMoveEvent, +): 'above' | 'below' | null { + if (e.over == null || e.over.id !== id) { + return null; + } + if (e.active.rect.current.initial == null) return null; + + const overRect = e.over.rect; + const activeTop = + e.active.rect.current.translated?.top ?? e.active.rect.current.initial.top + e.delta.y; + const pointerY = activeTop + e.active.rect.current.initial.height / 2; + + const hoverTop = overRect.top; + const hoverBottom = overRect.bottom; + const hoverMiddleY = (hoverBottom - hoverTop) / 2; + const hoverClientY = pointerY - hoverTop; + + return hoverClientY < hoverMiddleY ? 'above' : 'below'; +} diff --git a/src-web/main.tsx b/src-web/main.tsx index 33584ca3..527e32c0 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -10,13 +10,6 @@ import { initGlobalListeners } from './lib/initGlobalListeners'; import { jotaiStore } from './lib/jotai'; import { router } from './lib/router'; -import('react-pdf').then(({ pdfjs }) => { - pdfjs.GlobalWorkerOptions.workerSrc = new URL( - 'pdfjs-dist/build/pdf.worker.min.mjs', - import.meta.url, - ).toString(); -}); - // Hide decorations here because it doesn't work in Rust for some reason (bug?) const osType = type(); if (osType !== 'macos') { diff --git a/src-web/package.json b/src-web/package.json index 39bcfd86..1e77f4d4 100644 --- a/src-web/package.json +++ b/src-web/package.json @@ -23,10 +23,9 @@ "@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", - "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^5.76.1", - "@tanstack/react-router": "^1.120.3", - "@tanstack/react-virtual": "^3.13.8", + "@tanstack/react-query": "^5.90.5", + "@tanstack/react-router": "^1.133.13", + "@tanstack/react-virtual": "^3.13.12", "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.4.0", @@ -56,8 +55,6 @@ "parse-color": "^1.0.0", "react": "^19.1.0", "react-colorful": "^5.6.1", - "react-dnd": "^16.0.1", - "react-dnd-touch-backend": "^16.0.1", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", "react-pdf": "^10.0.1", @@ -86,6 +83,7 @@ "@types/whatwg-mimetype": "^3.0.2", "@vitejs/plugin-react": "^4.6.0", "autoprefixer": "^10.4.21", + "@tailwindcss/container-queries": "^0.1.1", "decompress": "^4.2.1", "eslint-plugin-react-refresh": "^0.4.20", "internal-ip": "^8.0.0", diff --git a/src-web/routes/__root.tsx b/src-web/routes/__root.tsx index ea2c0a73..6670e90f 100644 --- a/src-web/routes/__root.tsx +++ b/src-web/routes/__root.tsx @@ -3,36 +3,35 @@ import { createRootRoute, Outlet } from '@tanstack/react-router'; import { type } from '@tauri-apps/plugin-os'; import classNames from 'classnames'; import { Provider as JotaiProvider } from 'jotai'; -import { domAnimation, LazyMotion, MotionConfig } from 'motion/react'; -import React, { Suspense } from 'react'; -import { DndProvider } from 'react-dnd'; -import { TouchBackend } from 'react-dnd-touch-backend'; -import { Dialogs } from '../components/Dialogs'; +import { LazyMotion, MotionConfig } from 'motion/react'; +import React, { lazy, Suspense } from 'react'; import { GlobalHooks } from '../components/GlobalHooks'; import RouteError from '../components/RouteError'; -import { Toasts } from '../components/Toasts'; import { jotaiStore } from '../lib/jotai'; import { queryClient } from '../lib/queryClient'; +const Toasts = lazy(() => import('../components/Toasts').then((m) => ({ default: m.Toasts }))); +const Dialogs = lazy(() => import('../components/Dialogs').then((m) => ({ default: m.Dialogs }))); + export const Route = createRootRoute({ component: RouteComponent, errorComponent: RouteError, }); +const motionFeatures = () => import('framer-motion').then((mod) => mod.domAnimation); + function RouteComponent() { return ( - + - - - - - - - - + + + + + + diff --git a/src-web/vite.config.ts b/src-web/vite.config.ts index 50c2aa00..fc8236bb 100644 --- a/src-web/vite.config.ts +++ b/src-web/vite.config.ts @@ -1,5 +1,5 @@ // @ts-ignore -import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; +import { tanstackRouter } from '@tanstack/router-plugin/vite'; import react from '@vitejs/plugin-react'; import reactRefresh from 'eslint-plugin-react-refresh'; import { internalIpV4 } from 'internal-ip'; @@ -26,7 +26,8 @@ export default defineConfig(async () => ({ plugins: [ wasm(), reactRefresh.configs.vite, - TanStackRouterVite({ + tanstackRouter({ + target: 'react', routesDirectory: './routes', generatedRouteTree: './routeTree.gen.ts', autoCodeSplitting: true, @@ -44,6 +45,14 @@ export default defineConfig(async () => ({ build: { outDir: '../dist', emptyOutDir: true, + rollupOptions: { + output: { + // Make chunk names readable + chunkFileNames: 'assets/chunk-[name]-[hash].js', + entryFileNames: 'assets/entry-[name]-[hash].js', + assetFileNames: 'assets/asset-[name]-[hash][extname]', + }, + }, }, clearScreen: false, server: {