diff --git a/package-lock.json b/package-lock.json index 95ae0979..c0b1c5c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,8 @@ "@radix-ui/react-dropdown-menu": "^2.0.2", "@radix-ui/react-icons": "^1.2.0", "@radix-ui/react-popover": "1.0.3", + "@radix-ui/react-scroll-area": "^1.0.2", + "@radix-ui/react-separator": "^1.0.1", "@tanstack/react-query": "^4.24.10", "@tauri-apps/api": "^1.2.0", "classnames": "^2.3.2", @@ -50,6 +52,7 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.32.2", "postcss": "^8.4.21", + "postcss-nesting": "^11.2.1", "prettier": "^2.8.4", "tailwindcss": "^3.2.7", "typescript": "^4.6.4", @@ -559,6 +562,23 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@csstools/selector-specificity": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz", + "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4", + "postcss-selector-parser": "^6.0.10" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -1267,6 +1287,14 @@ "node": ">= 8" } }, + "node_modules/@radix-ui/number": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz", + "integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", @@ -1585,6 +1613,40 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.2.tgz", + "integrity": "sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.0", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.1.tgz", + "integrity": "sha512-uc6Izot0D8uVz6T2nSb/HI7OaxkeaD50GgKr3W6HORnbfGVrG7LWuy+g6Fd58n8wHbrRblSYJZEfcjgymMlJjw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", @@ -5484,6 +5546,26 @@ "postcss": "^8.2.14" } }, + "node_modules/postcss-nesting": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-11.2.1.tgz", + "integrity": "sha512-E6Jq74Jo/PbRAtZioON54NPhUNJYxVWhwxbweYl1vAoBYuGlDIts5yhtKiZFLvkvwT73e/9nFrW3oMqAtgG+GQ==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/postcss-selector-parser": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", @@ -7164,6 +7246,13 @@ "w3c-keyname": "^2.2.4" } }, + "@csstools/selector-specificity": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz", + "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==", + "dev": true, + "requires": {} + }, "@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -7639,6 +7728,14 @@ "fastq": "^1.6.0" } }, + "@radix-ui/number": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz", + "integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, "@radix-ui/primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", @@ -7888,6 +7985,32 @@ "@radix-ui/react-use-controllable-state": "1.0.0" } }, + "@radix-ui/react-scroll-area": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.2.tgz", + "integrity": "sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.0", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + } + }, + "@radix-ui/react-separator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.1.tgz", + "integrity": "sha512-uc6Izot0D8uVz6T2nSb/HI7OaxkeaD50GgKr3W6HORnbfGVrG7LWuy+g6Fd58n8wHbrRblSYJZEfcjgymMlJjw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, "@radix-ui/react-slot": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", @@ -10597,6 +10720,16 @@ "postcss-selector-parser": "^6.0.10" } }, + "postcss-nesting": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-11.2.1.tgz", + "integrity": "sha512-E6Jq74Jo/PbRAtZioON54NPhUNJYxVWhwxbweYl1vAoBYuGlDIts5yhtKiZFLvkvwT73e/9nFrW3oMqAtgG+GQ==", + "dev": true, + "requires": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + } + }, "postcss-selector-parser": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", diff --git a/package.json b/package.json index 07d8c090..17672729 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@radix-ui/react-dropdown-menu": "^2.0.2", "@radix-ui/react-icons": "^1.2.0", "@radix-ui/react-popover": "1.0.3", + "@radix-ui/react-scroll-area": "^1.0.2", + "@radix-ui/react-separator": "^1.0.1", "@tanstack/react-query": "^4.24.10", "@tauri-apps/api": "^1.2.0", "classnames": "^2.3.2", @@ -55,6 +57,7 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.32.2", "postcss": "^8.4.21", + "postcss-nesting": "^11.2.1", "prettier": "^2.8.4", "tailwindcss": "^3.2.7", "typescript": "^4.6.4", diff --git a/postcss.config.cjs b/postcss.config.cjs index 33ad091d..5de80456 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,6 +1,7 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, + plugins: [ + require('tailwindcss'), + require('autoprefixer'), + require('postcss-nesting') + ] } diff --git a/src-tauri/src/window_ext.rs b/src-tauri/src/window_ext.rs index 5cad9bff..c04a030a 100644 --- a/src-tauri/src/window_ext.rs +++ b/src-tauri/src/window_ext.rs @@ -1,7 +1,7 @@ use tauri::{Runtime, Window}; const TRAFFIC_LIGHT_OFFSET_X: f64 = 15.0; -const TRAFFIC_LIGHT_OFFSET_Y: f64 = 20.0; +const TRAFFIC_LIGHT_OFFSET_Y: f64 = 26.0; pub trait WindowExt { #[cfg(target_os = "macos")] diff --git a/src-web/App.tsx b/src-web/App.tsx index 4bf6d0a9..c463a17f 100644 --- a/src-web/App.tsx +++ b/src-web/App.tsx @@ -1,19 +1,17 @@ +import classnames from 'classnames'; import { useEffect, useState } from 'react'; -import Editor from './components/Editor/Editor'; -import { HStack, VStack } from './components/Stacks'; -import { WindowDragRegion } from './components/WindowDragRegion'; -import { Sidebar } from './components/Sidebar'; -import { UrlBar } from './components/UrlBar'; -import { Grid } from './components/Grid'; import { useParams } from 'react-router-dom'; +import { Grid } from './components/Grid'; +import { RequestPane } from './components/RequestPane'; +import { ResponsePane } from './components/ResponsePane'; +import { Sidebar } from './components/Sidebar'; +import { HStack } from './components/Stacks'; import { useDeleteRequest, useRequests, useRequestUpdate, useSendRequest, } from './hooks/useRequest'; -import { ResponsePane } from './components/ResponsePane'; -import { IconButton } from './components/IconButton'; type Params = { workspaceId: string; @@ -26,56 +24,19 @@ function App() { const { data: requests } = useRequests(workspaceId); const request = requests?.find((r) => r.id === p.requestId); - const updateRequest = useRequestUpdate(request ?? null); - const sendRequest = useSendRequest(request ?? null); - const deleteRequest = useDeleteRequest(request ?? null); - - useEffect(() => { - const listener = async (e: KeyboardEvent) => { - if (e.metaKey && (e.key === 'Enter' || e.key === 'r')) { - await sendRequest.mutate(); - } - }; - document.documentElement.addEventListener('keypress', listener); - return () => document.documentElement.removeEventListener('keypress', listener); - }, []); - const [screenWidth, setScreenWidth] = useState(window.innerWidth); useEffect(() => { - console.log('SCREEN WIDTH', document.documentElement.clientWidth); window.addEventListener('resize', () => setScreenWidth(window.innerWidth)); }, []); + const isH = screenWidth > 900; return (
{request && ( - 700 ? 2 : 1} rows={screenWidth > 700 ? 1 : 2}> - - - Test Request - deleteRequest.mutate()} /> - - - updateRequest.mutate({ method })} - onUrlChange={(url) => updateRequest.mutate({ url })} - sendRequest={sendRequest.mutate} - /> - updateRequest.mutate({ body })} - /> - - - + + + )}
diff --git a/src-web/components/Button.tsx b/src-web/components/Button.tsx index ab745cc1..1ff46405 100644 --- a/src-web/components/Button.tsx +++ b/src-web/components/Button.tsx @@ -8,14 +8,22 @@ import type { import { forwardRef } from 'react'; import { Icon } from './Icon'; -export interface ButtonProps - extends ButtonHTMLAttributes { - color?: 'primary' | 'secondary' | 'warning' | 'danger'; +const colorStyles = { + default: 'hover:bg-gray-500/10 text-gray-600', + gray: 'bg-gray-50 text-gray-800 hover:bg-gray-500/10', + primary: 'bg-blue-400', + secondary: 'bg-violet-400', + warning: 'bg-orange-400', + danger: 'bg-red-400', +}; + +export type ButtonProps = ButtonHTMLAttributes & { + color?: keyof typeof colorStyles; size?: 'xs' | 'sm' | 'md'; justify?: 'start' | 'center'; forDropdown?: boolean; as?: T; -} +}; export const Button = forwardRef(function Button( { @@ -37,18 +45,14 @@ export const Button = forwardRef(function Button( type="button" className={classnames( className, - 'rounded-md flex items-center bg-opacity-80 hover:bg-opacity-100 text-white', + 'transition-all rounded-md flex items-center bg-opacity-80 hover:bg-opacity-100 hover:text-white', // 'active:translate-y-[0.5px] active:scale-[0.99]', + colorStyles[color || 'default'], justify === 'start' && 'justify-start', justify === 'center' && 'justify-center', size === 'md' && 'h-10 px-4', size === 'sm' && 'h-8 px-3 text-sm', - size === 'xs' && 'h-7 px-3 text-sm', - color === undefined && 'hover:bg-gray-500/[0.1]', - color === 'primary' && 'bg-blue-400', - color === 'secondary' && 'bg-violet-400', - color === 'warning' && 'bg-orange-400', - color === 'danger' && 'bg-red-400', + size === 'xs' && 'h-6 px-3 text-xs', )} {...props} > diff --git a/src-web/components/Dialog.tsx b/src-web/components/Dialog.tsx index 9afe465e..4f68cbb3 100644 --- a/src-web/components/Dialog.tsx +++ b/src-web/components/Dialog.tsx @@ -29,25 +29,27 @@ export function Dialog({ ('#radix-portal')}> - -
- - - - - - {title} - - {description && {description}} -
{children}
-
+ +
+
+ + + + + + {title} + + {description && {description}} +
{children}
+
+
diff --git a/src-web/components/Divider.tsx b/src-web/components/Divider.tsx new file mode 100644 index 00000000..0e1a194e --- /dev/null +++ b/src-web/components/Divider.tsx @@ -0,0 +1,23 @@ +import * as Separator from '@radix-ui/react-separator'; +import classnames from 'classnames'; + +interface Props { + orientation?: 'horizontal' | 'vertical'; + decorative?: boolean; + className?: string; +} + +export function Divider({ className, orientation = 'horizontal', decorative }: Props) { + return ( + + ); +} diff --git a/src-web/components/Dropdown.tsx b/src-web/components/Dropdown.tsx index d6eb4b80..cba04ac6 100644 --- a/src-web/components/Dropdown.tsx +++ b/src-web/components/Dropdown.tsx @@ -264,7 +264,11 @@ function DropdownMenuSeparator({ className, ...props }: D.DropdownMenuSeparatorP function DropdownMenuTrigger({ children, className, ...props }: D.DropdownMenuTriggerProps) { return ( - + {children} ); diff --git a/src-web/components/Editor/Editor.css b/src-web/components/Editor/Editor.css index a77f201f..03e0d573 100644 --- a/src-web/components/Editor/Editor.css +++ b/src-web/components/Editor/Editor.css @@ -5,45 +5,62 @@ } .cm-wrapper .cm-editor { + @apply inset-0; position: absolute !important; - left: 0; - right: 0; - top: 0; - bottom: 0; + font-size: 0.85em; } .cm-editor { @apply w-full block; + + &.cm-focused { + outline: none !important; + } + + .cm-line { + @apply text-gray-900 pl-1 pr-1.5; + } + + .cm-placeholder { + @apply text-placeholder; + } + + .placeholder-widget { + @apply text-xs text-white/90 bg-blue-400/80 py-[1px] px-1 mx-[1px] rounded cursor-default hover:bg-blue-400 hover:text-white; + text-shadow: 0 0 1px rgba(0, 0, 0, 0.9); + } } -.cm-singleline .cm-scroller { - overflow: hidden !important;; + +.cm-singleline { + .cm-editor { + @apply h-full w-full; + } + + .cm-scroller { + font-family: inherit; + overflow: hidden !important;; + } + + .cm-line { + @apply px-0; + } } -.cm-editor .placeholder-widget { - @apply text-xs text-white bg-blue-400 py-[1px] px-1 mx-[1px] rounded cursor-default hover:bg-blue-500; - text-shadow: 0 0 1px rgba(0, 0, 0, 0.9); +.cm-multiline { + .cm-editor { + @apply h-full; + } + + .cm-scroller { + @apply rounded; + } } -.cm-multiline .cm-editor .cm-scroller { - @apply rounded-lg bg-gray-50; -} - -.cm-editor.cm-focused { - outline: none !important; -} .cm-multiline .cm-editor.cm-focused .cm-scroller { - box-shadow: 0 0 0 1px hsl(var(--color-blue-400)/0.4); -} - -.cm-editor .cm-line { - color: hsl(var(--color-gray-900)); -} - -.cm-multiline .cm-editor .cm-line { - padding-left: 1em; - padding-right: 1.5em; + /* Active border state if we want it */ + /*box-shadow: 0 0 0 1px hsl(var(--color-blue-400)/0.4);*/ } .cm-singleline .cm-editor .cm-scroller { @@ -52,7 +69,8 @@ } .cm-editor .cm-gutters { - @apply bg-gray-50 border-r-0 text-gray-200; + /*@apply bg-gray-50 border-r-0 text-gray-200;*/ + @apply bg-transparent border-0 text-gray-200; } .cm-editor .cm-gutterElement { @@ -113,39 +131,56 @@ @apply bg-gray-200; } -/* --> Add padding to container. For some reason, using padding on both adds an extra - * 1px offset so we need to use a combination of padding and margin. - */ -.cm-editor .cm-gutters { - @apply pt-1; +.cm-singleline .cm-editor { + .cm-content { + @apply h-full flex items-center; + } } -.cm-editor .cm-content { - @apply mt-1; +.cm-scroller { + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar { + @apply w-[5px] h-[5px] bg-transparent; + } + + &::-webkit-scrollbar-thumb { + @apply bg-gray-100 bg-opacity-30 rounded-full; + } +} + +.cm-editor.cm-focused .cm-scroller::-webkit-scrollbar-thumb { + @apply bg-opacity-80; } /* <-- */ +/* NOTE: Extra selector required to override default styles */ .cm-tooltip.cm-tooltip { - @apply shadow-lg bg-background rounded overflow-hidden text-gray-900 border border-gray-100/70 z-50; -} + @apply shadow-lg bg-background rounded overflow-hidden text-gray-900 border border-gray-100/70 z-50 pointer-events-auto; -.cm-tooltip.cm-tooltip * { - @apply transition-none; -} + * { + @apply transition-none; + } -.cm-tooltip.cm-tooltip.cm-tooltip-autocomplete > ul { - @apply p-1 max-h-[40vh]; -} + &.cm-tooltip-autocomplete { + & > ul { + @apply p-1 max-h-[40vh]; + } -.cm-tooltip.cm-tooltip.cm-tooltip-autocomplete > ul > li { - @apply cursor-default py-1 px-2 rounded-sm text-gray-500; -} + & > ul > li { + @apply cursor-default px-2 rounded-sm text-gray-500 h-7 flex items-center; + } -.cm-tooltip.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected] { - @apply bg-gray-50 text-gray-800; -} + & > ul > li[aria-selected] { + @apply bg-gray-50 text-gray-800; + } -.cm-tooltip.cm-tooltip.cm-tooltip-autocomplete .cm-completionIcon { - @apply text-sm; + & > ul > li:hover { + @apply text-gray-700; + } + + .cm-completionIcon { + @apply text-sm flex items-center pb-0.5; + } + } } diff --git a/src-web/components/Editor/Editor.tsx b/src-web/components/Editor/Editor.tsx index 8e25415b..3babf33b 100644 --- a/src-web/components/Editor/Editor.tsx +++ b/src-web/components/Editor/Editor.tsx @@ -1,4 +1,5 @@ import { defaultKeymap } from '@codemirror/commands'; +import type { Extension } from '@codemirror/state'; import { Compartment, EditorState } from '@codemirror/state'; import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view'; import classnames from 'classnames'; @@ -9,29 +10,30 @@ import './Editor.css'; import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions'; import { singleLineExt } from './singleLine'; -interface Props extends Omit, 'onChange'> { - contentType: string; - valueKey?: string; +export interface EditorProps extends Omit, 'onChange'> { + contentType?: string; + autoFocus?: boolean; + valueKey?: string | number; + defaultValue?: string; placeholder?: string; tooltipContainer?: HTMLElement; useTemplating?: boolean; onChange?: (value: string) => void; - onSubmit?: () => void; singleLine?: boolean; } export default function Editor({ contentType, + autoFocus, placeholder, valueKey, useTemplating, defaultValue, onChange, - onSubmit, className, singleLine, ...props -}: Props) { +}: EditorProps) { const [cm, setCm] = useState<{ view: EditorView; langHolder: Compartment } | null>(null); const ref = useRef(null); const extensions = useMemo( @@ -39,7 +41,6 @@ export default function Editor({ getExtensions({ container: ref.current, placeholder, - onSubmit, singleLine, onChange, contentType, @@ -48,25 +49,23 @@ export default function Editor({ [contentType, ref.current], ); - const newState = (langHolder: Compartment) => { - const langExt = getLanguageExtension({ contentType, useTemplating }); - return EditorState.create({ - doc: `${defaultValue ?? ''}`, - extensions: [...extensions, langHolder.of(langExt)], - }); - }; - // Create codemirror instance when ref initializes useEffect(() => { if (ref.current === null) return; let view: EditorView | null = null; try { const langHolder = new Compartment(); + const langExt = getLanguageExtension({ contentType, useTemplating }); + const state = EditorState.create({ + doc: `${defaultValue ?? ''}`, + extensions: [...extensions, langHolder.of(langExt)], + }); view = new EditorView({ - state: newState(langHolder), + state, parent: ref.current, }); setCm({ view, langHolder }); + if (autoFocus && view) view.focus(); } catch (e) { console.log('Failed to initialize Codemirror', e); } @@ -108,17 +107,19 @@ function getExtensions({ singleLine, placeholder, onChange, - onSubmit, contentType, useTemplating, }: Pick< - Props, - 'singleLine' | 'onChange' | 'onSubmit' | 'contentType' | 'useTemplating' | 'placeholder' + EditorProps, + 'singleLine' | 'onChange' | 'contentType' | 'useTemplating' | 'placeholder' > & { container: HTMLDivElement | null }) { const ext = getLanguageExtension({ contentType, useTemplating }); - // TODO: This is a hack to get the tooltips to render in the correct place when inside a modal dialog - const parent = container?.closest('.dialog-content') ?? undefined; + // TODO: Ensure tooltips render inside the dialog if we are in one. + const parent = + container?.closest('[role="dialog"]') ?? + document.querySelector('#cm-portal') ?? + undefined; return [ ...baseExtensions, @@ -130,11 +131,15 @@ function getExtensions({ ...(placeholder ? [placeholderExt(placeholder)] : []), // Handle onSubmit - ...(onSubmit + ...(singleLine ? [ EditorView.domEventHandlers({ keydown: (e) => { - if (e.key === 'Enter') onSubmit?.(); + if (e.key === 'Enter') { + const el = e.currentTarget as HTMLElement; + const form = el.closest('form'); + form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true })); + } }, }), ] @@ -147,3 +152,24 @@ function getExtensions({ }), ]; } + +const newState = ({ + langHolder, + contentType, + useTemplating, + defaultValue, + extensions, +}: { + langHolder: Compartment; + contentType?: string; + useTemplating?: boolean; + defaultValue?: string; + extensions: Extension[]; +}) => { + console.log('NEW STATE', defaultValue); + const langExt = getLanguageExtension({ contentType, useTemplating }); + return EditorState.create({ + doc: `${defaultValue ?? ''}`, + extensions: [...extensions, langHolder.of(langExt)], + }); +}; diff --git a/src-web/components/Editor/extensions.ts b/src-web/components/Editor/extensions.ts index c098c57a..98d598b2 100644 --- a/src-web/components/Editor/extensions.ts +++ b/src-web/components/Editor/extensions.ts @@ -31,7 +31,6 @@ import { keymap, lineNumbers, rectangularSelection, - tooltips, } from '@codemirror/view'; import { tags as t } from '@lezer/highlight'; import { twig } from './twig/extension'; @@ -90,10 +89,10 @@ export function getLanguageExtension({ contentType, useTemplating, }: { - contentType: string; + contentType?: string; useTemplating?: boolean; }) { - const justContentType = contentType.split(';')[0] ?? contentType; + const justContentType = contentType?.split(';')[0] ?? contentType ?? ''; const base = syntaxExtensions[justContentType] ?? json(); if (!useTemplating) { return [base]; @@ -108,7 +107,7 @@ export const baseExtensions = [ drawSelection(), dropCursor(), bracketMatching(), - autocompletion({ closeOnBlur: true }), + autocompletion({ closeOnBlur: true, interactionDelay: 200 }), syntaxHighlighting(myHighlightStyle), EditorState.allowMultipleSelections.of(true), ]; diff --git a/src-web/components/Editor/twig/completion.ts b/src-web/components/Editor/twig/completion.ts index 751e12e2..8f9aae22 100644 --- a/src-web/components/Editor/twig/completion.ts +++ b/src-web/components/Editor/twig/completion.ts @@ -1,5 +1,4 @@ import type { CompletionContext } from '@codemirror/autocomplete'; -import { match } from 'assert'; const openTag = '${[ '; const closeTag = ' ]}'; @@ -18,7 +17,7 @@ const variables = [ ]; const MIN_MATCH_VAR = 2; -const MIN_MATCH_NAME = 2; +const MIN_MATCH_NAME = 4; export function completions(context: CompletionContext) { const toStartOfName = context.matchBefore(/\w*/); diff --git a/src-web/components/Grid.tsx b/src-web/components/Grid.tsx index 681fead3..7409e7ea 100644 --- a/src-web/components/Grid.tsx +++ b/src-web/components/Grid.tsx @@ -1,22 +1,25 @@ import classnames from 'classnames'; -import { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from 'react'; const colsClasses = { none: 'grid-cols-none', 1: 'grid-cols-1', 2: 'grid-cols-2', + 3: 'grid-cols-2', }; const rowsClasses = { none: 'grid-rows-none', 1: 'grid-rows-1', 2: 'grid-rows-2', + 3: 'grid-rows-2', }; const gapClasses = { 0: 'gap-0', 1: 'gap-1', 2: 'gap-2', + 3: 'gap-3', }; type Props = HTMLAttributes & { diff --git a/src-web/components/HeaderEditor.tsx b/src-web/components/HeaderEditor.tsx index a31b7049..81fb4c40 100644 --- a/src-web/components/HeaderEditor.tsx +++ b/src-web/components/HeaderEditor.tsx @@ -1,5 +1,5 @@ import type { FormEvent } from 'react'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import type { HttpHeader } from '../lib/models'; import { IconButton } from './IconButton'; import { Input } from './Input'; @@ -7,82 +7,107 @@ import { HStack, VStack } from './Stacks'; export function HeaderEditor() { const [headers, setHeaders] = useState([]); - const [newHeader, setNewHeader] = useState({ name: '', value: '' }); - const handleSubmit = (e?: FormEvent) => { - console.log('SUBMIT'); - e?.preventDefault(); - setHeaders([...headers, newHeader]); - setNewHeader({ name: '', value: '' }); - }; + const [newHeaderName, setNewHeaderName] = useState(''); + const [newHeaderValue, setNewHeaderValue] = useState(''); + const handleSubmit = useCallback( + (e?: FormEvent) => { + e?.preventDefault(); + setHeaders([...headers, { name: newHeaderName, value: newHeaderValue }]); + setNewHeaderName(''); + setNewHeaderValue(''); + }, + [newHeaderName, newHeaderValue], + ); + + const handleChangeHeader = useCallback( + (header: Partial, index: number) => { + setHeaders((headers) => + headers.map((h, i) => { + if (i === index) return h; + const newHeader: HttpHeader = { ...h, ...header }; + console.log('NEW HEADER', newHeader); + return newHeader; + }), + ); + }, + [headers], + ); const handleDelete = (index: number) => { setHeaders((headers) => headers.filter((_, i) => i !== index)); }; - const handleChangeHeader = (header: HttpHeader, index: number) => { - setHeaders((headers) => headers.map((h, i) => (i === index ? header : h))); - }; + console.log('HEADERS', headers); return (
{headers.map((header, i) => ( handleChangeHeader(h, i)} + key={`${headers.length}-${i}`} + valueKey={`${headers.length}-${i}`} + name={header.name} + value={header.value} + onChangeName={(name) => handleChangeHeader({ name }, i)} + onChangeValue={(value) => handleChangeHeader({ value }, i)} onDelete={() => handleDelete(i)} - onSubmit={handleSubmit} /> ))} - +
); } function FormRow({ - header, + autoFocus, + valueKey, + name, + value, addSubmit, - onChange, - onSubmit, + onChangeName, + onChangeValue, onDelete, }: { - header: HttpHeader; + autoFocus?: boolean; + valueKey: string | number; + name: string; + value: string; addSubmit?: boolean; onSubmit?: () => void; - onChange: (header: HttpHeader) => void; + onChangeName: (name: string) => void; + onChangeValue: (value: string) => void; onDelete?: () => void; }) { return (
{ - onChange({ name, value: header.value }); - }} + defaultValue={name} + onChange={onChangeName} /> { - onChange({ name: header.name, value }); - }} + defaultValue={value} + onChange={onChangeValue} /> {onDelete && } diff --git a/src-web/components/Icon.tsx b/src-web/components/Icon.tsx index 1e598582..63a480d4 100644 --- a/src-web/components/Icon.tsx +++ b/src-web/components/Icon.tsx @@ -3,7 +3,6 @@ import { CameraIcon, CheckIcon, CodeIcon, - Cross1Icon, Cross2Icon, EyeOpenIcon, GearIcon, diff --git a/src-web/components/Input.tsx b/src-web/components/Input.tsx index d777a2f0..1f8933cb 100644 --- a/src-web/components/Input.tsx +++ b/src-web/components/Input.tsx @@ -1,19 +1,22 @@ import classnames from 'classnames'; import type { InputHTMLAttributes, ReactNode } from 'react'; +import type { EditorProps } from './Editor/Editor'; import Editor from './Editor/Editor'; import { HStack, VStack } from './Stacks'; -interface Props extends Omit, 'size' | 'onChange'> { +interface Props + extends Omit< + InputHTMLAttributes, + 'size' | 'onChange' | 'onSubmit' | 'defaultValue' + > { name: string; label: string; hideLabel?: boolean; labelClassName?: string; containerClassName?: string; onChange?: (value: string) => void; - onSubmit?: () => void; - contentType?: string; - useTemplating?: boolean; - useEditor?: boolean; + useEditor?: Pick; + defaultValue?: string; leftSlot?: ReactNode; rightSlot?: ReactNode; size?: 'sm' | 'md'; @@ -25,13 +28,10 @@ export function Input({ className, containerClassName, labelClassName, - onSubmit, + onChange, placeholder, - useTemplating, size = 'md', useEditor, - contentType, - onChange, name, leftSlot, rightSlot, @@ -55,7 +55,7 @@ export function Input({ items="center" className={classnames( containerClassName, - 'relative w-full rounded-md overflow-hidden text-gray-900 bg-gray-200/10', + 'relative w-full rounded-md text-gray-900 bg-gray-200/10', 'border border-gray-500/10 focus-within:border-blue-400/40', size === 'md' && 'h-10', size === 'sm' && 'h-8', @@ -66,13 +66,15 @@ export function Input({ ) : ( +
{children}
+
+ ); +} diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx new file mode 100644 index 00000000..2c1b9335 --- /dev/null +++ b/src-web/components/RequestPane.tsx @@ -0,0 +1,69 @@ +import classnames from 'classnames'; +import { useDeleteRequest, useRequestUpdate, useSendRequest } from '../hooks/useRequest'; +import type { HttpRequest } from '../lib/models'; +import { Button } from './Button'; +import { Divider } from './Divider'; +import Editor from './Editor/Editor'; +import type { LayoutPaneProps } from './LayoutPane'; +import { LayoutPane } from './LayoutPane'; +import { ScrollArea } from './ScrollArea'; +import { HStack } from './Stacks'; +import { UrlBar } from './UrlBar'; + +interface Props extends LayoutPaneProps { + request: HttpRequest; +} + +export function RequestPane({ request, ...props }: Props) { + const updateRequest = useRequestUpdate(request ?? null); + const sendRequest = useSendRequest(request ?? null); + return ( + +
+ {/**/} + {/* Test Request*/} + {/* deleteRequest.mutate()} />*/} + {/**/} +
+ updateRequest.mutate({ method })} + onUrlChange={(url) => updateRequest.mutate({ url })} + sendRequest={sendRequest.mutate} + /> +
+ +
+
+ {/**/} + + + {['JSON', 'Params', 'Headers', 'Auth', 'Docs'].map((label, i) => ( + + ))} + + +
+ updateRequest.mutate({ body })} + /> +
+
+
+ ); +} diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 210427e0..ce49ac42 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -1,19 +1,20 @@ -import { motion } from 'framer-motion'; +import classnames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks/useResponses'; +import { Divider } from './Divider'; import { Dropdown } from './Dropdown'; import Editor from './Editor/Editor'; import { Icon } from './Icon'; import { IconButton } from './IconButton'; -import { HStack, VStack } from './Stacks'; -import { WindowDragRegion } from './WindowDragRegion'; +import type { LayoutPaneProps } from './LayoutPane'; +import { LayoutPane } from './LayoutPane'; +import { HStack } from './Stacks'; -interface Props { +interface Props extends LayoutPaneProps { requestId: string; - error: string | null; } -export function ResponsePane({ requestId, error }: Props) { +export function ResponsePane({ requestId, className, ...props }: Props) { const [activeResponseId, setActiveResponseId] = useState(null); const [viewMode, setViewMode] = useState<'pretty' | 'raw'>('pretty'); const responses = useResponses(requestId); @@ -22,7 +23,6 @@ export function ResponsePane({ requestId, error }: Props) { : responses.data[responses.data.length - 1]; const deleteResponse = useDeleteResponse(response); const deleteAllResponses = useDeleteAllResponses(response?.requestId); - error = response?.error ?? error; useEffect(() => { setActiveResponseId(null); @@ -44,49 +44,29 @@ export function ResponsePane({ requestId, error }: Props) { }, [response?.body, contentType]); return ( - - - ({ - label: r.status + ' - ' + r.elapsed + ' ms', - leftSlot: response?.id === r.id ? : <>, - onSelect: () => setActiveResponseId(r.id), - })), - ]} - > - - - - - - {error &&
{error}
} - {response && ( - <> + +
+ {/**/} + {/**/} + {response?.error && ( +
{response.error}
+ )} + {response && ( + <> +
-
- {response.updatedAt.toISOString()} -  •  +
{response.status} {response.statusReason && ` ${response.statusReason}`}  •  {response.elapsed}ms  •  {Math.round(response.body.length / 1000)} KB
+ {contentType.includes('html') && ( setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))} /> )} + ({ + label: r.status + ' - ' + r.elapsed + ' ms', + leftSlot: response?.id === r.id ? : <>, + onSelect: () => setActiveResponseId(r.id), + })), + ]} + > + + - {viewMode === 'pretty' && contentForIframe !== null ? ( -