Auto-expand URL bar height

This commit is contained in:
Gregory Schier
2023-10-27 10:57:07 -07:00
parent 356eaf1713
commit 9dafe4f704
6 changed files with 284 additions and 267 deletions

View File

@@ -47,11 +47,11 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
<form onSubmit={handleSubmit} className={classNames('url-bar', className)}>
<Input
ref={inputRef}
size="sm"
size="auto"
hideLabel
useTemplating
contentType="url"
className="px-0"
className="px-0 py-0.5"
name="url"
label="Enter URL"
forceUpdateKey={updateKey}
@@ -63,7 +63,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
<RequestMethodDropdown
method={method}
onChange={handleMethodChange}
className="mx-0.5 h-full my-1"
className="!h-auto mx-0.5 my-0.5"
/>
}
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}
/>

View File

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

View File

@@ -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<EditorView | undefined, EditorProps>(function Editor(
onChange,
onFocus,
onBlur,
onSubmit,
onEnter,
className,
singleLine,
format,
@@ -80,10 +80,10 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
}, [onChange]);
// Use ref so we can update the onChange handler without re-initializing the editor
const handleSubmit = useRef<EditorProps['onSubmit']>(onSubmit);
const handleEnter = useRef<EditorProps['onEnter']>(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<EditorProps['onFocus']>(onFocus);
@@ -143,7 +143,12 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(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<EditorView | undefined, EditorProps>(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<EditorProps, 'singleLine' | 'readOnly'> & {
container: HTMLDivElement | null;
onChange: MutableRefObject<EditorProps['onChange']>;
onFocus: MutableRefObject<EditorProps['onFocus']>;
onBlur: MutableRefObject<EditorProps['onBlur']>;
onSubmit: MutableRefObject<EditorProps['onSubmit']>;
onEnter: MutableRefObject<EditorProps['onEnter']>;
}) {
// 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

View File

@@ -21,7 +21,7 @@ export type InputProps = Omit<HTMLAttributes<HTMLInputElement>, '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<HTMLAttributes<HTMLInputElement>, 'onChange' | 'on
export const Input = forwardRef<EditorView | undefined, InputProps>(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<EditorView | undefined, InputProps>(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<EditorView | undefined, InputProps>(function Inp
const wrapperRef = useRef<HTMLDivElement>(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<EditorView | undefined, InputProps>(function Inp
{label}
</label>
<HStack
alignItems="center"
alignItems="stretch"
className={classNames(
containerClassName,
'relative w-full rounded-md text-gray-900',
@@ -121,24 +118,31 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
!isValid && '!border-invalid',
size === 'md' && 'h-md leading-md',
size === 'sm' && 'h-sm leading-sm',
size === 'auto' && '!min-h-sm',
)}
>
{leftSlot}
<Editor
ref={ref}
id={id}
singleLine
onSubmit={handleSubmit}
type={type === 'password' && !obscured ? 'text' : type}
defaultValue={defaultValue}
forceUpdateKey={forceUpdateKey}
placeholder={placeholder}
onChange={handleChange}
className={inputClassName}
onFocus={handleFocus}
onBlur={handleBlur}
{...props}
/>
<HStack
alignItems="center"
className={classNames('w-full', leftSlot && 'pl-0.5 -ml-2', rightSlot && 'pr-0.5 -mr-2')}
>
<Editor
ref={ref}
id={id}
singleLine
wrapLines={size === 'auto'}
onEnter={handleEnter}
type={type === 'password' && !obscured ? 'text' : type}
defaultValue={defaultValue}
forceUpdateKey={forceUpdateKey}
placeholder={placeholder}
onChange={handleChange}
className={inputClassName}
onFocus={handleFocus}
onBlur={handleBlur}
{...props}
/>
</HStack>
{type === 'password' && (
<IconButton
title={obscured ? `Show ${label}` : `Obscure ${label}`}

View File

@@ -56,7 +56,7 @@ export const VStack = forwardRef(function VStack(
type BaseStackProps = HTMLAttributes<HTMLElement> & {
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',

View File

@@ -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% / <alpha-value>)",
black: "hsl(0 100% 0% / <alpha-value>)",
placeholder: "hsl(var(--color-gray-400) / <alpha-value>)",
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% / <alpha-value>)',
black: 'hsl(0 100% 0% / <alpha-value>)',
placeholder: 'hsl(var(--color-gray-400) / <alpha-value>)',
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) / <alpha-value>)`,
900: `hsl(var(--color-${name}-900) / <alpha-value>)`,
950: `hsl(var(--color-${name}-950) / <alpha-value>)`,
1000: `hsl(var(--color-${name}-1000) / <alpha-value>)`
1000: `hsl(var(--color-${name}-1000) / <alpha-value>)`,
};
}