Merge branch 'main' into remix/watch-less
No files matched your search
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 6.5 KiB |
@@ -3,6 +3,8 @@ import { useBreakpoints } from "~/shared/nano-states";
|
||||
import { useZoom, useSelectedBreakpoint } from "../../shared/nano-states";
|
||||
import { minZoom } from "./zoom-setting";
|
||||
import { utils } from "@webstudio-is/project";
|
||||
import { shortcuts } from "~/shared/shortcuts";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
export const useSubscribeSelectBreakpointFromShortcut = () => {
|
||||
const [breakpoints] = useBreakpoints();
|
||||
@@ -17,6 +19,17 @@ const zoomStep = 20;
|
||||
|
||||
export const useSubscribeZoomFromShortcut = () => {
|
||||
const [zoom, setZoom] = useZoom();
|
||||
|
||||
// We need to prevent browser default behavior for zooming in/out
|
||||
// Maybe we should use shift modifier and keep the default behaivor functional?
|
||||
useHotkeys(
|
||||
shortcuts.zoom,
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useSubscribe("zoom", (direction) => {
|
||||
if (direction === "zoomIn") {
|
||||
setZoom(Math.min(zoom + zoomStep, 100));
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FontsManager } from "~/designer/shared/fonts-manager";
|
||||
import type { ControlProps } from "../../style-sections";
|
||||
import { getFinalValue } from "../../shared/get-final-value";
|
||||
import { ValuePickerPopover } from "../../shared/value-picker-popover";
|
||||
import { useState } from "react";
|
||||
|
||||
export const FontFamilyControl = ({
|
||||
currentStyle,
|
||||
@@ -16,6 +17,7 @@ export const FontFamilyControl = ({
|
||||
inheritedStyle,
|
||||
property: styleConfig.property,
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
if (value === undefined) return null;
|
||||
|
||||
@@ -25,9 +27,13 @@ export const FontFamilyControl = ({
|
||||
<ValuePickerPopover
|
||||
title="Fonts"
|
||||
content={<FontsManager value={String(value.value)} onChange={setValue} />}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{/* @todo this should be part of the design system, probably a varian="button" */}
|
||||
<TextField defaultValue={value.value} />
|
||||
<TextField
|
||||
defaultValue={value.value}
|
||||
state={isOpen ? "active" : undefined}
|
||||
/>
|
||||
</ValuePickerPopover>
|
||||
);
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { IconButton, TextField } from "@webstudio-is/design-system";
|
||||
import { Cross2Icon, MagnifyingGlassIcon } from "@webstudio-is/icons";
|
||||
import { components } from "@webstudio-is/react-sdk";
|
||||
import type { SelectedInstanceData } from "~/shared/canvas-components";
|
||||
|
||||
type OnSearch = (search: string) => void;
|
||||
|
||||
const useSearch = (onSearch: OnSearch): [string, OnSearch] => {
|
||||
const [search, setSearch] = useState("");
|
||||
return [
|
||||
search,
|
||||
(search: string) => {
|
||||
setSearch(search);
|
||||
onSearch(search);
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
type SearchProps = {
|
||||
onSearch: OnSearch;
|
||||
selectedInstanceData: SelectedInstanceData;
|
||||
};
|
||||
|
||||
export const Search = ({ onSearch, selectedInstanceData }: SearchProps) => {
|
||||
const [search, setSearch] = useSearch(onSearch);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<TextField
|
||||
type="search"
|
||||
value={search}
|
||||
inputRef={inputRef}
|
||||
prefix={
|
||||
<IconButton aria-label="Search">
|
||||
<MagnifyingGlassIcon />
|
||||
</IconButton>
|
||||
}
|
||||
suffix={
|
||||
search.length > 0 && (
|
||||
<IconButton
|
||||
aria-label="Reset search"
|
||||
onClick={() => {
|
||||
setSearch("");
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<Cross2Icon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
placeholder={components[selectedInstanceData.component].label}
|
||||
onChange={(event) => {
|
||||
const { value } = event.target;
|
||||
setSearch(value.toLowerCase());
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
ComboboxPopperAnchor,
|
||||
ComboboxListbox,
|
||||
ComboboxListboxItem,
|
||||
IconButton,
|
||||
numericScrubControl,
|
||||
TextFieldIconButton,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { ChevronDownIcon } from "@webstudio-is/icons";
|
||||
import {
|
||||
@@ -263,9 +263,12 @@ export const CssValueInput = ({
|
||||
|
||||
const suffix =
|
||||
value.type === "keyword" ? (
|
||||
<IconButton {...getToggleButtonProps()}>
|
||||
<TextFieldIconButton
|
||||
{...getToggleButtonProps()}
|
||||
state={isOpen ? "active" : undefined}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</IconButton>
|
||||
</TextFieldIconButton>
|
||||
) : value.type === "unit" ? (
|
||||
unitSelectElement
|
||||
) : null;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SelectViewport,
|
||||
SelectItem,
|
||||
SelectContent,
|
||||
TextFieldIconButton,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@webstudio-is/icons";
|
||||
|
||||
@@ -75,21 +76,8 @@ export const useUnitSelect = ({
|
||||
return [isOpen, select];
|
||||
};
|
||||
|
||||
const StyledTrigger = styled(SelectPrimitive.SelectTrigger, {
|
||||
all: "unset",
|
||||
height: "$5",
|
||||
px: "$1",
|
||||
borderRadius: 2,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
color: "$hiContrast",
|
||||
"&:hover": {
|
||||
backgroundColor: "$slate6",
|
||||
},
|
||||
"&:focus": {
|
||||
backgroundColor: "$blue10",
|
||||
color: "$loContrast",
|
||||
},
|
||||
const StyledTrigger = styled(TextFieldIconButton, {
|
||||
px: 3,
|
||||
});
|
||||
|
||||
type UnitSelectProps = {
|
||||
@@ -116,9 +104,11 @@ const UnitSelect = ({
|
||||
onOpenChange={onOpenChange}
|
||||
open={open}
|
||||
>
|
||||
<StyledTrigger>
|
||||
<SelectPrimitive.Value>{value}</SelectPrimitive.Value>
|
||||
</StyledTrigger>
|
||||
<SelectPrimitive.SelectTrigger asChild>
|
||||
<StyledTrigger>
|
||||
<SelectPrimitive.Value>{value}</SelectPrimitive.Value>
|
||||
</StyledTrigger>
|
||||
</SelectPrimitive.SelectTrigger>
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectContent onCloseAutoFocus={onCloseAutoFocus}>
|
||||
<SelectScrollUpButton>
|
||||
|
||||
@@ -17,27 +17,32 @@ const usePickerSideOffset = (
|
||||
return [ref, sideOffset];
|
||||
};
|
||||
|
||||
type AssetPickerProps = {
|
||||
type ValuePickerPopoverProps = {
|
||||
title: string;
|
||||
content: JSX.Element;
|
||||
children: JSX.Element;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const ValuePickerPopover = ({
|
||||
title,
|
||||
content,
|
||||
children,
|
||||
}: AssetPickerProps) => {
|
||||
onOpenChange,
|
||||
}: ValuePickerPopoverProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [ref, sideOffset] = usePickerSideOffset(isOpen);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setIsOpen(open);
|
||||
onOpenChange?.(open);
|
||||
};
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen} modal>
|
||||
<Popover open={isOpen} onOpenChange={handleOpenChange} modal>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
handleOpenChange(true);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -47,7 +52,8 @@ export const ValuePickerPopover = ({
|
||||
sideOffset={sideOffset}
|
||||
side="right"
|
||||
hideArrow
|
||||
css={{ minHeight: 500 }}
|
||||
align="start"
|
||||
css={{ width: PANEL_WIDTH }}
|
||||
>
|
||||
<PopoverHeader title={title} />
|
||||
{content}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { Publish } from "~/shared/pubsub";
|
||||
import { willRender } from "~/designer/shared/breakpoints";
|
||||
import { Box, Card, Paragraph } from "@webstudio-is/design-system";
|
||||
import { Box, Card, Paragraph, SearchField } from "@webstudio-is/design-system";
|
||||
import type { SelectedInstanceData } from "~/shared/canvas-components";
|
||||
import { useStyleData } from "./shared/use-style-data";
|
||||
import { StyleSettings } from "./style-settings";
|
||||
import { Search } from "./search";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
useCanvasWidth,
|
||||
@@ -56,9 +55,14 @@ export const StylePanel = ({
|
||||
return (
|
||||
<>
|
||||
<Box css={{ px: "$3", py: "$1" }}>
|
||||
<Search
|
||||
onSearch={setSearch}
|
||||
selectedInstanceData={selectedInstanceData}
|
||||
<SearchField
|
||||
placeholder="Search"
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setSearch("");
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box css={{ overflow: "auto" }}>
|
||||
|
||||
@@ -1,70 +1,57 @@
|
||||
/* This entire module is WIP */
|
||||
import type { Asset } from "@webstudio-is/asset-uploader";
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
ComboboxListboxItem,
|
||||
TextField,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
Text,
|
||||
DropdownMenuPortal,
|
||||
useCombobox,
|
||||
Separator,
|
||||
SearchField,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { AssetUpload, PreviewAsset, useAssets } from "~/designer/shared/assets";
|
||||
import { SYSTEM_FONTS } from "@webstudio-is/fonts";
|
||||
import { DotsHorizontalIcon, MagnifyingGlassIcon } from "@webstudio-is/icons";
|
||||
import { useMemo } from "react";
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ItemMenu } from "./item-menu";
|
||||
import { Listbox, ListboxItem } from "./listbox";
|
||||
|
||||
const getItems = (
|
||||
assets: Array<Asset | PreviewAsset>
|
||||
): Array<{ label: string }> => {
|
||||
const system = Array.from(SYSTEM_FONTS.keys()).map((label) => ({ label }));
|
||||
type Item = {
|
||||
label: string;
|
||||
type: "uploaded" | "system";
|
||||
};
|
||||
|
||||
const toItems = (assets: Array<Asset | PreviewAsset>): Array<Item> => {
|
||||
const system = Array.from(SYSTEM_FONTS.keys()).map((label) => ({
|
||||
label,
|
||||
type: "system",
|
||||
}));
|
||||
// We can have 2+ assets with the same family name, so we use a map to dedupe.
|
||||
const uploaded = new Map();
|
||||
for (const asset of assets) {
|
||||
// @todo need to teach ts the right type from useAssets
|
||||
if ("meta" in asset && "family" in asset.meta) {
|
||||
uploaded.set(asset.meta.family, { label: asset.meta.family });
|
||||
uploaded.set(asset.meta.family, {
|
||||
label: asset.meta.family,
|
||||
type: "uploaded",
|
||||
});
|
||||
}
|
||||
}
|
||||
return [...system, ...uploaded.values()];
|
||||
return [...uploaded.values(), ...system];
|
||||
};
|
||||
|
||||
const ItemMenu = ({ onDelete }: { onDelete: () => void }) => {
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton aria-label="Font menu button">
|
||||
<DotsHorizontalIcon />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<Text>Delete font</Text>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
<Flex align="center" justify="center" css={{ height: 100 }}>
|
||||
Font not found
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
type FontsManagerProps = {
|
||||
const useLogic = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
|
||||
}) => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { assets, handleDelete } = useAssets("font");
|
||||
|
||||
const handleDeleteByLabel = (family: string) => {
|
||||
// One family may have multiple assets for different formats, so we need to delete them all.
|
||||
const ids = assets
|
||||
@@ -79,63 +66,169 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
|
||||
handleDelete(ids);
|
||||
};
|
||||
|
||||
const fontItems = useMemo(() => getItems(assets), [assets]);
|
||||
const fontItems = useMemo(() => toItems(assets), [assets]);
|
||||
|
||||
const selectedItem =
|
||||
fontItems.find((item) => item.label === value) ?? fontItems[0];
|
||||
|
||||
const { items, getComboboxProps, getMenuProps, getItemProps, getInputProps } =
|
||||
useCombobox({
|
||||
items: fontItems,
|
||||
value: selectedItem,
|
||||
itemToString: (item) => item?.label ?? "",
|
||||
onItemSelect: (value) => {
|
||||
if (value !== null) {
|
||||
onChange(value.label);
|
||||
}
|
||||
},
|
||||
});
|
||||
const {
|
||||
items: filteredItems,
|
||||
resetFilter,
|
||||
highlightedIndex,
|
||||
isOpen,
|
||||
inputValue,
|
||||
...comboboxProps
|
||||
} = useCombobox({
|
||||
// Makes sure first keydown selectsthe item instead of opening the list first otherwise.
|
||||
isOpen: true,
|
||||
items: fontItems,
|
||||
value: selectedItem,
|
||||
itemToString(item) {
|
||||
return item?.label ?? "";
|
||||
},
|
||||
onItemSelect(value) {
|
||||
if (value !== null) {
|
||||
onChange(value.label);
|
||||
handleCancelSearch();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { isEmpty, ...menuProps } = getMenuProps();
|
||||
const { uploadedItems, systemItems } = useMemo(() => {
|
||||
const uploadedItems = filteredItems.filter(
|
||||
(item) => item.type === "uploaded"
|
||||
);
|
||||
const systemItems = filteredItems.filter((item) => item.type === "system");
|
||||
return { uploadedItems, systemItems };
|
||||
}, [filteredItems]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearch(inputValue);
|
||||
}, [inputValue]);
|
||||
|
||||
const handleCancelSearch = useCallback(() => {
|
||||
resetFilter();
|
||||
setSearch("");
|
||||
}, [resetFilter]);
|
||||
|
||||
useEffect(handleCancelSearch, [fontItems, selectedItem, handleCancelSearch]);
|
||||
|
||||
return {
|
||||
filteredItems,
|
||||
uploadedItems,
|
||||
systemItems,
|
||||
handleDelete: handleDeleteByLabel,
|
||||
handleCancelSearch,
|
||||
search,
|
||||
...comboboxProps,
|
||||
};
|
||||
};
|
||||
|
||||
type FontsManagerProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
|
||||
const {
|
||||
filteredItems,
|
||||
uploadedItems,
|
||||
systemItems,
|
||||
getInputProps,
|
||||
handleDelete,
|
||||
handleCancelSearch,
|
||||
getComboboxProps,
|
||||
getMenuProps,
|
||||
getItemProps,
|
||||
setHighlightedIndex,
|
||||
search,
|
||||
} = useLogic({ value, onChange });
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap="3"
|
||||
direction="column"
|
||||
css={{ padding: "$1", paddingTop: "$2", height: 460, overflow: "hidden" }}
|
||||
>
|
||||
<Box css={{ padding: "$2" }}>
|
||||
<Flex direction="column" css={{ overflow: "hidden", py: "$1" }}>
|
||||
<Flex css={{ py: "$2", px: "$3" }} gap="2" direction="column">
|
||||
<AssetUpload type="font" />
|
||||
</Box>
|
||||
|
||||
<SearchField
|
||||
{...getInputProps({ value: search })}
|
||||
autoFocus
|
||||
placeholder="Search"
|
||||
onBlur={undefined}
|
||||
onFocus={() => {
|
||||
setHighlightedIndex(-1);
|
||||
}}
|
||||
onCancel={handleCancelSearch}
|
||||
/>
|
||||
</Flex>
|
||||
<Separator css={{ my: "$1" }} />
|
||||
{filteredItems.length === 0 && <NotFound />}
|
||||
<Flex
|
||||
{...getComboboxProps()}
|
||||
css={{ flexDirection: "column", gap: "$3" }}
|
||||
css={{
|
||||
flexDirection: "column",
|
||||
gap: "$3",
|
||||
px: "$3",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
type="search"
|
||||
prefix={<MagnifyingGlassIcon />}
|
||||
{...getInputProps()}
|
||||
/>
|
||||
<fieldset>
|
||||
<legend>Choose an item</legend>
|
||||
<Flex {...menuProps} css={{ flexDirection: "column" }}>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<ComboboxListboxItem
|
||||
{...getItemProps({ item, index })}
|
||||
key={index}
|
||||
>
|
||||
{item.label}
|
||||
<Listbox {...getMenuProps()}>
|
||||
{uploadedItems.length !== 0 && (
|
||||
<ListboxItem disabled>{"Uploaded"}</ListboxItem>
|
||||
)}
|
||||
{uploadedItems.map((item, index) => {
|
||||
return (
|
||||
<ListboxItem
|
||||
{...getItemProps({
|
||||
item,
|
||||
index,
|
||||
})}
|
||||
key={index}
|
||||
suffix={
|
||||
<ItemMenu
|
||||
onOpenChange={() => {
|
||||
setHighlightedIndex(index);
|
||||
}}
|
||||
onDelete={() => {
|
||||
handleDeleteByLabel(item.label);
|
||||
handleDelete(item.label);
|
||||
}}
|
||||
onFocusTrigger={() => {
|
||||
setHighlightedIndex(-1);
|
||||
}}
|
||||
/>
|
||||
</ComboboxListboxItem>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</fieldset>
|
||||
}
|
||||
onFocus={(event) => {
|
||||
// We need to ignore focus on a menu button inside
|
||||
if (event.target === event.currentTarget) {
|
||||
setHighlightedIndex(index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</ListboxItem>
|
||||
);
|
||||
})}
|
||||
{systemItems.length !== 0 && (
|
||||
<>
|
||||
<Separator css={{ my: "$1" }} />
|
||||
<ListboxItem disabled>{"System"}</ListboxItem>
|
||||
</>
|
||||
)}
|
||||
{systemItems.map((item, index) => {
|
||||
const globalIndex = uploadedItems.length + index;
|
||||
return (
|
||||
<ListboxItem
|
||||
{...getItemProps({
|
||||
item,
|
||||
index: globalIndex,
|
||||
})}
|
||||
key={index}
|
||||
onFocus={() => {
|
||||
setHighlightedIndex(globalIndex);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</ListboxItem>
|
||||
);
|
||||
})}
|
||||
</Listbox>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
Text,
|
||||
DropdownMenuPortal,
|
||||
styled,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { DotsHorizontalIcon } from "@webstudio-is/icons";
|
||||
import { cssVars } from "@webstudio-is/css-vars";
|
||||
import { type FocusEventHandler, useState } from "react";
|
||||
|
||||
const visibilityVar = cssVars.define("visibility");
|
||||
|
||||
export const getItemMenuVars = (state: "visible" | "hidden") => {
|
||||
return {
|
||||
[visibilityVar]: state,
|
||||
};
|
||||
};
|
||||
|
||||
const MenuButton = styled(IconButton, {
|
||||
visibility: cssVars.use(visibilityVar, "hidden"),
|
||||
color: "$hint",
|
||||
"&:hover, &:focus": {
|
||||
color: "$hiContrast",
|
||||
},
|
||||
});
|
||||
|
||||
type ItemMenuProps = {
|
||||
onDelete: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onFocusTrigger?: FocusEventHandler;
|
||||
};
|
||||
|
||||
export const ItemMenu = ({
|
||||
onDelete,
|
||||
onOpenChange,
|
||||
onFocusTrigger,
|
||||
}: ItemMenuProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpen(open);
|
||||
onOpenChange(open);
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<MenuButton
|
||||
aria-label="Font menu"
|
||||
onFocus={onFocusTrigger}
|
||||
tabIndex={0}
|
||||
>
|
||||
<DotsHorizontalIcon />
|
||||
</MenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<Text>Delete font</Text>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Flex, styled, Text } from "@webstudio-is/design-system";
|
||||
import { CheckIcon } from "@webstudio-is/icons";
|
||||
import { ComponentProps, forwardRef } from "react";
|
||||
import { getItemMenuVars } from "./item-menu";
|
||||
|
||||
export const Listbox = styled("ul", {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
variants: {
|
||||
empty: {
|
||||
true: {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ListboxItemBase = styled("li", {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "$4 1fr",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "$5",
|
||||
px: "$1",
|
||||
listStyle: "none",
|
||||
borderRadius: "$1",
|
||||
outline: 0,
|
||||
"&[data-highlighted]": {
|
||||
boxShadow: "0px 0px 0px 2px $colors$blue10, 0px 0px 0px 2px $colors$blue10",
|
||||
...getItemMenuVars("visible"),
|
||||
},
|
||||
"&:focus-within": getItemMenuVars("visible"),
|
||||
"&[disabled]": {
|
||||
pointerEvents: "none",
|
||||
},
|
||||
});
|
||||
|
||||
export const ListboxItem = forwardRef<
|
||||
HTMLLIElement,
|
||||
ComponentProps<typeof ListboxItemBase> & {
|
||||
highlighted?: boolean;
|
||||
disabled?: boolean;
|
||||
selected?: boolean;
|
||||
suffix?: JSX.Element;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
highlighted,
|
||||
disabled = false,
|
||||
selected = false,
|
||||
children,
|
||||
suffix,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<ListboxItemBase
|
||||
{...props}
|
||||
ref={ref}
|
||||
{...(highlighted ? { ["data-highlighted"]: true } : {})}
|
||||
{...(disabled ? { ["aria-disabled"]: true, disabled: true } : {})}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
>
|
||||
{selected && <CheckIcon />}
|
||||
<Flex css={{ gridColumn: 2 }} align="center">
|
||||
<Text variant="label" truncate color={disabled ? "hint" : "contrast"}>
|
||||
{children}
|
||||
</Text>
|
||||
{suffix}
|
||||
</Flex>
|
||||
</ListboxItemBase>
|
||||
);
|
||||
}
|
||||
);
|
||||
ListboxItem.displayName = "ListboxItem";
|
||||
@@ -20,7 +20,6 @@ export const usePublishShortcuts = (publish: Publish) => {
|
||||
useHotkeys(
|
||||
shortcuts[name],
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
publish({
|
||||
type: "shortcut",
|
||||
payload: { name, key: event.key },
|
||||
|
||||
@@ -7,7 +7,7 @@ export const Button = styled("button", {
|
||||
boxSizing: "border-box",
|
||||
border: "none",
|
||||
userSelect: "none",
|
||||
transition: "all 200ms ease-out",
|
||||
transition: "all 150ms ease-out",
|
||||
"&::before": {
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
@@ -40,7 +40,7 @@ export const Button = styled("button", {
|
||||
variants: {
|
||||
size: {
|
||||
"1": {
|
||||
borderRadius: 2,
|
||||
borderRadius: "$1",
|
||||
height: "$5",
|
||||
px: "$3",
|
||||
fontSize: "$1",
|
||||
@@ -82,7 +82,7 @@ export const Button = styled("button", {
|
||||
},
|
||||
blue: {
|
||||
backgroundColor: "$blue10",
|
||||
color: "$loContrast",
|
||||
color: "white",
|
||||
"@hover": {
|
||||
"&:hover": {
|
||||
backgroundColor: "$loContrast",
|
||||
@@ -269,7 +269,8 @@ export const Button = styled("button", {
|
||||
backgroundColor: "$blueA4",
|
||||
},
|
||||
"&:focus": {
|
||||
boxShadow: "inset 0 0 0 1px $colors$blueA8, 0 0 0 1px $colors$blueA8",
|
||||
boxShadow:
|
||||
"0px 0px 0px 2px $colors$blue10, 0px 0px 0px 2px $colors$blue10",
|
||||
},
|
||||
'&[data-state="open"]': {
|
||||
backgroundColor: "$blueA4",
|
||||
|
||||
@@ -10,8 +10,9 @@ import React, {
|
||||
import { CheckIcon, ChevronDownIcon } from "@webstudio-is/icons";
|
||||
import { Popper, PopperContent, PopperAnchor } from "@radix-ui/react-popper";
|
||||
import {
|
||||
DownshiftState,
|
||||
UseComboboxStateChangeOptions,
|
||||
type DownshiftState,
|
||||
type UseComboboxStateChangeOptions,
|
||||
type UseComboboxProps as UseDownshiftComboboxProps,
|
||||
useCombobox as useDownshiftCombobox,
|
||||
} from "downshift";
|
||||
import { matchSorter } from "match-sorter";
|
||||
@@ -37,7 +38,7 @@ const Listbox = styled("ul", panelStyles, {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
isEmpty: {
|
||||
empty: {
|
||||
true: {
|
||||
display: "none",
|
||||
},
|
||||
@@ -128,7 +129,7 @@ const useFilter = <Item,>({
|
||||
};
|
||||
};
|
||||
|
||||
type UseComboboxProps<Item> = {
|
||||
type UseComboboxProps<Item> = UseDownshiftComboboxProps<Item> & {
|
||||
items: Array<Item>;
|
||||
itemToString: (item: Item | null) => string;
|
||||
value: Item | null; // This is to prevent: "downshift: A component has changed the uncontrolled prop "selectedItem" to be controlled."
|
||||
@@ -151,6 +152,7 @@ export const useCombobox = <Item,>({
|
||||
onItemHighlight,
|
||||
stateReducer = (state, { changes }) => changes,
|
||||
match,
|
||||
...rest
|
||||
}: UseComboboxProps<Item>) => {
|
||||
const { filteredItems, filter, resetFilter } = useFilter<Item>({
|
||||
items,
|
||||
@@ -159,7 +161,9 @@ export const useCombobox = <Item,>({
|
||||
});
|
||||
|
||||
const downshiftProps = useDownshiftCombobox({
|
||||
...rest,
|
||||
items: filteredItems,
|
||||
defaultHighlightedIndex: -1,
|
||||
selectedItem: value, // Prevent downshift warning about switching controlled mode
|
||||
stateReducer,
|
||||
itemToString,
|
||||
@@ -203,7 +207,7 @@ export const useCombobox = <Item,>({
|
||||
return {
|
||||
...getMenuProps(options),
|
||||
state: isOpen ? "open" : "closed",
|
||||
isEmpty: filteredItems.length === 0,
|
||||
empty: filteredItems.length === 0,
|
||||
};
|
||||
},
|
||||
[getMenuProps, isOpen, filteredItems.length]
|
||||
@@ -214,6 +218,7 @@ export const useCombobox = <Item,>({
|
||||
items: filteredItems,
|
||||
getItemProps: enhancedGetItemProps,
|
||||
getMenuProps: enhancedGetMenuProps,
|
||||
resetFilter,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ export const IconButton = styled("button", {
|
||||
"&:focus": {
|
||||
backgroundColor: "$slate6",
|
||||
outline: "none",
|
||||
border: "2px solid white",
|
||||
boxShadow: "0px 0px 0px 2px $colors$blue10, 0px 0px 0px 2px $colors$blue10",
|
||||
},
|
||||
"&:disabled": {
|
||||
pointerEvents: "none",
|
||||
@@ -41,8 +43,12 @@ export const IconButton = styled("button", {
|
||||
size: {
|
||||
"1": {
|
||||
borderRadius: 2,
|
||||
height: "$5",
|
||||
width: "$5",
|
||||
height: "$3",
|
||||
width: "$3",
|
||||
"&:hover, &:focus": {
|
||||
background: "none",
|
||||
borderColor: "transparent",
|
||||
},
|
||||
},
|
||||
"2": {
|
||||
borderRadius: "$1",
|
||||
@@ -60,12 +66,6 @@ export const IconButton = styled("button", {
|
||||
width: "$8",
|
||||
},
|
||||
},
|
||||
state: {
|
||||
active: {
|
||||
color: "$loContrast",
|
||||
backgroundColor: "$blue10",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "1",
|
||||
|
||||
@@ -2,10 +2,9 @@ import { css, styled } from "../stitches.config";
|
||||
|
||||
export const panelStyles = css({
|
||||
overflow: "auto",
|
||||
backgroundColor: "$colors$slate4",
|
||||
backgroundColor: "$slate4",
|
||||
borderRadius: "$1",
|
||||
boxShadow:
|
||||
"0px 2px 7px rgba(0, 0, 0, 0.1), 0px 5px 17px rgba(0, 0, 0, 0.15), inset 0 0 1px 1px $colors$slate1, 0 0 0 1px $colors$slate8",
|
||||
boxShadow: "0px 2px 7px rgba(0, 0, 0, 0.1), 0px 5px 17px rgba(0, 0, 0, 0.15)",
|
||||
});
|
||||
|
||||
export const Panel = styled("div", panelStyles);
|
||||
@@ -74,12 +74,12 @@ export const PopoverHeader = ({ title }: PopoverHeaderProps) => {
|
||||
>
|
||||
<Text variant="title">{title}</Text>
|
||||
<PopoverClose asChild>
|
||||
<IconButton size="1" css={{ marginRight: "$2" }} aria-label="Close">
|
||||
<IconButton size="2" css={{ marginRight: "$2" }} aria-label="Close">
|
||||
<Cross1Icon />
|
||||
</IconButton>
|
||||
</PopoverClose>
|
||||
</Flex>
|
||||
<Separator css={{ height: 2 }} />
|
||||
<Separator />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { type Placement } from "./geometry-utils";
|
||||
const placementStyle = {
|
||||
boxSizing: "content-box",
|
||||
position: "absolute",
|
||||
background: "$dropPlacement",
|
||||
background: "$blue10",
|
||||
pointerEvents: "none",
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { expect } from "@storybook/jest";
|
||||
import { type ComponentStory } from "@storybook/react";
|
||||
import { userEvent, waitFor, within } from "@storybook/testing-library";
|
||||
import React from "react";
|
||||
import { SearchField } from "./search-field";
|
||||
|
||||
export default {
|
||||
component: SearchField,
|
||||
};
|
||||
|
||||
export const Empty: ComponentStory<typeof SearchField> = () => {
|
||||
return <SearchField />;
|
||||
};
|
||||
|
||||
export const WithInitialValue: ComponentStory<typeof SearchField> = () => {
|
||||
return <SearchField value={"somevalue"} title="Search" />;
|
||||
};
|
||||
|
||||
let updateValue: (value: string) => void;
|
||||
|
||||
export const UpdateValue: ComponentStory<typeof SearchField> = () => {
|
||||
const [value, setValue] = React.useState("something");
|
||||
updateValue = setValue;
|
||||
return <SearchField value={value} title="Search" />;
|
||||
};
|
||||
|
||||
UpdateValue.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const search = canvas.getByTitle<HTMLInputElement>("Search");
|
||||
expect(search.value).toBe("something");
|
||||
updateValue?.("test");
|
||||
await waitFor(() => expect(search.value).toBe("test"));
|
||||
};
|
||||
|
||||
export const Reset: ComponentStory<typeof SearchField> = () => {
|
||||
return <SearchField value="something" title="Search" />;
|
||||
};
|
||||
|
||||
Reset.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const search = canvas.getByTitle<HTMLInputElement>("Search");
|
||||
await userEvent.click(canvas.getByTitle("Reset search"));
|
||||
await waitFor(() => expect(search.value).toBe(""));
|
||||
expect(await canvas.queryByTitle("Reset search")).toBeNull();
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
type ComponentProps,
|
||||
type ForwardRefRenderFunction,
|
||||
forwardRef,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { Cross2Icon, MagnifyingGlassIcon } from "@webstudio-is/icons";
|
||||
import { TextField } from "./text-field";
|
||||
import { IconButton } from "./icon-button";
|
||||
import { styled } from "../stitches.config";
|
||||
|
||||
const SearchIcon = styled(MagnifyingGlassIcon, {
|
||||
color: "$hint",
|
||||
padding: "$1",
|
||||
});
|
||||
|
||||
const SearchFieldBase: ForwardRefRenderFunction<
|
||||
HTMLInputElement,
|
||||
ComponentProps<typeof TextField> & { onCancel?: () => void }
|
||||
> = (props, ref) => {
|
||||
const {
|
||||
onChange,
|
||||
onCancel,
|
||||
value: propsValue = "",
|
||||
onKeyDown,
|
||||
...rest
|
||||
} = props;
|
||||
const [value, setValue] = useState(String(propsValue));
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => {
|
||||
setValue(String(propsValue));
|
||||
}, [propsValue]);
|
||||
return (
|
||||
<TextField
|
||||
{...rest}
|
||||
ref={ref}
|
||||
type="search"
|
||||
value={value}
|
||||
inputRef={inputRef}
|
||||
prefix={<SearchIcon />}
|
||||
suffix={
|
||||
value.length > 0 ? (
|
||||
<IconButton
|
||||
aria-label="Reset search"
|
||||
title="Reset search"
|
||||
tabIndex={-1}
|
||||
onClick={() => {
|
||||
inputRef.current?.focus();
|
||||
setValue("");
|
||||
onCancel?.();
|
||||
}}
|
||||
>
|
||||
<Cross2Icon />
|
||||
</IconButton>
|
||||
) : null
|
||||
}
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value);
|
||||
onChange?.(event);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
// Make sure we clear the search on esc and preserve the default browser behavior
|
||||
if (event.key === "Escape" && value.length !== 0) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
onKeyDown?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchField = forwardRef(SearchFieldBase);
|
||||
@@ -11,7 +11,7 @@ export const TextArea = styled("textarea", {
|
||||
width: "100%",
|
||||
WebkitTapHighlightColor: "rgba(0,0,0,0)",
|
||||
backgroundColor: "$loContrast",
|
||||
boxShadow: "inset 0 0 0 1px $colors$slate7",
|
||||
boxShadow: "inset 0 0 0 1px $colors$muted",
|
||||
color: "$hiContrast",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
position: "relative",
|
||||
|
||||
@@ -30,6 +30,7 @@ export const NativeProps: ComponentStory<typeof TextField> = () => {
|
||||
<TextField disabled placeholder="This is a disabled placeholder" />
|
||||
<TextField type="number" defaultValue={25} />
|
||||
<TextField type="search" placeholder="This is a search input" />
|
||||
<TextField type="button" defaultValue={"Arial"} />
|
||||
<TextField readOnly value="Read-only" />
|
||||
<TextField disabled value="Disabled" />
|
||||
</Flex>
|
||||
@@ -41,6 +42,8 @@ export const Variants: ComponentStory<typeof TextField> = () => {
|
||||
<Flex direction="column" gap={3}>
|
||||
<TextField />
|
||||
<TextField variant="ghost" />
|
||||
<TextField type="button" />
|
||||
<TextField type="button" state="active" />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,71 @@
|
||||
import { mergeRefs } from "@react-aria/utils";
|
||||
import React from "react";
|
||||
import { useFocusWithin } from "@react-aria/interactions";
|
||||
import { styled } from "../stitches.config";
|
||||
import { Flex } from "./flex";
|
||||
import { css, styled } from "../stitches.config";
|
||||
import { ChevronLeftIcon } from "@webstudio-is/icons";
|
||||
import { cssVars } from "@webstudio-is/css-vars";
|
||||
|
||||
const backgroundColorVar = cssVars.define("background-color");
|
||||
const colorVar = cssVars.define("color");
|
||||
|
||||
const getTextFieldSuffixCssVars = (state: "focus" | "hover") => {
|
||||
if (state === "focus") {
|
||||
return {
|
||||
[backgroundColorVar]: "$colors$blue10",
|
||||
[colorVar]: "white",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
[backgroundColorVar]: "$colors$slate7",
|
||||
[colorVar]: "$colors$hiContrast",
|
||||
};
|
||||
};
|
||||
|
||||
const textFieldIconBaseStyle = css({
|
||||
height: "$5",
|
||||
minWidth: "$2",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 2,
|
||||
});
|
||||
|
||||
// Trigger can be used as a button, which is focusable/hoverable itself or as an icon,
|
||||
// which has the same states but activated over the parent component element.
|
||||
export const TextFieldIconButton = styled(
|
||||
"button",
|
||||
{
|
||||
all: "unset",
|
||||
"&:hover": {
|
||||
backgroundColor: "$slate7",
|
||||
color: "$hiContrast",
|
||||
},
|
||||
"&:focus": {
|
||||
backgroundColor: "$blue10",
|
||||
color: "white",
|
||||
},
|
||||
variants: {
|
||||
state: {
|
||||
active: {
|
||||
backgroundColor: "$blue10",
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: "$blue10",
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
textFieldIconBaseStyle
|
||||
);
|
||||
|
||||
export const TextFieldIcon = styled("span", textFieldIconBaseStyle, {
|
||||
// Icon receives colors from parent.
|
||||
backgroundColor: cssVars.use(backgroundColorVar),
|
||||
color: cssVars.use(colorVar),
|
||||
});
|
||||
|
||||
const InputBase = styled("input", {
|
||||
// Reset
|
||||
@@ -52,7 +115,7 @@ const InputBase = styled("input", {
|
||||
},
|
||||
|
||||
"&::placeholder": {
|
||||
color: "$slate9",
|
||||
color: "$hint",
|
||||
},
|
||||
|
||||
"&:disabled": {
|
||||
@@ -78,12 +141,10 @@ const TextFieldBase = styled("div", {
|
||||
fontSize: "$1",
|
||||
height: 28, // @todo waiting for the sizing scale
|
||||
lineHeight: 1,
|
||||
|
||||
"&:focus-within": {
|
||||
boxShadow:
|
||||
"inset 0px 0px 0px 1px $colors$blue10, 0px 0px 0px 1px $colors$blue10",
|
||||
},
|
||||
|
||||
"&[aria-disabled=true]": {
|
||||
pointerEvents: "none",
|
||||
backgroundColor: "$slate2",
|
||||
@@ -94,7 +155,6 @@ const TextFieldBase = styled("div", {
|
||||
boxShadow: "inset 0px 0px 0px 1px $colors$slate7",
|
||||
},
|
||||
},
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
ghost: {
|
||||
@@ -108,7 +168,7 @@ const TextFieldBase = styled("div", {
|
||||
"&:focus": {
|
||||
backgroundColor: "$loContrast",
|
||||
boxShadow:
|
||||
"inset 0px 0px 0px 1px $colors$blue8, 0px 0px 0px 1px $colors$blue10",
|
||||
"inset 0px 0px 0px 1px $colors$blue10, 0px 0px 0px 1px $colors$blue10",
|
||||
},
|
||||
"&:disabled": {
|
||||
backgroundColor: "transparent",
|
||||
@@ -117,6 +177,10 @@ const TextFieldBase = styled("div", {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
button: {
|
||||
"&:hover": getTextFieldSuffixCssVars("hover"),
|
||||
"&:focus-within": getTextFieldSuffixCssVars("focus"),
|
||||
},
|
||||
},
|
||||
state: {
|
||||
invalid: {
|
||||
@@ -133,20 +197,46 @@ const TextFieldBase = styled("div", {
|
||||
"inset 0px 0px 0px 1px $colors$green8, 0px 0px 0px 1px $colors$green8",
|
||||
},
|
||||
},
|
||||
active: {
|
||||
boxShadow:
|
||||
"inset 0px 0px 0px 1px $colors$blue10, 0px 0px 0px 1px $colors$blue10",
|
||||
...getTextFieldSuffixCssVars("focus"),
|
||||
},
|
||||
},
|
||||
// Preffix and suffix are responsible for their spacing
|
||||
withPrefix: {
|
||||
true: {
|
||||
paddingLeft: 2,
|
||||
paddingLeft: 0,
|
||||
},
|
||||
},
|
||||
withSuffix: {
|
||||
true: {
|
||||
paddingRight: 2,
|
||||
paddingRight: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const PrefixSlot = styled("div", {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
order: 0,
|
||||
padding: 2,
|
||||
paddingRight: 0,
|
||||
borderRadius: 2,
|
||||
});
|
||||
|
||||
const SuffixSlot = styled("div", {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
order: 2,
|
||||
padding: 2,
|
||||
paddingLeft: 0,
|
||||
borderRadius: 2,
|
||||
});
|
||||
|
||||
export type TextFieldProps = Pick<
|
||||
React.ComponentProps<typeof TextFieldBase>,
|
||||
"variant" | "state" | "css"
|
||||
@@ -161,16 +251,22 @@ export const TextField = React.forwardRef<HTMLDivElement, TextFieldProps>(
|
||||
(props, forwardedRef) => {
|
||||
const {
|
||||
prefix,
|
||||
suffix,
|
||||
css,
|
||||
disabled,
|
||||
inputRef,
|
||||
state,
|
||||
variant,
|
||||
variant: variantProp,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onClick,
|
||||
type,
|
||||
// prevent spreading it into the dom
|
||||
suffix: suffixProp,
|
||||
...textFieldProps
|
||||
} = props;
|
||||
let suffix = suffixProp;
|
||||
const variant =
|
||||
type === "button" && variantProp === undefined ? "button" : variantProp;
|
||||
|
||||
const internalInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -180,16 +276,23 @@ export const TextField = React.forwardRef<HTMLDivElement, TextFieldProps>(
|
||||
|
||||
const { focusWithinProps } = useFocusWithin({
|
||||
isDisabled: disabled,
|
||||
onFocusWithin: (event) => {
|
||||
// @ts-expect-error Type mismatch from react-aria
|
||||
onFocus?.(event);
|
||||
},
|
||||
onBlurWithin: (event) => {
|
||||
// @ts-expect-error Type mismatch from react-aria
|
||||
onBlur?.(event);
|
||||
},
|
||||
// @ts-expect-error Type mismatch from react-aria
|
||||
onFocusWithin: onFocus,
|
||||
// @ts-expect-error Type mismatch from react-aria
|
||||
onBlurWithin: onBlur,
|
||||
});
|
||||
|
||||
if (type === "button" && suffix === undefined) {
|
||||
suffix = (
|
||||
<TextFieldIcon
|
||||
as={ChevronLeftIcon}
|
||||
onClick={() => {
|
||||
internalInputRef.current?.click();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextFieldBase
|
||||
{...focusWithinProps}
|
||||
@@ -204,34 +307,15 @@ export const TextField = React.forwardRef<HTMLDivElement, TextFieldProps>(
|
||||
>
|
||||
{/* We want input to be the first element in DOM so it receives the focus first */}
|
||||
<InputBase
|
||||
disabled={disabled}
|
||||
{...textFieldProps}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
ref={mergeRefs(internalInputRef, inputRef ?? null)}
|
||||
/>
|
||||
|
||||
{prefix && (
|
||||
<Flex
|
||||
css={{
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
order: 0,
|
||||
}}
|
||||
>
|
||||
{prefix}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{suffix && (
|
||||
<Flex
|
||||
css={{
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
order: 2,
|
||||
}}
|
||||
>
|
||||
{suffix}
|
||||
</Flex>
|
||||
)}
|
||||
{prefix && <PrefixSlot>{prefix}</PrefixSlot>}
|
||||
{suffix && <SuffixSlot>{suffix}</SuffixSlot>}
|
||||
</TextFieldBase>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const Circle = styled(Box, {
|
||||
width: CIRCLE_SIZE,
|
||||
height: CIRCLE_SIZE,
|
||||
position: "absolute",
|
||||
border: "solid $dropPlacement",
|
||||
border: "solid $blue10",
|
||||
borderWidth: 2,
|
||||
borderRadius: "50%",
|
||||
pointerEvents: "none",
|
||||
@@ -32,7 +32,7 @@ const Circle = styled(Box, {
|
||||
const Line = styled(Box, {
|
||||
boxSizing: "content-box",
|
||||
position: "absolute",
|
||||
background: "$dropPlacement",
|
||||
background: "$blue10",
|
||||
pointerEvents: "none",
|
||||
outline: "solid $loContrast",
|
||||
outlineWidth: OUTLINE_WIDTH,
|
||||
|
||||
@@ -10,7 +10,8 @@ export { Label } from "./components/label";
|
||||
export * as ToggleGroup from "./components/toggle-group";
|
||||
export * from "./components/progress-radial";
|
||||
export * from "./components/select";
|
||||
export { TextField } from "./components/text-field";
|
||||
export * from "./components/text-field";
|
||||
export { SearchField } from "./components/search-field";
|
||||
export { Flex } from "./components/flex";
|
||||
export { Image } from "./components/image";
|
||||
export { Grid } from "./components/grid";
|
||||
|
||||
@@ -195,9 +195,9 @@ const {
|
||||
// Semantic colors
|
||||
hiContrast: "$slate12",
|
||||
loContrast: "$slate1",
|
||||
hint: "$slate10",
|
||||
hint: "$slate9",
|
||||
muted: "$slate6",
|
||||
primary: "$blue12",
|
||||
primary: "$blue10",
|
||||
canvas: "hsl(0 0% 93%)",
|
||||
panel: "white",
|
||||
panelOutline: "hsl(0deg 0% 0% / 10%)",
|
||||
@@ -206,7 +206,6 @@ const {
|
||||
shadowDark: "hsl(206 22% 7% / 20%)",
|
||||
background: "$slate1",
|
||||
text: "$slate12",
|
||||
dropPlacement: "$blue9",
|
||||
},
|
||||
fonts: {
|
||||
sans: "Inter, -apple-system, system-ui, sans-serif",
|
||||
@@ -478,12 +477,11 @@ export const darkTheme = createTheme({
|
||||
...goldDarkA,
|
||||
|
||||
// Semantic colors
|
||||
hint: "$slate10",
|
||||
muted: "$slate8",
|
||||
primary: "$blue9",
|
||||
hiContrast: "$slate12",
|
||||
// radix colors in this range are either too dark/too light, nothing gray/black in the 16-17% luminosity range
|
||||
loContrast: "hsl(0 0% 17%)",
|
||||
hint: "$slate9",
|
||||
muted: "$slate8",
|
||||
primary: "$blue10",
|
||||
canvas: "hsl(0 0% 15%)",
|
||||
panel: "$slate3",
|
||||
panelOutline: "hsl(0deg 0% 100% / 10%)",
|
||||
@@ -492,6 +490,5 @@ export const darkTheme = createTheme({
|
||||
shadowDark: "hsl(206 22% 7% / 20%)",
|
||||
background: "$slate12",
|
||||
text: "$slate1",
|
||||
dropPlacement: "$blue9",
|
||||
},
|
||||
});
|
||||