Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/shared/src/components/dropdown/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.DropdownMenuSubContent {
user-select: none;
z-index: 1000;
@apply py-1 px-0 m-0 bg-background-subtle rounded-12 border border-border-subtlest-secondary shadow-2;
@apply p-1.5 m-0 bg-background-subtle rounded-14 border border-border-subtlest-secondary shadow-2;
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform, opacity;
Expand Down Expand Up @@ -71,7 +71,15 @@
.DropdownMenuSubTrigger {
border-radius: 0.625rem;
@apply text-text-tertiary;
@apply flex items-center typo-footnote h-7 px-2 py-0 truncate;
@apply flex items-center typo-footnote h-8 px-2.5 py-0 truncate;
transition: background-color 0.12s linear, color 0.12s linear;
}

.DropdownMenuItem[data-highlighted],
.DropdownMenuCheckboxItem[data-highlighted],
.DropdownMenuRadioItem[data-highlighted],
.DropdownMenuSubTrigger[data-highlighted] {
@apply text-text-primary;
}

.DropdownMenuItem[data-disabled],
Expand Down
27 changes: 21 additions & 6 deletions packages/shared/src/components/fields/BaseFieldContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import classNames from 'classnames';
import type { ReactElement, ReactNode, MutableRefObject } from 'react';
import type { ReactElement, ReactNode, ForwardedRef } from 'react';
import React, { forwardRef } from 'react';
import type { FieldType, TextInputProps } from './common';
import { BaseField } from './common';
import type { IconProps } from '../Icon';
import { FieldSize, fieldSizeToRadius } from './fieldSizes';

interface FieldStateProps {
readOnly?: boolean;
Expand Down Expand Up @@ -73,7 +74,10 @@ export const getFieldFontColor = ({
return 'text-text-quaternary';
}

return 'text-text-tertiary hover:text-text-primary';
// Resting (empty, editable) fields read as active — secondary content on the
// floated surface, brightening to primary on hover. Tertiary here made an
// empty field look indistinguishable from the dimmed disabled state.
return 'text-text-secondary hover:text-text-primary';
};

interface InnerLabelProps extends FieldStateProps {
Expand All @@ -91,6 +95,7 @@ interface BaseFieldContainerProps extends FieldPlaceholderProps {
className?: FieldClassName;
inputId: string;
fieldType?: FieldType;
fieldSize?: FieldSize;
hint?: string;
hintIcon?: ReactElement<IconProps>;
saveHintSpace?: boolean;
Expand All @@ -112,11 +117,11 @@ export const getFieldPlaceholder = ({
label,
}: FieldPlaceholderProps): string => {
if (isQuaternaryField) {
return placeholder;
return placeholder ?? '';
}

if (isTertiaryField) {
return focused ? placeholder : label;
return (focused ? placeholder : label) ?? '';
}

if (focused || isSecondaryField) {
Expand Down Expand Up @@ -150,6 +155,7 @@ function BaseFieldContainer(
{
className = {},
fieldType = 'primary',
fieldSize,
readOnly,
isLocked,
hasInput,
Expand All @@ -164,9 +170,17 @@ function BaseFieldContainer(
saveHintSpace,
focusInput,
}: BaseFieldContainerProps,
ref?: MutableRefObject<HTMLDivElement>,
ref: ForwardedRef<HTMLDivElement>,
): ReactElement {
const isSecondaryField = fieldType === 'secondary';
// Radius always comes from the shared button-aligned scale, so a field's
// corner radius matches a button of the same size. The default (no explicit
// `fieldSize`) maps the compact secondary field to Small and every other
// field to Large — the same rung the default heights resolve to.
const radiusClass =
fieldSizeToRadius[
fieldSize ?? (isSecondaryField ? FieldSize.Small : FieldSize.Large)
];

return (
<div ref={ref} className={classNames('flex flex-col', className.container)}>
Expand All @@ -193,8 +207,9 @@ function BaseFieldContainer(
onClick={focusInput}
className={classNames(
'relative flex',
isSecondaryField ? 'rounded-10' : 'rounded-14',
radiusClass,
className.baseField,
disabled && 'pointer-events-none opacity-32',
{ readOnly, focused, invalid },
)}
>
Expand Down
11 changes: 8 additions & 3 deletions packages/shared/src/components/fields/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { RootPortal } from '../tooltips/Portal';
import type { DrawerProps } from '../drawers';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import type { IconProps } from '../Icon';
import { IconSize } from '../Icon';
import { Loader } from '../Loader';

export interface DropdownClassName {
Expand Down Expand Up @@ -142,7 +143,10 @@ export function Dropdown({
size={buttonSize}
disabled={disabled}
className={classNames(
'group flex w-full items-center px-3 font-normal text-text-tertiary typo-body hover:bg-surface-hover hover:text-text-primary',
// `!pl-4 !pr-2.5` overrides the Button's built-in Large padding (px-6)
// so the value lines up with the other fields' 16px text inset and the
// chevron sits tight to the right edge instead of floating 24px in.
'group flex w-full items-center !pl-4 !pr-2.5 font-normal text-text-secondary typo-body hover:bg-surface-hover hover:text-text-primary',
className?.button,
iconOnly && 'items-center justify-center',
)}
Expand Down Expand Up @@ -172,13 +176,14 @@ export function Dropdown({
{iconOnly ? null : (
<>
<span
className={classNames('mr-1 flex flex-1 truncate', className.label)}
className={classNames('mr-2 flex flex-1 truncate', className.label)}
>
{selectedIndex >= 0 ? options[selectedIndex] : placeholder}
</span>
<ArrowIcon
size={IconSize.Size16}
className={classNames(
'ml-auto text-xl transition-transform group-hover:text-text-tertiary',
'ml-auto shrink-0 text-text-quaternary transition-transform group-hover:text-text-primary',
isVisible ? 'rotate-0' : 'rotate-180',
styles.chevron,
className.chevron,
Expand Down
39 changes: 29 additions & 10 deletions packages/shared/src/components/fields/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type { ButtonProps } from '../buttons/Button';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { getFieldFontColor } from './BaseFieldContainer';
import type { IconProps } from '../Icon';
import { IconSize } from '../Icon';
import { FieldSize, fieldSizeToRadius } from './fieldSizes';

export interface SearchFieldProps
extends Pick<
Expand Down Expand Up @@ -88,25 +90,44 @@ export const SearchField = forwardRef(function SearchField(
onInput,
focusInput,
setInput,
} = useInputField(value, valueChanged);
} = useInputField(value as string | number | readonly string[], valueChanged);

const onClearClick = (event: MouseEvent): void => {
event.stopPropagation();
setInput(null);
setInput('');
};

const isPrimary = fieldType === 'primary';
const isSecondary = fieldType === 'secondary';
const sizeClass =
fieldSize === 'medium' ? 'h-10 rounded-12' : 'h-12 rounded-14';
const isMedium = fieldSize === 'medium';
const resolvedFieldSize = isMedium ? FieldSize.Medium : FieldSize.Large;
// Height + radius both come from the shared button-aligned scale so a search
// field lines up with a button (and every other field) of the same size.
const sizeClass = classNames(
isMedium ? 'h-10' : 'h-12',
fieldSizeToRadius[resolvedFieldSize],
);
// Mirror the TextField icon/gap scale so a search field lines up with the
// other fields and a button of the same height.
const searchIconSize = isMedium ? IconSize.Small : IconSize.Medium;
const gapClass = isMedium ? 'gap-1' : 'gap-1.5';

return (
<BaseField
{...props}
className={classNames(
'items-center !border !border-border-subtlest-tertiary !bg-background-default',
// Border width + background only — the resting border *color* is the Float
// hairline from `.field` so the search field matches every other field.
'items-center !border !bg-background-default',
// The base `.field:hover` background is blocked by `!bg-background-default`,
// so the search field needs its own hover feedback. Brighten the border and
// tint the surface, scoped to `:not(.focused)` so it never overrides the
// focus ring while the field is active.
'[&:hover:not(.focused)]:!border-border-subtlest-secondary [&:hover:not(.focused)]:!bg-surface-hover',
gapClass,
sizeClass,
className,
disabled && 'pointer-events-none opacity-32',
{ focused },
)}
onClick={focusInput}
Expand All @@ -117,21 +138,19 @@ export const SearchField = forwardRef(function SearchField(
(isSecondary && hasInput ? (
<Button
aria-label="Clear input text"
className="mr-2"
size={ButtonSize.XSmall}
variant={ButtonVariant.Tertiary}
title="Clear query"
onClick={onClearClick}
icon={
<CloseIcon className="icon text-lg group-hover:text-text-primary" />
}
icon={<CloseIcon className="icon group-hover:text-text-primary" />}
disabled={!hasInput}
/>
) : (
<SearchIcon
aria-hidden
className="icon mr-2 text-2xl"
className="icon"
role="presentation"
size={searchIconSize}
secondary={focused}
style={{
color:
Expand Down
120 changes: 72 additions & 48 deletions packages/shared/src/components/fields/TextField.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,91 @@
}
}

/*
* Fields v2: focus / invalid / readonly all use the same 1px border mechanism
* so the states read as one family (no mix of border + ring). Focus paints the
* border text-primary (see fields.module.css); the rules below override the
* border colour for the read-only "ready" and invalid states. Password strength
* keeps the previous left-edge indicator bar + colour-graded fill (a progress
* cue rather than a validity cue).
*/
.field {
&:hover {
box-shadow: inset 0.125rem 0 0 0 var(--theme-text-primary);
}

/* Read-only "ready" affordance: a blue border while focused. */
&:global(.focused.readOnly) {
box-shadow: inset 0.125rem 0 0 0 var(--theme-accent-blueCheese-default);
border-color: var(--theme-accent-blueCheese-default);
}

&:global(.focused) {
box-shadow: inset 0.125rem 0 0 0 var(--theme-text-primary);
border-color: var(--theme-text-primary);
/*
* Invalid: a solid red border. Same width/mechanism as the focus border, and
* it stays red while focused/typing so the error remains visible until fixed.
*/
&:global(.invalid),
&:global(.focused.invalid) {
border-color: var(--status-error);
}

&:global(.invalid) {
&:global(.password-0),
&:global(.password-1) {
box-shadow: inset 0.125rem 0 0 0 var(--status-error);
}

&:global(.password-0), &:global(.password-1) {
box-shadow: inset 0.125rem 0 0 0 var(--status-error);
&:before {
content: '';
border-radius: 14px 0px 0px 14px;
opacity: 0.24;
background: linear-gradient(270deg, rgba(252, 83, 141, 0) 0%, rgba(252, 83, 141, 1) 100%);
width: 44px;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
/*
* The strength bar inherits the field's rounded left corners via the
* parent's `overflow: hidden` clip, so it matches whatever radius the
* field size resolves to instead of a hardcoded value.
*/
&:before {
content: '';
opacity: 0.24;
background: linear-gradient(
270deg,
rgba(252, 83, 141, 0) 0%,
rgba(252, 83, 141, 1) 100%
);
width: 2.75rem;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
}

&:global(.password-2) {
box-shadow: inset 0.125rem 0 0 0 var(--status-warning);
box-shadow: inset 0.125rem 0 0 0 var(--status-warning);

&:before {
content: '';
border-radius: 14px 0px 0px 14px;
opacity: 0.24;
background: linear-gradient(270deg, rgba(255, 142, 59, 0) 0%, rgba(255, 142, 59, 1) 100%);
width: 44px;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
}
content: '';
opacity: 0.24;
background: linear-gradient(
270deg,
rgba(255, 142, 59, 0) 0%,
rgba(255, 142, 59, 1) 100%
);
width: 2.75rem;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
}

&:global(.password-3) {
box-shadow: inset 0.125rem 0 0 0 var(--status-success);
&:before {
content: '';
border-radius: 14px 0px 0px 14px;
opacity: 0.24;
background: linear-gradient(270deg, rgba(57, 229, 140, 0) 0%, rgba(107, 244, 192, 1) 100%);
width: 44px;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
}
box-shadow: inset 0.125rem 0 0 0 var(--status-success);

&:before {
content: '';
opacity: 0.24;
background: linear-gradient(
270deg,
rgba(57, 229, 140, 0) 0%,
rgba(107, 244, 192, 1) 100%
);
width: 2.75rem;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
}
}

.field input[type='number']::-webkit-inner-spin-button,
Expand Down
Loading
Loading