Better code splitting and removed final instances of react-dnd

This commit is contained in:
Gregory Schier
2025-10-19 08:16:56 -07:00
parent 8055b625d0
commit ba6163b6d8
32 changed files with 654 additions and 605 deletions

148
package-lock.json generated
View File

@@ -1969,24 +1969,6 @@
"node": ">=14" "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": { "node_modules/@replit/codemirror-emacs": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
@@ -2854,6 +2836,7 @@
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz",
"integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"tailwindcss": ">=3.2.0" "tailwindcss": ">=3.2.0"
@@ -2873,9 +2856,9 @@
} }
}, },
"node_modules/@tanstack/history": { "node_modules/@tanstack/history": {
"version": "1.121.34", "version": "1.133.3",
"resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.121.34.tgz", "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.133.3.tgz",
"integrity": "sha512-YL8dGi5ZU+xvtav2boRlw4zrRghkY6hvdcmHhA0RGSJ/CBgzv+cbADW9eYJLx74XMZvIQ1pp6VMbrpXnnM5gHA==", "integrity": "sha512-zFQnGdX0S4g5xRuS+95iiEXM+qlGvYG7ksmOKx7LaMv60lDWa0imR8/24WwXXvBWJT1KnwVdZcjvhCwz9IiJCw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -2886,9 +2869,9 @@
} }
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.83.0", "version": "5.90.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.5.tgz",
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==", "integrity": "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@@ -2896,12 +2879,12 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.83.0", "version": "5.90.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.5.tgz",
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==", "integrity": "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.83.0" "@tanstack/query-core": "5.90.5"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -2912,14 +2895,14 @@
} }
}, },
"node_modules/@tanstack/react-router": { "node_modules/@tanstack/react-router": {
"version": "1.127.3", "version": "1.133.13",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.127.3.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.133.13.tgz",
"integrity": "sha512-QprmWHJrGbEKXJiP7WZ+dilTJRc7nMbsFCUnfAUw8PsOYanhgvBkBwAU05YEo8WTIZ9atCc1R90hyzqbiBFkdA==", "integrity": "sha512-mVAj70mPOH/a60Hjlha3gHEWLFuE4kHeKau/AL5Xp6e5GtNk1JTRwN4sJ9QlSyLcClOUUtGfED1FoLj0D2W0Eg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/history": "1.121.34", "@tanstack/history": "1.133.3",
"@tanstack/react-store": "^0.7.0", "@tanstack/react-store": "^0.7.0",
"@tanstack/router-core": "1.127.3", "@tanstack/router-core": "1.133.13",
"isbot": "^5.1.22", "isbot": "^5.1.22",
"tiny-invariant": "^1.3.3", "tiny-invariant": "^1.3.3",
"tiny-warning": "^1.0.3" "tiny-warning": "^1.0.3"
@@ -2972,14 +2955,14 @@
} }
}, },
"node_modules/@tanstack/router-core": { "node_modules/@tanstack/router-core": {
"version": "1.127.3", "version": "1.133.13",
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.127.3.tgz", "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.133.13.tgz",
"integrity": "sha512-08JlfwsMIDkMyCQsRviMVBn0cVUzlNzkll4pZgf6QRSO1RASBsci5hMojcsdH0d/yXLH0FBJ6fINbj0ctBm63Q==", "integrity": "sha512-zZptdlS/wSkqozb07Y3zX5gas2OapJdjEG6/Id0e/twNefVdR4EY2TK/mgvyhHtKIpCxIcnZz/3opypgeQi9bg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/history": "1.121.34", "@tanstack/history": "1.133.3",
"@tanstack/store": "^0.7.0", "@tanstack/store": "^0.7.0",
"cookie-es": "^1.2.2", "cookie-es": "^2.0.0",
"seroval": "^1.3.2", "seroval": "^1.3.2",
"seroval-plugins": "^1.3.2", "seroval-plugins": "^1.3.2",
"tiny-invariant": "^1.3.3", "tiny-invariant": "^1.3.3",
@@ -5915,9 +5898,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie-es": { "node_modules/cookie-es": {
"version": "1.2.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/copy-descriptor": { "node_modules/copy-descriptor": {
@@ -6707,17 +6690,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/doctrine": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -9273,15 +9245,6 @@
"@babel/runtime": "^7.7.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": { "node_modules/hosted-git-info": {
"version": "2.8.9", "version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -14421,46 +14384,6 @@
"react-dom": ">=16.8.0" "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": { "node_modules/react-dom": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -14479,6 +14402,7 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-markdown": { "node_modules/react-markdown": {
@@ -14847,15 +14771,6 @@
"node": ">=8" "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": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -16907,6 +16822,7 @@
"version": "4.1.11", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
@@ -18864,7 +18780,7 @@
}, },
"packages/plugin-runtime-types": { "packages/plugin-runtime-types": {
"name": "@yaakapp/api", "name": "@yaakapp/api",
"version": "0.6.6", "version": "0.7.0",
"dependencies": { "dependencies": {
"@types/node": "^24.0.13" "@types/node": "^24.0.13"
}, },
@@ -19136,10 +19052,9 @@
"@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vscode-keymap": "^6.0.2", "@replit/codemirror-vscode-keymap": "^6.0.2",
"@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-query": "^5.90.5",
"@tanstack/react-query": "^5.76.1", "@tanstack/react-router": "^1.133.13",
"@tanstack/react-router": "^1.120.3", "@tanstack/react-virtual": "^3.13.12",
"@tanstack/react-virtual": "^3.13.8",
"@tauri-apps/api": "^2.8.0", "@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-dialog": "^2.4.0",
@@ -19169,8 +19084,6 @@
"parse-color": "^1.0.0", "parse-color": "^1.0.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
"react-dnd-touch-backend": "^16.0.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-pdf": "^10.0.1", "react-pdf": "^10.0.1",
@@ -19187,6 +19100,7 @@
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.8.0", "@lezer/generator": "^1.8.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e", "@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/router-plugin": "^1.127.5", "@tanstack/router-plugin": "^1.127.5",
"@types/node": "^24.0.13", "@types/node": "^24.0.13",

View File

@@ -21,7 +21,7 @@ import { resolvedModelName } from '../lib/resolvedModelName';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { Checkbox } from './core/Checkbox'; import { Checkbox } from './core/Checkbox';
import { DetailsBanner } from './core/DetailsBanner'; import { DetailsBanner } from './core/DetailsBanner';
import { Editor } from './core/Editor/Editor'; import { Editor } from './core/Editor/LazyEditor';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { Input } from './core/Input'; import { Input } from './core/Input';
import { Label } from './core/Label'; import { Label } from './core/Label';

View File

@@ -17,7 +17,7 @@ import { showDialog } from '../lib/dialog';
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from '../lib/pluralize';
import { Button } from './core/Button'; import { Button } from './core/Button';
import type { EditorProps } from './core/Editor/Editor'; 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 { FormattedError } from './core/FormattedError';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';

View File

@@ -15,7 +15,7 @@ import { copyToClipboard } from '../lib/copy';
import { AutoScroller } from './core/AutoScroller'; import { AutoScroller } from './core/AutoScroller';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { Editor } from './core/Editor/Editor'; import { Editor } from './core/Editor/LazyEditor';
import { HotKeyList } from './core/HotKeyList'; import { HotKeyList } from './core/HotKeyList';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';

View File

@@ -53,9 +53,6 @@ export function HeadersEditor({
disabled disabled
disableDrag disableDrag
className="py-1" className="py-1"
onChange={() => {}}
onEnd={() => {}}
onMove={() => {}}
pair={ensurePairId(pair)} pair={ensurePairId(pair)}
stateKey={null} stateKey={null}
nameAutocompleteFunctions nameAutocompleteFunctions

View File

@@ -4,7 +4,7 @@ import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames'; import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import type { CSSProperties } from 'react'; 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 { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { allRequestsAtom } from '../hooks/useAllRequests'; import { allRequestsAtom } from '../hooks/useAllRequests';
import { useAuthTab } from '../hooks/useAuthTab'; import { useAuthTab } from '../hooks/useAuthTab';
@@ -37,7 +37,7 @@ import { showToast } from '../lib/toast';
import { BinaryFileEditor } from './BinaryFileEditor'; import { BinaryFileEditor } from './BinaryFileEditor';
import { ConfirmLargeRequestBody } from './ConfirmLargeRequestBody'; import { ConfirmLargeRequestBody } from './ConfirmLargeRequestBody';
import { CountBadge } from './core/CountBadge'; 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 type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import type { Pair } from './core/PairEditor'; import type { Pair } from './core/PairEditor';
@@ -53,7 +53,10 @@ import { MarkdownEditor } from './MarkdownEditor';
import { RequestMethodDropdown } from './RequestMethodDropdown'; import { RequestMethodDropdown } from './RequestMethodDropdown';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor'; import { UrlParametersEditor } from './UrlParameterEditor';
import { GraphQLEditor } from './graphql/GraphQLEditor';
const GraphQLEditor = lazy(() =>
import('./graphql/GraphQLEditor').then((m) => ({ default: m.GraphQLEditor })),
);
interface Props { interface Props {
style: CSSProperties; style: CSSProperties;
@@ -405,12 +408,14 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
stateKey={`xml.${activeRequest.id}`} stateKey={`xml.${activeRequest.id}`}
/> />
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? ( ) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
<GraphQLEditor <Suspense>
forceUpdateKey={forceUpdateKey} <GraphQLEditor
baseRequest={activeRequest} forceUpdateKey={forceUpdateKey}
request={activeRequest} baseRequest={activeRequest}
onChange={handleBodyChange} request={activeRequest}
/> onChange={handleBodyChange}
/>
</Suspense>
) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? ( ) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? (
<FormUrlencodedEditor <FormUrlencodedEditor
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}

View File

@@ -1,7 +1,7 @@
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from '@yaakapp-internal/models';
import classNames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react'; import type { CSSProperties, ReactNode} from 'react';
import React, { useCallback, useMemo } from 'react'; import React, { Suspense , lazy, useCallback, useMemo } from 'react';
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from 'react-use';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse'; import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
@@ -28,12 +28,15 @@ import { CsvViewer } from './responseViewers/CsvViewer';
import { EventStreamViewer } from './responseViewers/EventStreamViewer'; import { EventStreamViewer } from './responseViewers/EventStreamViewer';
import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer'; import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer';
import { ImageViewer } from './responseViewers/ImageViewer'; import { ImageViewer } from './responseViewers/ImageViewer';
import { PdfViewer } from './responseViewers/PdfViewer';
import { SvgViewer } from './responseViewers/SvgViewer'; import { SvgViewer } from './responseViewers/SvgViewer';
import { VideoViewer } from './responseViewers/VideoViewer'; import { VideoViewer } from './responseViewers/VideoViewer';
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from './ErrorBoundary';
import { Button } from './core/Button'; import { Button } from './core/Button';
const PdfViewer = lazy(() =>
import('./responseViewers/PdfViewer').then((m) => ({ default: m.PdfViewer })),
);
interface Props { interface Props {
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
@@ -106,9 +109,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
)} )}
> >
{activeResponse == null ? ( {activeResponse == null ? (
<HotKeyList <HotKeyList hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']} />
hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']}
/>
) : ( ) : (
<div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1"> <div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1">
<HStack <HStack
@@ -161,41 +162,46 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
> >
<TabContent value={TAB_BODY}> <TabContent value={TAB_BODY}>
<ErrorBoundary name="Http Response Viewer"> <ErrorBoundary name="Http Response Viewer">
<ConfirmLargeResponse response={activeResponse}> <Suspense>
{activeResponse.state === 'initialized' ? ( <ConfirmLargeResponse response={activeResponse}>
<EmptyStateText> {activeResponse.state === 'initialized' ? (
<VStack space={3}> <EmptyStateText>
<HStack space={3}> <VStack space={3}>
<LoadingIcon className="text-text-subtlest" /> <HStack space={3}>
Sending Request <LoadingIcon className="text-text-subtlest" />
</HStack> Sending Request
<Button size="sm" variant="border" onClick={() => cancel.mutate()}>Cancel</Button> </HStack>
</VStack> <Button size="sm" variant="border" onClick={() => cancel.mutate()}>
</EmptyStateText> Cancel
) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? ( </Button>
<EmptyStateText>Empty </EmptyStateText> </VStack>
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? ( </EmptyStateText>
<EventStreamViewer response={activeResponse} /> ) : activeResponse.state === 'closed' &&
) : mimeType?.match(/^image\/svg/) ? ( activeResponse.contentLength === 0 ? (
<SvgViewer response={activeResponse} /> <EmptyStateText>Empty </EmptyStateText>
) : mimeType?.match(/^image/i) ? ( ) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
<EnsureCompleteResponse response={activeResponse} render={ImageViewer} /> <EventStreamViewer response={activeResponse} />
) : mimeType?.match(/^audio/i) ? ( ) : mimeType?.match(/^image\/svg/) ? (
<EnsureCompleteResponse response={activeResponse} render={AudioViewer} /> <SvgViewer response={activeResponse} />
) : mimeType?.match(/^video/i) ? ( ) : mimeType?.match(/^image/i) ? (
<EnsureCompleteResponse response={activeResponse} render={VideoViewer} /> <EnsureCompleteResponse response={activeResponse} render={ImageViewer} />
) : mimeType?.match(/pdf/i) ? ( ) : mimeType?.match(/^audio/i) ? (
<EnsureCompleteResponse response={activeResponse} render={PdfViewer} /> <EnsureCompleteResponse response={activeResponse} render={AudioViewer} />
) : mimeType?.match(/csv|tab-separated/i) ? ( ) : mimeType?.match(/^video/i) ? (
<CsvViewer className="pb-2" response={activeResponse} /> <EnsureCompleteResponse response={activeResponse} render={VideoViewer} />
) : ( ) : mimeType?.match(/pdf/i) ? (
<HTMLOrTextViewer <EnsureCompleteResponse response={activeResponse} render={PdfViewer} />
textViewerClassName="-mr-2 bg-surface" // Pull to the right ) : mimeType?.match(/csv|tab-separated/i) ? (
response={activeResponse} <CsvViewer className="pb-2" response={activeResponse} />
pretty={viewMode === 'pretty'} ) : (
/> <HTMLOrTextViewer
)} textViewerClassName="-mr-2 bg-surface" // Pull to the right
</ConfirmLargeResponse> response={activeResponse}
pretty={viewMode === 'pretty'}
/>
)}
</ConfirmLargeResponse>
</Suspense>
</ErrorBoundary> </ErrorBoundary>
</TabContent> </TabContent>
<TabContent value={TAB_HEADERS}> <TabContent value={TAB_HEADERS}>

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import type { EditorProps } from './core/Editor/Editor'; 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 { SegmentedControl } from './core/SegmentedControl';
import { Markdown } from './Markdown'; import { Markdown } from './Markdown';

View File

@@ -1,10 +1,11 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { FocusTrap } from 'focus-trap-react';
import * as m from 'motion/react-m'; import * as m from 'motion/react-m';
import type { ReactNode } from 'react'; import type { ReactNode} from 'react';
import React, { useRef } from 'react'; import React, { Suspense , lazy, useRef } from 'react';
import { Portal } from './Portal'; import { Portal } from './Portal';
const FocusTrap = lazy(() => import('focus-trap-react'));
interface Props { interface Props {
children: ReactNode; children: ReactNode;
portalName: string; portalName: string;
@@ -50,50 +51,52 @@ export function Overlay({
return ( return (
<Portal name={portalName}> <Portal name={portalName}>
{open && ( {open && (
<FocusTrap <Suspense>
focusTrapOptions={{ <FocusTrap
allowOutsideClick: true, // So we can still click toasts and things focusTrapOptions={{
delayInitialFocus: true, allowOutsideClick: true, // So we can still click toasts and things
fallbackFocus: () => containerRef.current!, // always have a target delayInitialFocus: true,
initialFocus: () => fallbackFocus: () => containerRef.current!, // always have a target
// Doing this explicitly seems to work better than the default behavior for some reason initialFocus: () =>
containerRef.current?.querySelector<HTMLElement>( // Doing this explicitly seems to work better than the default behavior for some reason
[ containerRef.current?.querySelector<HTMLElement>(
'a[href]', [
'input:not([disabled])', 'a[href]',
'select:not([disabled])', 'input:not([disabled])',
'textarea:not([disabled])', 'select:not([disabled])',
'button:not([disabled])', 'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])', 'button:not([disabled])',
'[contenteditable]:not([contenteditable="false"])', '[tabindex]:not([tabindex="-1"])',
].join(', '), '[contenteditable]:not([contenteditable="false"])',
) ?? undefined, ].join(', '),
}} ) ?? undefined,
> }}
<m.div
ref={containerRef}
tabIndex={-1}
className={classNames('fixed inset-0', zIndexes[zIndex])}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
> >
<div <m.div
aria-hidden ref={containerRef}
onClick={onClose} tabIndex={-1}
className={classNames( className={classNames('fixed inset-0', zIndexes[zIndex])}
'absolute inset-0', initial={{ opacity: 0 }}
variant === 'default' && 'bg-backdrop backdrop-blur-sm', animate={{ opacity: 1 }}
)} >
/> <div
aria-hidden
onClick={onClose}
className={classNames(
'absolute inset-0',
variant === 'default' && 'bg-backdrop backdrop-blur-sm',
)}
/>
{/* Show the draggable region at the top */} {/* Show the draggable region at the top */}
{/* TODO: Figure out tauri drag region and also make clickable still */} {/* TODO: Figure out tauri drag region and also make clickable still */}
{variant === 'default' && ( {variant === 'default' && (
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" /> <div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
)} )}
{children} {children}
</m.div> </m.div>
</FocusTrap> </FocusTrap>
</Suspense>
)} )}
</Portal> </Portal>
); );

View File

@@ -1,11 +1,10 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models'; import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import React from 'react'; import React, { lazy, Suspense } from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace'; import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance'; import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
import { useResolvedTheme } from '../../hooks/useResolvedTheme'; import { useResolvedTheme } from '../../hooks/useResolvedTheme';
import type { ButtonProps } from '../core/Button'; import type { ButtonProps } from '../core/Button';
import { Editor } from '../core/Editor/Editor';
import type { IconProps } from '../core/Icon'; import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon'; import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton'; import { IconButton } from '../core/IconButton';
@@ -13,6 +12,8 @@ import type { SelectProps } from '../core/Select';
import { Select } from '../core/Select'; import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks'; import { HStack, VStack } from '../core/Stacks';
const Editor = lazy(() => import('../core/Editor/Editor').then((m) => ({ default: m.Editor })));
const buttonColors: ButtonProps['color'][] = [ const buttonColors: ButtonProps['color'][] = [
'primary', 'primary',
'info', 'info',
@@ -144,17 +145,19 @@ export function SettingsTheme() {
/> />
))} ))}
</HStack> </HStack>
<Editor <Suspense>
defaultValue={[ <Editor
'let foo = { // Demo code editor', defaultValue={[
' foo: ("bar" || "baz" ?? \'qux\'),', 'let foo = { // Demo code editor',
' baz: [1, 10.2, null, false, true],', ' foo: ("bar" || "baz" ?? \'qux\'),',
'};', ' baz: [1, 10.2, null, false, true],',
].join('\n')} '};',
heightMode="auto" ].join('\n')}
language="javascript" heightMode="auto"
stateKey={null} language="javascript"
/> stateKey={null}
/>
</Suspense>
</VStack> </VStack>
</VStack> </VStack>
); );

View File

@@ -25,7 +25,7 @@ import { generateId } from '../lib/generateId';
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring'; import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from '../lib/resolvedModelName';
import { CountBadge } from './core/CountBadge'; 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 type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import type { Pair } from './core/PairEditor'; import type { Pair } from './core/PairEditor';

View File

@@ -17,7 +17,7 @@ import { copyToClipboard } from '../lib/copy';
import { AutoScroller } from './core/AutoScroller'; import { AutoScroller } from './core/AutoScroller';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { Editor } from './core/Editor/Editor'; import { Editor } from './core/Editor/LazyEditor';
import { HotKeyList } from './core/HotKeyList'; import { HotKeyList } from './core/HotKeyList';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { generateId } from '../../lib/generateId'; import { generateId } from '../../lib/generateId';
import { Editor } from './Editor/Editor'; import { Editor } from './Editor/LazyEditor';
import type { Pair, PairEditorProps, PairWithId } from './PairEditor'; import type { Pair, PairEditorProps, PairWithId } from './PairEditor';
type Props = PairEditorProps; type Props = PairEditorProps;

View File

@@ -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<EditorView, EditorProps>(function LazyEditor(props, ref) {
return (
<Suspense>
<Editor_ ref={ref} {...props} />
</Suspense>
);
});

View File

@@ -1,127 +1,245 @@
import type { Color } from '@yaakapp-internal/plugins'; import type { Color } from '@yaakapp-internal/plugins';
import classNames from 'classnames'; 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 type { CSSProperties, HTMLAttributes } from 'react';
import { memo } from 'react'; import { memo } from 'react';
const icons = { const icons = {
alert_triangle: lucide.AlertTriangleIcon, alert_triangle: AlertTriangleIcon,
archive: lucide.ArchiveIcon, archive: ArchiveIcon,
arrow_big_down_dash: lucide.ArrowBigDownDashIcon, arrow_big_down_dash: ArrowBigDownDashIcon,
arrow_big_left_dash: lucide.ArrowBigLeftDashIcon, arrow_big_left_dash: ArrowBigLeftDashIcon,
arrow_big_right: lucide.ArrowBigRightIcon, arrow_big_right: ArrowBigRightIcon,
arrow_big_right_dash: lucide.ArrowBigRightDashIcon, arrow_big_right_dash: ArrowBigRightDashIcon,
arrow_big_up_dash: lucide.ArrowBigUpDashIcon, arrow_big_up_dash: ArrowBigUpDashIcon,
arrow_down: lucide.ArrowDownIcon, arrow_down: ArrowDownIcon,
arrow_down_to_dot: lucide.ArrowDownToDotIcon, arrow_down_to_dot: ArrowDownToDotIcon,
arrow_down_to_line: lucide.ArrowDownToLineIcon, arrow_down_to_line: ArrowDownToLineIcon,
arrow_right_circle: lucide.ArrowRightCircleIcon, arrow_right_circle: ArrowRightCircleIcon,
arrow_up: lucide.ArrowUpIcon, arrow_up: ArrowUpIcon,
arrow_up_down: lucide.ArrowUpDownIcon, arrow_up_down: ArrowUpDownIcon,
arrow_up_from_dot: lucide.ArrowUpFromDotIcon, arrow_up_from_dot: ArrowUpFromDotIcon,
arrow_up_from_line: lucide.ArrowUpFromLineIcon, arrow_up_from_line: ArrowUpFromLineIcon,
badge_check: lucide.BadgeCheckIcon, badge_check: BadgeCheckIcon,
book_open_text: lucide.BookOpenText, book_open_text: BookOpenText,
box: lucide.BoxIcon, box: BoxIcon,
cake: lucide.CakeIcon, cake: CakeIcon,
chat: lucide.MessageSquare, chat: MessageSquare,
check: lucide.CheckIcon, check: CheckIcon,
check_circle: lucide.CheckCircleIcon, check_circle: CheckCircleIcon,
check_square_checked: lucide.SquareCheckIcon, check_square_checked: SquareCheckIcon,
check_square_unchecked: lucide.SquareIcon, check_square_unchecked: SquareIcon,
chevron_down: lucide.ChevronDownIcon, chevron_down: ChevronDownIcon,
chevron_left: lucide.ChevronLeftIcon, chevron_left: ChevronLeftIcon,
chevron_right: lucide.ChevronRightIcon, chevron_right: ChevronRightIcon,
circle_alert: lucide.CircleAlertIcon, circle_alert: CircleAlertIcon,
circle_dashed: lucide.CircleDashedIcon, circle_dashed: CircleDashedIcon,
circle_dollar_sign: lucide.CircleDollarSignIcon, circle_dollar_sign: CircleDollarSignIcon,
circle_fading_arrow_up: lucide.CircleFadingArrowUpIcon, circle_fading_arrow_up: CircleFadingArrowUpIcon,
clock: lucide.ClockIcon, clock: ClockIcon,
code: lucide.CodeIcon, code: CodeIcon,
columns_2: lucide.Columns2Icon, columns_2: Columns2Icon,
command: lucide.CommandIcon, command: CommandIcon,
cookie: lucide.CookieIcon, cookie: CookieIcon,
copy: lucide.CopyIcon, copy: CopyIcon,
copy_check: lucide.CopyCheck, copy_check: CopyCheck,
corner_right_up: lucide.CornerRightUpIcon, corner_right_up: CornerRightUpIcon,
credit_card: lucide.CreditCardIcon, credit_card: CreditCardIcon,
dot: lucide.DotIcon, dot: DotIcon,
download: lucide.DownloadIcon, download: DownloadIcon,
ellipsis: lucide.EllipsisIcon, ellipsis: EllipsisIcon,
expand: lucide.ExpandIcon, expand: ExpandIcon,
external_link: lucide.ExternalLinkIcon, external_link: ExternalLinkIcon,
eye: lucide.EyeIcon, eye: EyeIcon,
eye_closed: lucide.EyeOffIcon, eye_closed: EyeOffIcon,
file_code: lucide.FileCodeIcon, file_code: FileCodeIcon,
filter: lucide.FilterIcon, filter: FilterIcon,
flame: lucide.FlameIcon, flame: FlameIcon,
flask: lucide.FlaskConicalIcon, flask: FlaskConicalIcon,
folder: lucide.FolderIcon, folder: FolderIcon,
folder_code: lucide.FolderCodeIcon, folder_code: FolderCodeIcon,
folder_cog: lucide.FolderCogIcon, folder_cog: FolderCogIcon,
folder_git: lucide.FolderGitIcon, folder_git: FolderGitIcon,
folder_input: lucide.FolderInputIcon, folder_input: FolderInputIcon,
folder_open: lucide.FolderOpenIcon, folder_open: FolderOpenIcon,
folder_output: lucide.FolderOutputIcon, folder_output: FolderOutputIcon,
folder_symlink: lucide.FolderSymlinkIcon, folder_symlink: FolderSymlinkIcon,
folder_sync: lucide.FolderSyncIcon, folder_sync: FolderSyncIcon,
folder_up: lucide.FolderUpIcon, folder_up: FolderUpIcon,
git_branch: lucide.GitBranchIcon, git_branch: GitBranchIcon,
git_branch_plus: lucide.GitBranchPlusIcon, git_branch_plus: GitBranchPlusIcon,
git_commit: lucide.GitCommitIcon, git_commit: GitCommitIcon,
git_commit_vertical: lucide.GitCommitVerticalIcon, git_commit_vertical: GitCommitVerticalIcon,
git_fork: lucide.GitForkIcon, git_fork: GitForkIcon,
git_pull_request: lucide.GitPullRequestIcon, git_pull_request: GitPullRequestIcon,
grip_vertical: lucide.GripVerticalIcon, grip_vertical: GripVerticalIcon,
hand: lucide.HandIcon, hand: HandIcon,
help: lucide.CircleHelpIcon, help: CircleHelpIcon,
history: lucide.HistoryIcon, history: HistoryIcon,
house: lucide.HomeIcon, house: HomeIcon,
import: lucide.ImportIcon, import: ImportIcon,
info: lucide.InfoIcon, info: InfoIcon,
key_round: lucide.KeyRoundIcon, key_round: KeyRoundIcon,
keyboard: lucide.KeyboardIcon, keyboard: KeyboardIcon,
left_panel_hidden: lucide.PanelLeftOpenIcon, left_panel_hidden: PanelLeftOpenIcon,
left_panel_visible: lucide.PanelLeftCloseIcon, left_panel_visible: PanelLeftCloseIcon,
lock: lucide.LockIcon, lock: LockIcon,
lock_open: lucide.LockOpenIcon, lock_open: LockOpenIcon,
magic_wand: lucide.Wand2Icon, magic_wand: Wand2Icon,
merge: lucide.MergeIcon, merge: MergeIcon,
minus: lucide.MinusIcon, minus: MinusIcon,
minus_circle: lucide.MinusCircleIcon, minus_circle: MinusCircleIcon,
moon: lucide.MoonIcon, moon: MoonIcon,
more_vertical: lucide.MoreVerticalIcon, more_vertical: MoreVerticalIcon,
palette: lucide.PaletteIcon, palette: PaletteIcon,
paste: lucide.ClipboardPasteIcon, paste: ClipboardPasteIcon,
pencil: lucide.PencilIcon, pencil: PencilIcon,
pin: lucide.PinIcon, pin: PinIcon,
plug: lucide.Plug, plug: Plug,
plus: lucide.PlusIcon, plus: PlusIcon,
plus_circle: lucide.PlusCircleIcon, plus_circle: PlusCircleIcon,
puzzle: lucide.PuzzleIcon, puzzle: PuzzleIcon,
refresh: lucide.RefreshCwIcon, refresh: RefreshCwIcon,
rocket: lucide.RocketIcon, rocket: RocketIcon,
rows_2: lucide.Rows2Icon, rows_2: Rows2Icon,
save: lucide.SaveIcon, save: SaveIcon,
search: lucide.SearchIcon, search: SearchIcon,
send_horizontal: lucide.SendHorizonalIcon, send_horizontal: SendHorizonalIcon,
settings: lucide.SettingsIcon, settings: SettingsIcon,
shield: lucide.ShieldIcon, shield: ShieldIcon,
shield_check: lucide.ShieldCheckIcon, shield_check: ShieldCheckIcon,
shield_off: lucide.ShieldOffIcon, shield_off: ShieldOffIcon,
sparkles: lucide.SparklesIcon, sparkles: SparklesIcon,
square_terminal: lucide.SquareTerminalIcon, square_terminal: SquareTerminalIcon,
sun: lucide.SunIcon, sun: SunIcon,
table: lucide.TableIcon, table: TableIcon,
text: lucide.FileTextIcon, text: FileTextIcon,
trash: lucide.Trash2Icon, trash: Trash2Icon,
unpin: lucide.PinOffIcon, unpin: PinOffIcon,
update: lucide.RefreshCcwIcon, update: RefreshCcwIcon,
upload: lucide.UploadIcon, upload: UploadIcon,
variable: lucide.VariableIcon, variable: VariableIcon,
wrench: lucide.WrenchIcon, wrench: WrenchIcon,
x: lucide.XIcon, x: XIcon,
_unknown: lucide.ShieldAlertIcon, _unknown: ShieldAlertIcon,
empty: (props: HTMLAttributes<HTMLSpanElement>) => <div {...props} />, empty: (props: HTMLAttributes<HTMLSpanElement>) => <div {...props} />,
}; };

View File

@@ -1,4 +1,3 @@
import { EditorSelection } from '@codemirror/state';
import type { EditorView } from '@codemirror/view'; import type { EditorView } from '@codemirror/view';
import type { Color } from '@yaakapp-internal/plugins'; import type { Color } from '@yaakapp-internal/plugins';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -30,7 +29,7 @@ import { Button } from './Button';
import type { DropdownItem } from './Dropdown'; import type { DropdownItem } from './Dropdown';
import { Dropdown } from './Dropdown'; import { Dropdown } from './Dropdown';
import type { EditorProps } from './Editor/Editor'; import type { EditorProps } from './Editor/Editor';
import { Editor } from './Editor/Editor'; import { Editor } from './Editor/LazyEditor';
import type { IconProps } from './Icon'; import type { IconProps } from './Icon';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
@@ -161,11 +160,12 @@ const BaseInput = forwardRef<EditorView, InputProps>(function InputBase(
onFocus?.(); onFocus?.();
}, [onFocus, readOnly]); }, [onFocus, readOnly]);
const handleBlur = useCallback(() => { const handleBlur = useCallback(async () => {
setFocused(false); setFocused(false);
// Move selection to the end on blur // Move selection to the end on blur
const anchor = editorRef.current?.state.doc.length ?? 0;
editorRef.current?.dispatch({ editorRef.current?.dispatch({
selection: EditorSelection.single(editorRef.current.state.doc.length ), selection: { anchor, head: anchor },
}); });
onBlur?.(); onBlur?.();
}, [onBlur]); }, [onBlur]);

View File

@@ -1,4 +1,16 @@
import type { EditorView } from '@codemirror/view'; 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 classNames from 'classnames';
import { import {
forwardRef, forwardRef,
@@ -10,13 +22,12 @@ import {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables'; import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables';
import { useRandomKey } from '../../hooks/useRandomKey'; import { useRandomKey } from '../../hooks/useRandomKey';
import { useToggle } from '../../hooks/useToggle'; import { useToggle } from '../../hooks/useToggle';
import { languageFromContentType } from '../../lib/contentType'; import { languageFromContentType } from '../../lib/contentType';
import { showDialog } from '../../lib/dialog'; import { showDialog } from '../../lib/dialog';
import { computeSideForDragMove } from '../../lib/dnd';
import { showPrompt } from '../../lib/prompt'; import { showPrompt } from '../../lib/prompt';
import { DropMarker } from '../DropMarker'; import { DropMarker } from '../DropMarker';
import { SelectFile } from '../SelectFile'; import { SelectFile } from '../SelectFile';
@@ -25,8 +36,8 @@ import { Checkbox } from './Checkbox';
import type { DropdownItem } from './Dropdown'; import type { DropdownItem } from './Dropdown';
import { Dropdown } from './Dropdown'; import { Dropdown } from './Dropdown';
import type { EditorProps } from './Editor/Editor'; import type { EditorProps } from './Editor/Editor';
import { Editor } from './Editor/Editor';
import type { GenericCompletionConfig } from './Editor/genericCompletion'; import type { GenericCompletionConfig } from './Editor/genericCompletion';
import { Editor } from './Editor/LazyEditor';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import type { InputProps } from './Input'; import type { InputProps } from './Input';
@@ -108,6 +119,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
const [forceFocusNamePairId, setForceFocusNamePairId] = useState<string | null>(null); const [forceFocusNamePairId, setForceFocusNamePairId] = useState<string | null>(null);
const [forceFocusValuePairId, setForceFocusValuePairId] = useState<string | null>(null); const [forceFocusValuePairId, setForceFocusValuePairId] = useState<string | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [isDragging, setIsDragging] = useState<PairWithId | null>(null);
const [pairs, setPairs] = useState<PairWithId[]>([]); const [pairs, setPairs] = useState<PairWithId[]>([]);
const [showAll, toggleShowAll] = useToggle(false); const [showAll, toggleShowAll] = useToggle(false);
// NOTE: Use local force update key because we trigger an effect on forceUpdateKey change. If // NOTE: Use local force update key because we trigger an effect on forceUpdateKey change. If
@@ -158,33 +170,6 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
[onChange], [onChange],
); );
const handleMove = useCallback<PairEditorRowProps['onMove']>(
(id, side) => {
const dragIndex = pairs.findIndex((r) => r.id === id);
setHoveredIndex(side === 'above' ? dragIndex : dragIndex + 1);
},
[pairs],
);
const handleEnd = useCallback<PairEditorRowProps['onEnd']>(
(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( const handleChange = useCallback(
(pair: PairWithId) => (pair: PairWithId) =>
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))), setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
@@ -233,6 +218,55 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(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 ( return (
<div <div
className={classNames( className={classNames(
@@ -246,67 +280,82 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
'pt-0.5', 'pt-0.5',
)} )}
> >
{pairs.map((p, i) => { <DndContext
if (!showAll && i > MAX_INITIAL_PAIRS) return null; autoScroll
sensors={sensors}
onDragMove={onDragMove}
onDragEnd={onDragEnd}
onDragStart={onDragStart}
onDragCancel={onDragCancel}
collisionDetection={pointerWithin}
>
{pairs.map((p, i) => {
if (!showAll && i > MAX_INITIAL_PAIRS) return null;
const isLast = i === pairs.length - 1; const isLast = i === pairs.length - 1;
return ( return (
<Fragment key={p.id}> <Fragment key={p.id}>
{hoveredIndex === i && <DropMarker />} {hoveredIndex === i && <DropMarker />}
<PairEditorRow
allowFileValues={allowFileValues}
allowMultilineValues={allowMultilineValues}
className="py-1"
forcedEnvironmentId={forcedEnvironmentId}
forceFocusNamePairId={forceFocusNamePairId}
forceFocusValuePairId={forceFocusValuePairId}
forceUpdateKey={localForceUpdateKey}
index={i}
isLast={isLast}
isDraggingGlobal={!!isDragging}
nameAutocomplete={nameAutocomplete}
nameAutocompleteFunctions={nameAutocompleteFunctions}
nameAutocompleteVariables={nameAutocompleteVariables}
namePlaceholder={namePlaceholder}
nameValidate={nameValidate}
onChange={handleChange}
onDelete={handleDelete}
onFocusName={handleFocusName}
onFocusValue={handleFocusValue}
pair={p}
stateKey={stateKey}
valueAutocomplete={valueAutocomplete}
valueAutocompleteFunctions={valueAutocompleteFunctions}
valueAutocompleteVariables={valueAutocompleteVariables}
valuePlaceholder={valuePlaceholder}
valueType={valueType}
valueValidate={valueValidate}
/>
</Fragment>
);
})}
{!showAll && pairs.length > MAX_INITIAL_PAIRS && (
<Button onClick={toggleShowAll} variant="border" className="m-2" size="xs">
Show {pairs.length - MAX_INITIAL_PAIRS} More
</Button>
)}
<DragOverlay dropAnimation={null}>
{isDragging && (
<PairEditorRow <PairEditorRow
allowFileValues={allowFileValues}
allowMultilineValues={allowMultilineValues}
className="py-1"
forcedEnvironmentId={forcedEnvironmentId}
forceFocusNamePairId={forceFocusNamePairId}
forceFocusValuePairId={forceFocusValuePairId}
forceUpdateKey={localForceUpdateKey}
index={i}
isLast={isLast}
nameAutocomplete={nameAutocomplete}
nameAutocompleteFunctions={nameAutocompleteFunctions}
nameAutocompleteVariables={nameAutocompleteVariables}
namePlaceholder={namePlaceholder} namePlaceholder={namePlaceholder}
nameValidate={nameValidate}
onChange={handleChange}
onDelete={handleDelete}
onEnd={handleEnd}
onFocusName={handleFocusName}
onFocusValue={handleFocusValue}
onMove={handleMove}
pair={p}
stateKey={stateKey}
valueAutocomplete={valueAutocomplete}
valueAutocompleteFunctions={valueAutocompleteFunctions}
valueAutocompleteVariables={valueAutocompleteVariables}
valuePlaceholder={valuePlaceholder} valuePlaceholder={valuePlaceholder}
valueType={valueType} className="opacity-80"
valueValidate={valueValidate} pair={isDragging}
index={0}
stateKey={null}
/> />
</Fragment> )}
); </DragOverlay>
})} </DndContext>
{!showAll && pairs.length > MAX_INITIAL_PAIRS && (
<Button onClick={toggleShowAll} variant="border" className="m-2" size="xs">
Show {pairs.length - MAX_INITIAL_PAIRS} More
</Button>
)}
</div> </div>
); );
}); });
enum ItemTypes {
ROW = 'pair-row',
}
type PairEditorRowProps = { type PairEditorRowProps = {
className?: string; className?: string;
pair: PairWithId; pair: PairWithId;
forceFocusNamePairId?: string | null; forceFocusNamePairId?: string | null;
forceFocusValuePairId?: string | null; forceFocusValuePairId?: string | null;
onMove: (id: string, side: 'above' | 'below') => void; onChange?: (pair: PairWithId) => void;
onEnd: (id: string) => void;
onChange: (pair: PairWithId) => void;
onDelete?: (pair: PairWithId, focusPrevious: boolean) => void; onDelete?: (pair: PairWithId, focusPrevious: boolean) => void;
onFocusName?: (pair: PairWithId) => void; onFocusName?: (pair: PairWithId) => void;
onFocusValue?: (pair: PairWithId) => void; onFocusValue?: (pair: PairWithId) => void;
@@ -315,6 +364,7 @@ type PairEditorRowProps = {
disabled?: boolean; disabled?: boolean;
disableDrag?: boolean; disableDrag?: boolean;
index: number; index: number;
isDraggingGlobal?: boolean;
} & Pick< } & Pick<
PairEditorProps, PairEditorProps,
| 'allowFileValues' | 'allowFileValues'
@@ -352,12 +402,11 @@ export function PairEditorRow({
nameAutocompleteVariables, nameAutocompleteVariables,
namePlaceholder, namePlaceholder,
nameValidate, nameValidate,
isDraggingGlobal,
onChange, onChange,
onDelete, onDelete,
onEnd,
onFocusName, onFocusName,
onFocusValue, onFocusValue,
onMove,
pair, pair,
stateKey, stateKey,
valueAutocomplete, valueAutocomplete,
@@ -367,7 +416,6 @@ export function PairEditorRow({
valueType, valueType,
valueValidate, valueValidate,
}: PairEditorRowProps) { }: PairEditorRowProps) {
const ref = useRef<HTMLDivElement>(null);
const nameInputRef = useRef<EditorView>(null); const nameInputRef = useRef<EditorView>(null);
const valueInputRef = useRef<EditorView>(null); const valueInputRef = useRef<EditorView>(null);
@@ -388,29 +436,29 @@ export function PairEditorRow({
const handleDelete = useCallback(() => onDelete?.(pair, false), [onDelete, pair]); const handleDelete = useCallback(() => onDelete?.(pair, false), [onDelete, pair]);
const handleChangeEnabled = useMemo( const handleChangeEnabled = useMemo(
() => (enabled: boolean) => onChange({ ...pair, enabled }), () => (enabled: boolean) => onChange?.({ ...pair, enabled }),
[onChange, pair], [onChange, pair],
); );
const handleChangeName = useMemo( const handleChangeName = useMemo(
() => (name: string) => onChange({ ...pair, name }), () => (name: string) => onChange?.({ ...pair, name }),
[onChange, pair], [onChange, pair],
); );
const handleChangeValueText = useMemo( const handleChangeValueText = useMemo(
() => (value: string) => onChange({ ...pair, value, isFile: false }), () => (value: string) => onChange?.({ ...pair, value, isFile: false }),
[onChange, pair], [onChange, pair],
); );
const handleChangeValueFile = useMemo( const handleChangeValueFile = useMemo(
() => () =>
({ filePath }: { filePath: string | null }) => ({ filePath }: { filePath: string | null }) =>
onChange({ ...pair, value: filePath ?? '', isFile: true }), onChange?.({ ...pair, value: filePath ?? '', isFile: true }),
[onChange, pair], [onChange, pair],
); );
const handleChangeValueContentType = useMemo( const handleChangeValueContentType = useMemo(
() => (contentType: string) => onChange({ ...pair, contentType }), () => (contentType: string) => onChange?.({ ...pair, contentType }),
[onChange, pair], [onChange, pair],
); );
@@ -448,30 +496,8 @@ export function PairEditorRow({
[allowMultilineValues, handleDelete, handleEditMultiLineValue], [allowMultilineValues, handleDelete, handleEditMultiLineValue],
); );
const [, connectDrop] = useDrop<Pair>( const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({ id: pair.id });
{ const { setNodeRef: setDroppableRef } = useDroppable({ id: pair.id });
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],
);
// Filter out the current pair name // Filter out the current pair name
const valueAutocompleteVariablesFiltered = useMemo<EditorProps['autocompleteVariables']>(() => { const valueAutocompleteVariablesFiltered = useMemo<EditorProps['autocompleteVariables']>(() => {
@@ -482,12 +508,17 @@ export function PairEditorRow({
} }
}, [pair.name, valueAutocompleteVariables]); }, [pair.name, valueAutocompleteVariables]);
connectDrag(ref); const handleSetRef = useCallback(
connectDrop(ref); (n: HTMLDivElement | null) => {
setDraggableRef(n);
setDroppableRef(n);
},
[setDraggableRef, setDroppableRef],
);
return ( return (
<div <div
ref={ref} ref={handleSetRef}
className={classNames( className={classNames(
className, className,
'group grid grid-cols-[auto_auto_minmax(0,1fr)_auto]', 'group grid grid-cols-[auto_auto_minmax(0,1fr)_auto]',
@@ -505,6 +536,8 @@ export function PairEditorRow({
/> />
{!isLast && !disableDrag ? ( {!isLast && !disableDrag ? (
<div <div
{...attributes}
{...listeners}
className={classNames( className={classNames(
'py-2 h-7 w-4 flex items-center', 'py-2 h-7 w-4 flex items-center',
'justify-center opacity-0 group-hover:opacity-70', 'justify-center opacity-0 group-hover:opacity-70',
@@ -529,6 +562,7 @@ export function PairEditorRow({
hideLabel hideLabel
size="sm" size="sm"
containerClassName={classNames(isLast && 'border-dashed')} containerClassName={classNames(isLast && 'border-dashed')}
className={classNames(isDraggingGlobal && 'pointer-events-none')}
label="Name" label="Name"
name={`name[${index}]`} name={`name[${index}]`}
onFocus={handleFocusName} onFocus={handleFocusName}
@@ -541,13 +575,13 @@ export function PairEditorRow({
stateKey={`name.${pair.id}.${stateKey}`} stateKey={`name.${pair.id}.${stateKey}`}
disabled={disabled} disabled={disabled}
wrapLines={false} wrapLines={false}
readOnly={pair.readOnlyName} readOnly={pair.readOnlyName || isDraggingGlobal}
size="sm" size="sm"
required={!isLast && !!pair.enabled && !!pair.value} required={!isLast && !!pair.enabled && !!pair.value}
validate={nameValidate} validate={nameValidate}
forcedEnvironmentId={forcedEnvironmentId} forcedEnvironmentId={forcedEnvironmentId}
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
containerClassName={classNames(isLast && 'border-dashed')} containerClassName={classNames('bg-surface', isLast && 'border-dashed')}
defaultValue={pair.name} defaultValue={pair.name}
label="Name" label="Name"
name={`name[${index}]`} name={`name[${index}]`}
@@ -578,6 +612,7 @@ export function PairEditorRow({
containerClassName={classNames(isLast && 'border-dashed')} containerClassName={classNames(isLast && 'border-dashed')}
label="Value" label="Value"
name={`value[${index}]`} name={`value[${index}]`}
className={classNames(isDraggingGlobal && 'pointer-events-none')}
onFocus={handleFocusValue} onFocus={handleFocusValue}
placeholder={valuePlaceholder ?? 'value'} placeholder={valuePlaceholder ?? 'value'}
/> />
@@ -599,7 +634,8 @@ export function PairEditorRow({
wrapLines={false} wrapLines={false}
size="sm" size="sm"
disabled={disabled} disabled={disabled}
containerClassName={classNames(isLast && 'border-dashed')} readOnly={isDraggingGlobal}
containerClassName={classNames('bg-surface', isLast && 'border-dashed')}
validate={valueValidate} validate={valueValidate}
forcedEnvironmentId={forcedEnvironmentId} forcedEnvironmentId={forcedEnvironmentId}
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}

View File

@@ -73,7 +73,6 @@ export function Tabs({
className={classNames( className={classNames(
className, className,
'tabs-container', 'tabs-container',
'transform-gpu',
'h-full grid', 'h-full grid',
layout === 'horizontal' && 'grid-rows-1 grid-cols-[auto_minmax(0,1fr)]', layout === 'horizontal' && 'grid-rows-1 grid-cols-[auto_minmax(0,1fr)]',
layout === 'vertical' && 'grid-rows-[auto_minmax(0,1fr)] grid-cols-1', layout === 'vertical' && 'grid-rows-[auto_minmax(0,1fr)] grid-cols-1',

View File

@@ -1,13 +1,22 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties, KeyboardEvent, ReactNode } from 'react'; import type {
import React, { useRef, useState } from 'react'; CSSProperties,
KeyboardEvent,
ReactNode} from 'react';
import React, {
lazy,
Suspense,
useRef,
useState,
} from 'react';
import { generateId } from '../../lib/generateId'; import { generateId } from '../../lib/generateId';
import { Portal } from '../Portal';
const Portal = lazy(() => import('../Portal').then((m) => ({ default: m.Portal })));
export interface TooltipProps { export interface TooltipProps {
children: ReactNode; children: ReactNode;
content: ReactNode; content: ReactNode;
tabIndex?: number, tabIndex?: number;
size?: 'md' | 'lg'; size?: 'md' | 'lg';
} }
@@ -66,7 +75,7 @@ export function Tooltip({ children, content, tabIndex, size = 'md' }: TooltipPro
const id = useRef(`tooltip-${generateId()}`); const id = useRef(`tooltip-${generateId()}`);
return ( return (
<> <Suspense>
<Portal name="tooltip"> <Portal name="tooltip">
<div <div
ref={tooltipRef} ref={tooltipRef}
@@ -105,7 +114,7 @@ export function Tooltip({ children, content, tabIndex, size = 'md' }: TooltipPro
> >
{children} {children}
</span> </span>
</> </Suspense>
); );
} }

View File

@@ -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<number | null>(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 theres 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;
}

View File

@@ -14,6 +14,7 @@ import { forwardRef, memo, useCallback, useImperativeHandle, useMemo, useRef } f
import { useKey, useKeyPressEvent } from 'react-use'; import { useKey, useKeyPressEvent } from 'react-use';
import type { HotkeyAction, HotKeyOptions } from '../../../hooks/useHotKey'; import type { HotkeyAction, HotKeyOptions } from '../../../hooks/useHotKey';
import { useHotKey } from '../../../hooks/useHotKey'; import { useHotKey } from '../../../hooks/useHotKey';
import { computeSideForDragMove } from '../../../lib/dnd';
import { jotaiStore } from '../../../lib/jotai'; import { jotaiStore } from '../../../lib/jotai';
import type { ContextMenuProps } from '../Dropdown'; import type { ContextMenuProps } from '../Dropdown';
import { import {
@@ -24,7 +25,7 @@ import {
selectedIdsFamily, selectedIdsFamily,
} from './atoms'; } from './atoms';
import type { SelectableTreeNode, TreeNode } from './common'; import type { SelectableTreeNode, TreeNode } from './common';
import { computeSideForDragMove, equalSubtree, getSelectedItems, hasAncestor } from './common'; import { equalSubtree, getSelectedItems, hasAncestor } from './common';
import { TreeDragOverlay } from './TreeDragOverlay'; import { TreeDragOverlay } from './TreeDragOverlay';
import type { TreeItemProps } from './TreeItem'; import type { TreeItemProps } from './TreeItem';
import type { TreeItemListProps } from './TreeItemList'; import type { TreeItemListProps } from './TreeItemList';
@@ -255,7 +256,7 @@ function TreeInner<T extends { id: string }>(
} }
const node = selectableItem.node; const node = selectableItem.node;
const side = computeSideForDragMove(node, e); const side = computeSideForDragMove(node.item.id, e);
const item = node.item; const item = node.item;
let hoveredParent = node.parent; let hoveredParent = node.parent;

View File

@@ -5,13 +5,13 @@ import { useAtomValue } from 'jotai';
import { selectAtom } from 'jotai/utils'; import { selectAtom } from 'jotai/utils';
import type { MouseEvent, PointerEvent } from 'react'; import type { MouseEvent, PointerEvent } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { computeSideForDragMove } from '../../../lib/dnd';
import { jotaiStore } from '../../../lib/jotai'; import { jotaiStore } from '../../../lib/jotai';
import type { ContextMenuProps, DropdownItem } from '../Dropdown'; import type { ContextMenuProps, DropdownItem } from '../Dropdown';
import { ContextMenu } from '../Dropdown'; import { ContextMenu } from '../Dropdown';
import { Icon } from '../Icon'; import { Icon } from '../Icon';
import { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily } from './atoms'; import { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily } from './atoms';
import type { TreeNode } from './common'; import type { TreeNode } from './common';
import { computeSideForDragMove } from './common';
import type { TreeProps } from './Tree'; import type { TreeProps } from './Tree';
import { TreeIndentGuide } from './TreeIndentGuide'; import { TreeIndentGuide } from './TreeIndentGuide';
@@ -161,7 +161,7 @@ function TreeItem_<T extends { id: string }>({
clearDropHover(); clearDropHover();
}, },
onDragMove(e: DragMoveEvent) { onDragMove(e: DragMoveEvent) {
const side = computeSideForDragMove(node, e); const side = computeSideForDragMove(node.item.id, e);
const isFolder = node.children != null; const isFolder = node.children != null;
const hasChildren = (node.children?.length ?? 0) > 0; const hasChildren = (node.children?.length ?? 0) > 0;
const isCollapsed = jotaiStore.get(isCollapsedFamily({ treeId, itemId: node.item.id })); const isCollapsed = jotaiStore.get(isCollapsedFamily({ treeId, itemId: node.item.id }));

View File

@@ -1,8 +1,7 @@
import type { DragMoveEvent } from '@dnd-kit/core';
import { jotaiStore } from '../../../lib/jotai'; import { jotaiStore } from '../../../lib/jotai';
import { selectedIdsFamily } from './atoms'; import { selectedIdsFamily } from './atoms';
export interface TreeNode<T extends { id: string } > { export interface TreeNode<T extends { id: string }> {
children?: TreeNode<T>[]; children?: TreeNode<T>[];
item: T; item: T;
parent: TreeNode<T> | null; parent: TreeNode<T> | null;
@@ -48,25 +47,3 @@ export function hasAncestor<T extends { id: string }>(node: TreeNode<T>, ancesto
// Check parents recursively // Check parents recursively
return hasAncestor(node.parent, ancestorId); return hasAncestor(node.parent, ancestorId);
} }
export function computeSideForDragMove<T extends { id: string }>(
node: TreeNode<T>,
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';
}

View File

@@ -13,7 +13,7 @@ import { Button } from '../core/Button';
import type { DropdownItem } from '../core/Dropdown'; import type { DropdownItem } from '../core/Dropdown';
import { Dropdown } from '../core/Dropdown'; import { Dropdown } from '../core/Dropdown';
import type { EditorProps } from '../core/Editor/Editor'; 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 { FormattedError } from '../core/FormattedError';
import { Icon } from '../core/Icon'; import { Icon } from '../core/Icon';
import { Separator } from '../core/Separator'; import { Separator } from '../core/Separator';

View File

@@ -9,7 +9,7 @@ import { AutoScroller } from '../core/AutoScroller';
import { Banner } from '../core/Banner'; import { Banner } from '../core/Banner';
import { Button } from '../core/Button'; import { Button } from '../core/Button';
import type { EditorProps } from '../core/Editor/Editor'; 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 { Icon } from '../core/Icon';
import { InlineCode } from '../core/InlineCode'; import { InlineCode } from '../core/InlineCode';
import { Separator } from '../core/Separator'; import { Separator } from '../core/Separator';

View File

@@ -3,10 +3,19 @@ import 'react-pdf/dist/Page/AnnotationLayer.css';
import { convertFileSrc } from '@tauri-apps/api/core'; import { convertFileSrc } from '@tauri-apps/api/core';
import './PdfViewer.css'; import './PdfViewer.css';
import type { PDFDocumentProxy } from 'pdfjs-dist'; import type { PDFDocumentProxy } from 'pdfjs-dist';
import React, { useRef, useState } from 'react'; import React, { lazy, useRef, useState } from 'react';
import { Document, Page } from 'react-pdf';
import { useContainerSize } from '../../hooks/useContainerQuery'; 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 { interface Props {
bodyPath: string; bodyPath: string;
} }

View File

@@ -7,7 +7,7 @@ import { useDebouncedValue } from '../../hooks/useDebouncedValue';
import { useFormatText } from '../../hooks/useFormatText'; import { useFormatText } from '../../hooks/useFormatText';
import { useResponseBodyText } from '../../hooks/useResponseBodyText'; import { useResponseBodyText } from '../../hooks/useResponseBodyText';
import type { EditorProps } from '../core/Editor/Editor'; 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 { hyperlink } from '../core/Editor/hyperlink/extension';
import { IconButton } from '../core/IconButton'; import { IconButton } from '../core/IconButton';
import { Input } from '../core/Input'; import { Input } from '../core/Input';

23
src-web/lib/dnd.ts Normal file
View File

@@ -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';
}

View File

@@ -10,13 +10,6 @@ import { initGlobalListeners } from './lib/initGlobalListeners';
import { jotaiStore } from './lib/jotai'; import { jotaiStore } from './lib/jotai';
import { router } from './lib/router'; 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?) // Hide decorations here because it doesn't work in Rust for some reason (bug?)
const osType = type(); const osType = type();
if (osType !== 'macos') { if (osType !== 'macos') {

View File

@@ -23,10 +23,9 @@
"@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vscode-keymap": "^6.0.2", "@replit/codemirror-vscode-keymap": "^6.0.2",
"@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-query": "^5.90.5",
"@tanstack/react-query": "^5.76.1", "@tanstack/react-router": "^1.133.13",
"@tanstack/react-router": "^1.120.3", "@tanstack/react-virtual": "^3.13.12",
"@tanstack/react-virtual": "^3.13.8",
"@tauri-apps/api": "^2.8.0", "@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-dialog": "^2.4.0",
@@ -56,8 +55,6 @@
"parse-color": "^1.0.0", "parse-color": "^1.0.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
"react-dnd-touch-backend": "^16.0.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-pdf": "^10.0.1", "react-pdf": "^10.0.1",
@@ -86,6 +83,7 @@
"@types/whatwg-mimetype": "^3.0.2", "@types/whatwg-mimetype": "^3.0.2",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.6.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"@tailwindcss/container-queries": "^0.1.1",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"internal-ip": "^8.0.0", "internal-ip": "^8.0.0",

View File

@@ -3,36 +3,35 @@ import { createRootRoute, Outlet } from '@tanstack/react-router';
import { type } from '@tauri-apps/plugin-os'; import { type } from '@tauri-apps/plugin-os';
import classNames from 'classnames'; import classNames from 'classnames';
import { Provider as JotaiProvider } from 'jotai'; import { Provider as JotaiProvider } from 'jotai';
import { domAnimation, LazyMotion, MotionConfig } from 'motion/react'; import { LazyMotion, MotionConfig } from 'motion/react';
import React, { Suspense } from 'react'; import React, { lazy, Suspense } from 'react';
import { DndProvider } from 'react-dnd';
import { TouchBackend } from 'react-dnd-touch-backend';
import { Dialogs } from '../components/Dialogs';
import { GlobalHooks } from '../components/GlobalHooks'; import { GlobalHooks } from '../components/GlobalHooks';
import RouteError from '../components/RouteError'; import RouteError from '../components/RouteError';
import { Toasts } from '../components/Toasts';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
import { queryClient } from '../lib/queryClient'; 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({ export const Route = createRootRoute({
component: RouteComponent, component: RouteComponent,
errorComponent: RouteError, errorComponent: RouteError,
}); });
const motionFeatures = () => import('framer-motion').then((mod) => mod.domAnimation);
function RouteComponent() { function RouteComponent() {
return ( return (
<JotaiProvider store={jotaiStore}> <JotaiProvider store={jotaiStore}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<LazyMotion features={domAnimation}> <LazyMotion strict features={motionFeatures}>
<MotionConfig transition={{ duration: 0.1 }}> <MotionConfig transition={{ duration: 0.1 }}>
<DndProvider backend={TouchBackend} options={{ enableMouseEvents: true }}> <Suspense>
<Suspense> <Toasts />
<GlobalHooks /> <Dialogs />
<Toasts /> </Suspense>
<Dialogs /> <Layout />
<Layout /> <GlobalHooks />
</Suspense>
</DndProvider>
</MotionConfig> </MotionConfig>
</LazyMotion> </LazyMotion>
</QueryClientProvider> </QueryClientProvider>

View File

@@ -1,5 +1,5 @@
// @ts-ignore // @ts-ignore
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; import { tanstackRouter } from '@tanstack/router-plugin/vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import reactRefresh from 'eslint-plugin-react-refresh'; import reactRefresh from 'eslint-plugin-react-refresh';
import { internalIpV4 } from 'internal-ip'; import { internalIpV4 } from 'internal-ip';
@@ -26,7 +26,8 @@ export default defineConfig(async () => ({
plugins: [ plugins: [
wasm(), wasm(),
reactRefresh.configs.vite, reactRefresh.configs.vite,
TanStackRouterVite({ tanstackRouter({
target: 'react',
routesDirectory: './routes', routesDirectory: './routes',
generatedRouteTree: './routeTree.gen.ts', generatedRouteTree: './routeTree.gen.ts',
autoCodeSplitting: true, autoCodeSplitting: true,
@@ -44,6 +45,14 @@ export default defineConfig(async () => ({
build: { build: {
outDir: '../dist', outDir: '../dist',
emptyOutDir: true, 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, clearScreen: false,
server: { server: {