From 439af5e8f2a70a3ce41132ec046207639e3207d9 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 27 Oct 2023 10:57:07 -0700 Subject: [PATCH] Auto-expand URL bar height --- src-web/components/UrlBar.tsx | 8 +- src-web/components/core/Editor/Editor.css | 283 +++++++++++----------- src-web/components/core/Editor/Editor.tsx | 49 ++-- src-web/components/core/Input.tsx | 76 +++--- src-web/components/core/Stacks.tsx | 3 +- tailwind.config.cjs | 132 +++++----- 6 files changed, 284 insertions(+), 267 deletions(-) diff --git a/src-web/components/UrlBar.tsx b/src-web/components/UrlBar.tsx index d32fc82c..fb1f30ce 100644 --- a/src-web/components/UrlBar.tsx +++ b/src-web/components/UrlBar.tsx @@ -47,11 +47,11 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
} rightSlot={ @@ -72,7 +72,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa iconSize="sm" title="Send Request" type="submit" - className="w-8 mr-0.5" + className="!h-auto w-8 mr-0.5 my-0.5" icon={loading ? 'update' : 'paperPlane'} spin={loading} /> diff --git a/src-web/components/core/Editor/Editor.css b/src-web/components/core/Editor/Editor.css index 407174fd..d0e0cfce 100644 --- a/src-web/components/core/Editor/Editor.css +++ b/src-web/components/core/Editor/Editor.css @@ -1,203 +1,210 @@ .cm-wrapper { - @apply h-full overflow-hidden; + @apply h-full overflow-hidden; + + .cm-editor { + @apply w-full block text-base; + + * { + @apply cursor-text; + } + + &.cm-focused { + outline: none !important; + } + + .cm-content { + @apply py-0; + } + + .cm-line { + @apply text-gray-800 pl-1 pr-1.5; + } + + .cm-placeholder { + @apply text-placeholder; + } + + .cm-scroller { + /* Inherit line-height from outside */ + line-height: inherit; + } + + /* Don't show selection on blurred input */ + .cm-selectionBackground { + @apply bg-transparent; + } + &.cm-focused .cm-selectionBackground { + @apply bg-selection; + } + + /* Style gutters */ + .cm-gutters { + @apply border-0 text-gray-500/50; + + .cm-gutterElement { + @apply cursor-default; + } + } + + .placeholder-widget { + @apply text-xs text-gray-800 dark:text-gray-900 px-1 rounded cursor-default dark:shadow; + + /* NOTE: Background and border are translucent so we can see text selection through it */ + @apply bg-gray-300/40 border border-gray-300 border-opacity-40 hover:border-opacity-80; + + /* Bring above on hover */ + @apply hover:z-10 relative; + } + } + + &.cm-singleline { + .cm-editor { + @apply w-full h-auto; + } + + .cm-scroller { + @apply font-mono text-[0.8rem] overflow-hidden; + } + + .cm-line { + @apply px-2 overflow-hidden; + } + } + + &.cm-multiline { + &.cm-full-height { + @apply relative; + + .cm-editor { + @apply inset-0 absolute; + position: absolute !important; + } + } .cm-editor { - @apply w-full block text-base; - - * { - @apply cursor-text; - } - - &.cm-focused { - outline: none !important; - } - - .cm-content { - @apply py-0; - } - - .cm-line { - @apply text-gray-800 pl-1 pr-1.5; - } - - .cm-placeholder { - @apply text-placeholder; - } - - .cm-scroller { - /* Inherit line-height from outside */ - line-height: inherit; - } - - /* Don't show selection on blurred input */ - .cm-selectionBackground { - @apply bg-transparent; - } - &.cm-focused .cm-selectionBackground { - @apply bg-selection; - } - - /* Style gutters */ - .cm-gutters { - @apply border-0 text-gray-500/50; - - .cm-gutterElement { - @apply cursor-default; - } - } - - .placeholder-widget { - @apply text-xs text-gray-800 dark:text-gray-900 px-1 rounded cursor-default dark:shadow; - - /* NOTE: Background and border are translucent so we can see text selection through it */ - @apply bg-gray-300/40 border border-gray-300 border-opacity-40 hover:border-opacity-80; - - /* Bring above on hover */ - @apply hover:z-10 relative; - } + @apply h-full; } - &.cm-singleline { - .cm-editor { - @apply w-full h-auto; - } + .cm-scroller { + @apply font-mono text-[0.75rem]; - .cm-scroller { - @apply font-mono text-[0.8rem] overflow-hidden; - } - - .cm-line { - @apply px-2 overflow-hidden; - } - } - - &.cm-multiline { - &.cm-full-height { - @apply relative; - - .cm-editor { - @apply inset-0 absolute; - position: absolute !important; - } - } - - .cm-editor { - @apply h-full; - } - - .cm-scroller { - @apply font-mono text-[0.75rem]; - - /* + /* * Round corners or they'll stick out of the editor bounds of editor is rounded. * Could potentially be pushed up from the editor like we do with bg color but this * is probably fine. */ - @apply rounded-lg; - } + @apply rounded-lg; } + } } /* Obscure text for password fields */ .cm-wrapper.cm-obscure-text .cm-line { - -webkit-text-security: disc; + -webkit-text-security: disc; } .cm-editor .cm-gutterElement { - @apply flex items-center; - transition: color var(--transition-duration); + @apply flex items-center; + transition: color var(--transition-duration); } .cm-editor .fold-gutter-icon { - @apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 cursor-pointer rounded; + @apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 cursor-pointer rounded; } .cm-editor .fold-gutter-icon::after { - @apply block w-1.5 h-1.5 border-transparent -rotate-45 + @apply block w-1.5 h-1.5 border-transparent -rotate-45 border-l border-b border-l-[currentColor] border-b-[currentColor] content-['']; } .cm-editor .fold-gutter-icon[data-open] { - @apply pt-[0.38em] pl-[0.3em]; + @apply pt-[0.38em] pl-[0.3em]; } .cm-editor .fold-gutter-icon[data-open]::after { - @apply rotate-[-135deg]; + @apply rotate-[-135deg]; } .cm-editor .fold-gutter-icon:hover { - @apply text-gray-900 bg-gray-300/50; + @apply text-gray-900 bg-gray-300/50; } .cm-editor .cm-foldPlaceholder { - @apply px-2 border border-gray-400/50 bg-gray-300/50 cursor-default; - @apply hover:text-gray-800 hover:border-gray-400; + @apply px-2 border border-gray-400/50 bg-gray-300/50 cursor-default; + @apply hover:text-gray-800 hover:border-gray-400; } .cm-editor .cm-activeLineGutter { - @apply bg-transparent; + @apply bg-transparent; } .cm-wrapper:not(.cm-readonly) .cm-editor { - &.cm-focused .cm-activeLineGutter { - @apply text-gray-600; - } + &.cm-focused .cm-activeLineGutter { + @apply text-gray-600; + } - .cm-cursor { - @apply border-l-2 border-gray-800; - } + .cm-cursor { + @apply border-l-2 border-gray-800; + } } .cm-singleline .cm-editor { - .cm-content { - @apply h-full flex items-center; + .cm-content { + @apply h-full flex items-center; + + /* Break characters on line wrapping mode, useful for URL field. + * We can make this dynamic if we need it to be configurable later + */ + &.cm-lineWrapping { + @apply break-all; } + } } /* NOTE: Extra selector required to override default styles */ .cm-tooltip.cm-tooltip { - @apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-200 z-50 pointer-events-auto text-[0.75rem]; + @apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-200 z-50 pointer-events-auto text-[0.75rem]; - &.cm-completionInfo-right { - @apply ml-1 -mt-0.5 text-sm; + &.cm-completionInfo-right { + @apply ml-1 -mt-0.5 text-sm; + } + + &.cm-completionInfo-right-narrow { + @apply ml-1; + } + + * { + @apply transition-none; + } + + &.cm-tooltip-autocomplete { + & > ul { + @apply p-1 max-h-[40vh]; } - &.cm-completionInfo-right-narrow { - @apply ml-1; + & > ul > li { + @apply cursor-default px-2 rounded-sm text-gray-600 h-7 flex items-center; } - * { - @apply transition-none; + & > ul > li[aria-selected] { + @apply bg-highlight text-gray-900; } - &.cm-tooltip-autocomplete { - & > ul { - @apply p-1 max-h-[40vh]; - } - - & > ul > li { - @apply cursor-default px-2 rounded-sm text-gray-600 h-7 flex items-center; - } - - & > ul > li[aria-selected] { - @apply bg-highlight text-gray-900; - } - - .cm-completionIcon { - @apply text-sm flex items-center pb-0.5 flex-shrink-0; - } - - .cm-completionLabel { - @apply text-gray-700; - } - - .cm-completionDetail { - @apply ml-auto pl-6; - } + .cm-completionIcon { + @apply text-sm flex items-center pb-0.5 flex-shrink-0; } + + .cm-completionLabel { + @apply text-gray-700; + } + + .cm-completionDetail { + @apply ml-auto pl-6; + } + } } /* Add default icon. Needs low priority so it can be overwritten */ .cm-completionIcon::after { - content: '𝑥'; + content: '𝑥'; } diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 55ba0e6a..5ad37c3c 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -36,7 +36,7 @@ export interface EditorProps { onChange?: (value: string) => void; onFocus?: () => void; onBlur?: () => void; - onSubmit?: () => void; + onEnter?: () => void; singleLine?: boolean; wrapLines?: boolean; format?: (v: string) => string; @@ -58,7 +58,7 @@ const _Editor = forwardRef(function Editor( onChange, onFocus, onBlur, - onSubmit, + onEnter, className, singleLine, format, @@ -80,10 +80,10 @@ const _Editor = forwardRef(function Editor( }, [onChange]); // Use ref so we can update the onChange handler without re-initializing the editor - const handleSubmit = useRef(onSubmit); + const handleEnter = useRef(onEnter); useEffect(() => { - handleSubmit.current = onSubmit; - }, [onSubmit]); + handleEnter.current = onEnter; + }, [onEnter]); // Use ref so we can update the onChange handler without re-initializing the editor const handleFocus = useRef(onFocus); @@ -143,7 +143,12 @@ const _Editor = forwardRef(function Editor( let view: EditorView; try { const languageCompartment = new Compartment(); - const langExt = getLanguageExtension({ contentType, useTemplating, autocomplete, environment }); + const langExt = getLanguageExtension({ + contentType, + useTemplating, + autocomplete, + environment, + }); const state = EditorState.create({ doc: `${defaultValue ?? ''}`, @@ -155,7 +160,7 @@ const _Editor = forwardRef(function Editor( container, readOnly, singleLine, - onSubmit: handleSubmit, + onEnter: handleEnter, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, @@ -228,13 +233,13 @@ function getExtensions({ onChange, onFocus, onBlur, - onSubmit, + onEnter, }: Pick & { container: HTMLDivElement | null; onChange: MutableRefObject; onFocus: MutableRefObject; onBlur: MutableRefObject; - onSubmit: MutableRefObject; + onEnter: MutableRefObject; }) { // TODO: Ensure tooltips render inside the dialog if we are in one. const parent = @@ -253,19 +258,19 @@ function getExtensions({ : []), ...(singleLine ? [ - EditorView.domEventHandlers({ - focus: (_, view) => { - // select all text on focus, like a regular input does - view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } }); - }, - keydown: (e) => { - // Submit nearest form on enter if there is one - if (onSubmit != null && e.key === 'Enter') { - onSubmit.current?.(); - } - }, - }), - ] + EditorView.domEventHandlers({ + focus: (_, view) => { + // select all text on focus, like a regular input does + view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } }); + }, + keydown: (e) => { + // Submit nearest form on enter if there is one + if (onEnter != null && e.key === 'Enter') { + onEnter.current?.(); + } + }, + }), + ] : []), // Handle onFocus diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index 802930d2..e2317062 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -21,7 +21,7 @@ export type InputProps = Omit, 'onChange' | 'on defaultValue?: string; leftSlot?: ReactNode; rightSlot?: ReactNode; - size?: 'sm' | 'md'; + size?: 'sm' | 'md' | 'auto'; className?: string; placeholder?: string; autoFocus?: boolean; @@ -31,24 +31,24 @@ export type InputProps = Omit, 'onChange' | 'on export const Input = forwardRef(function Input( { - label, - type = 'text', - hideLabel, className, containerClassName, - labelClassName, - onChange, - placeholder, - size = 'md', - name, - leftSlot, - rightSlot, defaultValue, - validate, - require, - onFocus, - onBlur, forceUpdateKey, + hideLabel, + label, + labelClassName, + leftSlot, + name, + onBlur, + onChange, + onFocus, + placeholder, + require, + rightSlot, + size = 'md', + type = 'text', + validate, ...props }: InputProps, ref, @@ -70,10 +70,7 @@ export const Input = forwardRef(function Inp const id = `input-${name}`; const inputClassName = classNames( className, - '!bg-transparent min-w-0 h-full w-full focus:outline-none placeholder:text-placeholder', - // Bump things over if the slots are occupied - leftSlot && 'pl-0.5 -ml-2', - rightSlot && 'pr-0.5 -mr-2', + '!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder', ); const isValid = useMemo(() => { @@ -92,7 +89,7 @@ export const Input = forwardRef(function Inp const wrapperRef = useRef(null); - const handleSubmit = useCallback(() => { + const handleEnter = useCallback(() => { const form = wrapperRef.current?.closest('form'); if (!isValid || form == null) return; @@ -112,7 +109,7 @@ export const Input = forwardRef(function Inp {label} (function Inp !isValid && '!border-invalid', size === 'md' && 'h-md leading-md', size === 'sm' && 'h-sm leading-sm', + size === 'auto' && '!min-h-sm', )} > {leftSlot} - + + + {type === 'password' && ( & { as?: ComponentType | 'ul' | 'form'; space?: keyof typeof gapClasses; - alignItems?: 'start' | 'center'; + alignItems?: 'start' | 'center' | 'stretch'; justifyContent?: 'start' | 'center' | 'end' | 'between'; }; @@ -74,6 +74,7 @@ const BaseStack = forwardRef(function BaseStack( 'flex', alignItems === 'center' && 'items-center', alignItems === 'start' && 'items-start', + alignItems === 'stretch' && 'items-stretch', justifyContent === 'start' && 'justify-start', justifyContent === 'center' && 'justify-center', justifyContent === 'end' && 'justify-end', diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 1d29d1ce..cfa02192 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,89 +1,89 @@ -const plugin = require('tailwindcss/plugin') +const plugin = require('tailwindcss/plugin'); + +const height = { + xs: '1.75rem', + sm: '2.0rem', + md: '2.5rem', +}; /** @type {import("tailwindcss").Config} */ module.exports = { - darkMode: ["class", "[data-appearance=\"dark\"]"], - content: [ - "./index.html", - "./src-web/**/*.{html,js,jsx,ts,tsx}" - ], + darkMode: ['class', '[data-appearance="dark"]'], + content: ['./index.html', './src-web/**/*.{html,js,jsx,ts,tsx}'], theme: { extend: { opacity: { - "disabled": "0.3" + disabled: '0.3', }, fontSize: { - "xs": "0.8rem" - }, - height: { - "xs": "1.75rem", - "sm": "2.0rem", - "md": "2.5rem" + xs: '0.8rem', }, + height, + minHeight: height, lineHeight: { // HACK: Minus 2 to account for borders inside inputs - "xs": "calc(1.75rem - 2px)", - "sm": "calc(2.0rem - 2px)", - "md": "calc(2.5rem - 2px)" + xs: 'calc(1.75rem - 2px)', + sm: 'calc(2.0rem - 2px)', + md: 'calc(2.5rem - 2px)', }, }, fontFamily: { - "mono": ["JetBrains Mono", "Menlo", "monospace"], - "sans": [ - "Inter", - "-apple-system", - "BlinkMacSystemFont", - "Segoe UI", - "Roboto", - "Oxygen-Sans", - "Ubuntu", - "Cantarell", - "Helvetica Neue", - "sans-serif", - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", + mono: ['JetBrains Mono', 'Menlo', 'monospace'], + sans: [ + 'Inter', + '-apple-system', + 'BlinkMacSystemFont', + 'Segoe UI', + 'Roboto', + 'Oxygen-Sans', + 'Ubuntu', + 'Cantarell', + 'Helvetica Neue', + 'sans-serif', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', ], }, fontSize: { - '3xs': "0.6rem", - '2xs': "0.7rem", - xs: "0.8rem", - sm: "0.9rem", - base: "1rem", - xl: "1.25rem", - "2xl": "1.563rem", - "3xl": "1.953rem", - "4xl": "2.441rem", - "5xl": "3.052rem" + '3xs': '0.6rem', + '2xs': '0.7rem', + xs: '0.8rem', + sm: '0.9rem', + base: '1rem', + xl: '1.25rem', + '2xl': '1.563rem', + '3xl': '1.953rem', + '4xl': '2.441rem', + '5xl': '3.052rem', }, colors: { - selection: "hsl(var(--color-violet-500) / 0.4)", - focus: "hsl(var(--color-blue-500) / 0.6)", - invalid: "hsl(var(--color-red-500))", - highlight: "hsl(var(--color-gray-300) / 0.35)", - highlightSecondary: "hsl(var(--color-gray-300) / 0.2)", - transparent: "transparent", - white: "hsl(0 100% 100% / )", - black: "hsl(0 100% 0% / )", - placeholder: "hsl(var(--color-gray-400) / )", - red: color("red"), - orange: color("orange"), - yellow: color("yellow"), - gray: color("gray"), - blue: color("blue"), - green: color("green"), - pink: color("pink"), - violet: color("violet") - } + selection: 'hsl(var(--color-violet-500) / 0.4)', + focus: 'hsl(var(--color-blue-500) / 0.6)', + invalid: 'hsl(var(--color-red-500))', + highlight: 'hsl(var(--color-gray-300) / 0.35)', + highlightSecondary: 'hsl(var(--color-gray-300) / 0.2)', + transparent: 'transparent', + white: 'hsl(0 100% 100% / )', + black: 'hsl(0 100% 0% / )', + placeholder: 'hsl(var(--color-gray-400) / )', + red: color('red'), + orange: color('orange'), + yellow: color('yellow'), + gray: color('gray'), + blue: color('blue'), + green: color('green'), + pink: color('pink'), + violet: color('violet'), + }, }, plugins: [ - require("@tailwindcss/container-queries"), - plugin(function({ addVariant }) { - addVariant('hocus', ['&:hover', '&:focus-visible', '&.focus:focus']) - addVariant('focus-visible-or-class', ['&:focus-visible', '&.focus:focus']) - }) - ] + require('@tailwindcss/container-queries'), + plugin(function ({ addVariant }) { + addVariant('hocus', ['&:hover', '&:focus-visible', '&.focus:focus']); + addVariant('focus-visible-or-class', ['&:focus-visible', '&.focus:focus']); + }), + ], }; function color(name) { @@ -100,6 +100,6 @@ function color(name) { 800: `hsl(var(--color-${name}-800) / )`, 900: `hsl(var(--color-${name}-900) / )`, 950: `hsl(var(--color-${name}-950) / )`, - 1000: `hsl(var(--color-${name}-1000) / )` + 1000: `hsl(var(--color-${name}-1000) / )`, }; }