Merge branch 'main' into text-control-prefix-icon

This commit is contained in:
Oleg Isonen authored and GitHub committed 2022-11-07 16:17:07 +01:00
commit dadb6dc43c
15 files changed
+327 -193

No files matched your search

@@ -29,7 +29,6 @@ export const FontFamilyControl = ({
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}
state={isOpen ? "active" : undefined}
@@ -30,13 +30,12 @@ export const ImageControl = ({
title="Images"
content={
<ImageManager
onSelect={(asset) => {
onChange={(asset) => {
setValue(asset.id);
}}
/>
}
>
{/* @todo this should be part of the design system, probably a varian="button" */}
<TextField defaultValue={selectedAsset?.name} />
</ValuePickerPopover>
);
@@ -0,0 +1,42 @@
import { type ComponentProps } from "react";
import type { AssetType } from "@webstudio-is/asset-uploader";
import { Flex, SearchField } from "@webstudio-is/design-system";
import { AssetUpload } from "./asset-upload";
import { NotFound } from "./not-found";
import { Separator } from "./separator";
type AssetsShellProps = {
searchProps: ComponentProps<typeof SearchField>;
children: JSX.Element;
type: AssetType;
isEmpty: boolean;
};
export const AssetsShell = ({
searchProps,
isEmpty,
children,
type,
}: AssetsShellProps) => {
return (
<Flex
direction="column"
css={{ overflow: "hidden", paddingTop: "$1", paddingBottom: "$3" }}
>
<Flex css={{ py: "$2", px: "$3" }} gap="2" direction="column">
<AssetUpload type={type} />
<SearchField {...searchProps} autoFocus placeholder="Search" />
</Flex>
<Separator />
{isEmpty && <NotFound />}
<Flex
css={{
flexDirection: "column",
px: "$3",
}}
>
{children}
</Flex>
</Flex>
);
};
@@ -1,4 +1,6 @@
export * from "./asset-upload";
export * from "./use-assets";
export * from "./types";
export * from "./use-set-assets";
export * from "./separator";
export * from "./use-search";
export * from "./assets-shell";
@@ -0,0 +1,9 @@
import { Flex } from "@webstudio-is/design-system";
export const NotFound = () => {
return (
<Flex align="center" justify="center" css={{ height: 100 }}>
No matching assets
</Flex>
);
};
@@ -0,0 +1,9 @@
import {
Separator as SeparatorPrimitive,
styled,
} from "@webstudio-is/design-system";
export const Separator = styled(SeparatorPrimitive, {
marginTop: "$1",
marginBottom: "$2",
});
@@ -1,9 +1,8 @@
import { Asset } from "@webstudio-is/asset-uploader";
export type PreviewAsset = Pick<
Asset,
"path" | "name" | "id" | "status" | "format"
>;
export type PreviewAsset = Pick<Asset, "path" | "name" | "id" | "format"> & {
status: "uploading";
};
export type ActionData = {
uploadedAssets?: Array<Asset>;
@@ -0,0 +1,28 @@
import { useCallback, useEffect, useRef, useState } from "react";
export const useFilter = <Item>({
items,
onReset,
}: {
onReset: () => void;
items: Array<Item>;
}) => {
const [filteredItems, setFilteredItems] = useState(items);
const onResetRef = useRef(onReset);
onResetRef.current = onReset;
const resetFilteredItems = useCallback(() => {
setFilteredItems(items);
}, [items]);
useEffect(() => {
setFilteredItems(items);
onResetRef.current();
}, [items]);
return {
filteredItems,
resetFilteredItems,
setFilteredItems,
};
};
@@ -1,15 +1,16 @@
import {
Flex,
Separator as SeparatorPrimitive,
SearchField,
List,
ListItem,
useList,
findNextListIndex,
styled,
} from "@webstudio-is/design-system";
import { AssetUpload, useAssets } from "~/designer/shared/assets";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
AssetsShell,
Separator,
useAssets,
useSearch,
} from "~/designer/shared/assets";
import { useMemo, useState } from "react";
import { useMenu } from "./item-menu";
import { CheckIcon } from "@webstudio-is/icons";
import {
@@ -19,38 +20,7 @@ import {
groupItemsByType,
toItems,
} from "./item-utils";
import { useSearch } from "./use-search";
const NotFound = () => {
return (
<Flex align="center" justify="center" css={{ height: 100 }}>
Font not found
</Flex>
);
};
const useFilteredItems = ({ onReset }: { onReset: () => void }) => {
const { assets } = useAssets("font");
const fontItems = useMemo(() => toItems(assets), [assets]);
const [filteredItems, setFilteredItems] = useState(fontItems);
const onResetRef = useRef(onReset);
onResetRef.current = onReset;
const resetFilteredItems = useCallback(() => {
setFilteredItems(fontItems);
}, [fontItems]);
useEffect(() => {
setFilteredItems(fontItems);
onResetRef.current();
}, [fontItems]);
return {
filteredItems,
resetFilteredItems,
setFilteredItems,
};
};
import { useFilter } from "../assets/use-filter";
const useLogic = ({
onChange,
@@ -61,12 +31,13 @@ const useLogic = ({
}) => {
const { assets, handleDelete: handleDeleteAssets } = useAssets("font");
const [selectedIndex, setSelectedIndex] = useState(-1);
const { filteredItems, resetFilteredItems, setFilteredItems } =
useFilteredItems({
onReset() {
searchProps.onCancel();
},
});
const fontItems = useMemo(() => toItems(assets), [assets]);
const { filteredItems, resetFilteredItems, setFilteredItems } = useFilter({
items: fontItems,
onReset() {
searchProps.onCancel();
},
});
const { uploadedItems, systemItems, groupedItems } = useMemo(
() => groupItemsByType(filteredItems),
[filteredItems]
@@ -136,11 +107,6 @@ const useLogic = ({
};
};
const Separator = styled(SeparatorPrimitive, {
marginTop: "$1",
marginBottom: "$2",
});
type FontsManagerProps = {
value: string;
onChange: (value: string) => void;
@@ -180,46 +146,33 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
};
return (
<Flex
direction="column"
css={{ overflow: "hidden", paddingTop: "$1", paddingBottom: "$3" }}
<AssetsShell
searchProps={searchProps}
type="font"
isEmpty={groupedItems.length === 0}
>
<Flex css={{ py: "$2", px: "$3" }} gap="2" direction="column">
<AssetUpload type="font" />
<SearchField {...searchProps} autoFocus placeholder="Search" />
</Flex>
<Separator />
{groupedItems.length === 0 && <NotFound />}
<Flex
css={{
flexDirection: "column",
gap: "$3",
px: "$3",
<List
{...listProps}
onBlur={(event) => {
if (isMenuOpen === false) {
listProps.onBlur(event);
}
}}
>
<List
{...listProps}
onBlur={(event) => {
if (isMenuOpen === false) {
listProps.onBlur(event);
}
}}
>
{uploadedItems.length !== 0 && (
<ListItem state="disabled">{"Uploaded"}</ListItem>
)}
{uploadedItems.map(renderItem)}
{systemItems.length !== 0 && (
<>
{uploadedItems.length !== 0 && <Separator />}
<ListItem state="disabled">{"System"}</ListItem>
</>
)}
{systemItems.map((item, index) =>
renderItem(item, index + uploadedItems.length)
)}
</List>
</Flex>
</Flex>
{uploadedItems.length !== 0 && (
<ListItem state="disabled">{"Uploaded"}</ListItem>
)}
{uploadedItems.map(renderItem)}
{systemItems.length !== 0 && (
<>
{uploadedItems.length !== 0 && <Separator />}
<ListItem state="disabled">{"System"}</ListItem>
</>
)}
{systemItems.map((item, index) =>
renderItem(item, index + uploadedItems.length)
)}
</List>
</AssetsShell>
);
};
@@ -0,0 +1,20 @@
import { Flex, Text } from "@webstudio-is/design-system";
import type { ComponentProps } from "react";
type FilenameProps = Omit<ComponentProps<typeof Text>, "children"> & {
children: string;
};
export const Filename = ({ children, ...props }: FilenameProps) => {
const splitName = children.split(".");
const extension = splitName[splitName.length - 1];
const baseName = children.substr(0, children.length - extension.length - 1);
return (
<Flex>
<Text truncate {...props}>
{baseName}
</Text>
<Text {...props}>{extension}</Text>
</Flex>
);
};
@@ -8,41 +8,7 @@ import {
} from "@webstudio-is/icons";
import prettyBytes from "pretty-bytes";
import { Asset } from "@webstudio-is/asset-uploader";
const Filename = ({ name }: { name: string }) => {
const splitName = name.split(".");
const extension = splitName[splitName.length - 1];
const lastLetterBeforeExtension = splitName[splitName.length - 2]
.split("")
.pop();
return (
<Box
css={{
position: "relative",
maxWidth: "70%",
}}
>
<Text
variant="label"
data-extension={`${lastLetterBeforeExtension}.${extension}`}
css={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
"&::after": {
content: "attr(data-extension)",
position: "absolute",
left: "100%",
top: 0,
whiteSpace: "nowrap",
},
}}
>
{name}
</Text>
</Box>
);
};
import { Filename } from "./filename";
type ImageInfoProps = {
asset: Asset;
@@ -56,7 +22,7 @@ export const ImageInfo = ({ asset, onDelete }: ImageInfoProps) => {
<Box css={{ p: "$2 $3" }}>
<Grid columns={2} align="center" gap={2}>
<Box css={{ width: 100 }}>
<Filename name={name} />
<Filename variant="label">{name}</Filename>
</Box>
<Flex align="center" css={{ gap: "$1" }}>
<CloudIcon />
@@ -1,29 +1,101 @@
import { Box, Flex, Grid } from "@webstudio-is/design-system";
import { useState } from "react";
import { findNextListIndex, Grid } from "@webstudio-is/design-system";
import type { Asset } from "@webstudio-is/asset-uploader";
import { AssetsShell, PreviewAsset, useAssets, useSearch } from "../assets";
import { useFilter } from "../assets/use-filter";
import { ImageThumbnail } from "./image-thumbnail";
import { AssetUpload, useAssets } from "../assets";
import { matchSorter } from "match-sorter";
type ImageManagerProps = {
onSelect?: (asset: Asset) => void;
const filterItems = (search: string, items: Array<Asset | PreviewAsset>) => {
return matchSorter(items, search, {
keys: [(item) => item.name],
});
};
export const ImageManager = ({ onSelect }: ImageManagerProps) => {
const useLogic = ({ onChange }: { onChange?: (asset: Asset) => void }) => {
const { assets, handleDelete } = useAssets("image");
const [selectedIndex, setSelectedIndex] = useState(-1);
const { filteredItems, resetFilteredItems, setFilteredItems } = useFilter({
items: assets,
onReset() {
searchProps.onCancel();
},
});
const searchProps = useSearch({
onCancel: resetFilteredItems,
onSearch(search) {
if (search === "") {
return resetFilteredItems();
}
const items = filterItems(search, assets);
setFilteredItems(items);
},
onSelect(direction) {
if (direction === "current") {
setSelectedIndex(selectedIndex);
const asset = filteredItems[selectedIndex];
if (asset?.status === "uploaded") {
onChange?.(asset);
}
return;
}
const nextIndex = findNextListIndex(
selectedIndex,
filteredItems.length,
direction
);
setSelectedIndex(nextIndex);
},
});
const handleSelect = (asset?: Asset | PreviewAsset) => {
const selectedIndex = filteredItems.findIndex(
(item) => item.id === asset?.id
);
setSelectedIndex(selectedIndex);
};
return {
searchProps,
handleDelete,
filteredItems,
handleSelect,
selectedIndex,
};
};
type ImageManagerProps = {
onChange?: (asset: Asset) => void;
};
export const ImageManager = ({ onChange }: ImageManagerProps) => {
const {
handleDelete,
handleSelect,
filteredItems,
searchProps,
selectedIndex,
} = useLogic({ onChange });
return (
<Flex gap="3" direction="column" css={{ padding: "$1", paddingTop: "$2" }}>
<Box css={{ padding: "$2" }}>
<AssetUpload type="image" />
</Box>
<Grid columns={2} gap={2}>
{assets.map((asset) => (
<AssetsShell
searchProps={searchProps}
isEmpty={filteredItems.length === 0}
type="image"
>
<Grid columns={3} gap={2}>
{filteredItems.map((asset, index) => (
<ImageThumbnail
key={asset.id}
asset={asset}
onDelete={handleDelete}
onSelect={onSelect}
onSelect={handleSelect}
onChange={onChange}
state={index === selectedIndex ? "selected" : undefined}
/>
))}
</Grid>
</Flex>
</AssetsShell>
);
};
@@ -1,11 +1,17 @@
import { useEffect, useState } from "react";
import { Box } from "@webstudio-is/design-system";
import {
useEffect,
useState,
type KeyboardEvent,
type FocusEvent,
} from "react";
import { Box, styled } from "@webstudio-is/design-system";
import placeholderImage from "~/shared/images/image-placeholder.svg";
import brokenImage from "~/shared/images/broken-image-placeholder.svg";
import { UploadingAnimation } from "./uploading-animation";
import { ImageInfoTrigger, imageInfoTriggerCssVars } from "./image-info-tigger";
import type { PreviewAsset } from "~/designer/shared/assets";
import { Asset } from "@webstudio-is/asset-uploader";
import type { Asset } from "@webstudio-is/asset-uploader";
import { Filename } from "./filename";
const useImageWithFallback = ({
path = placeholderImage,
@@ -24,72 +30,102 @@ const useImageWithFallback = ({
return src;
};
type ThumbnailProps = {
path?: string;
status: Asset["status"];
onClick: () => void;
};
const ThumbnailContainer = styled(Box, {
position: "relative",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
border: "2px solid transparent",
borderRadius: "$1",
outline: 0,
gap: "$1",
overflow: "hidden",
backgroundColor: "$slate4",
"&:hover": imageInfoTriggerCssVars({ show: true }),
variants: {
status: {
uploading: {
filter: "blur(1px)",
opacity: 0.7,
},
uploaded: {},
},
state: {
selected: {
boxShadow:
"0px 0px 0px 2px $colors$blue10, 0px 0px 0px 2px $colors$blue10",
},
},
},
});
const Thumbnail = ({ path, status, onClick }: ThumbnailProps) => {
const src = useImageWithFallback({ path });
const Thumbnail = styled(Box, {
width: "$8",
height: "$8",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
flexShrink: 0,
});
return (
<Box
css={{
backgroundImage: `url("${src}")`,
width: "100%",
height: "100%",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
position: "absolute",
left: 0,
top: 0,
...(status === "uploading"
? { filter: "blur(1px)", opacity: 0.7 }
: {}),
}}
onClick={onClick}
></Box>
);
type ImageThumbnailProps = {
asset: Asset | PreviewAsset;
onDelete: (ids: Array<string>) => void;
onSelect: (asset?: Asset | PreviewAsset) => void;
onChange?: (asset: Asset) => void;
state?: "selected";
};
export const ImageThumbnail = ({
asset,
onDelete,
onSelect,
}: {
asset: Asset | PreviewAsset;
onDelete: (ids: Array<string>) => void;
onSelect?: (asset: Asset) => void;
}) => {
onChange,
state,
}: ImageThumbnailProps) => {
const { path, status, name } = asset;
const description =
"description" in asset && asset.description ? asset.description : name;
const [isDeleting, setIsDeleting] = useState(false);
const isUploading = status === "uploading";
const isUploadedAsset = isUploading === false && "size" in asset;
const [isDeleting, setIsDeleting] = useState(false);
const src = useImageWithFallback({ path });
return (
<Box
<ThumbnailContainer
title={description}
css={{
aspectRatio: "1/1",
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "0 $2",
position: "relative",
"&:hover": imageInfoTriggerCssVars({ show: true }),
tabIndex={0}
status={status}
state={state}
onFocus={() => onSelect?.(asset)}
onBlur={(event: FocusEvent) => {
const isFocusWithin = event.currentTarget.contains(event.relatedTarget);
if (isFocusWithin === false) {
onSelect();
}
}}
onKeyDown={(event: KeyboardEvent) => {
if (event.code === "Enter" && asset.status === "uploaded") {
onChange?.(asset);
}
}}
>
<Thumbnail
path={path}
status={status}
css={{ backgroundImage: `url("${src}")` }}
onClick={() => {
if (isUploadedAsset) onSelect?.(asset);
if (isUploadedAsset) onChange?.(asset);
}}
/>
<Box
css={{
width: "100%",
// @todo should be a token from design system
height: 12,
}}
>
<Filename variant={"tiny"}>{name}</Filename>
</Box>
{isUploadedAsset && (
<ImageInfoTrigger
asset={asset}
@@ -100,6 +136,6 @@ export const ImageThumbnail = ({
/>
)}
{(isUploading || isDeleting) && <UploadingAnimation />}
</Box>
</ThumbnailContainer>
);
};
+1 -1
View File
@@ -5,7 +5,7 @@ import type { ImageMeta } from "./utils/format-asset";
type BaseAsset = Omit<DbAsset, "meta"> & {
path: string;
status?: "uploading" | "uploaded";
status?: "uploaded";
};
export type FontAsset = Omit<BaseAsset, "format"> & {