## Description

Share UI implemented as a story



https://user-images.githubusercontent.com/52824/215069285-0ee7b214-8218-4ead-a468-07a22f65e433.mp4



## Steps for reproduction

1. open stories in designer
2. share project section

## Code Review

- [ ] hi @rpominov , I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview
- [ ] hi @istarkov, please check if the interface I made fits the data
layer you built

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
5de6)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio-designer/blob/main/apps/designer/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env.example`
and the `designer/env-check.js` if mandatory
This commit is contained in:
Oleg Isonen authored and GitHub committed 2023-01-27 18:38:58 +01:00
1 parent 3f4cc69976
commit cba726a58f
16 files changed
+444 -66

No files matched your search

@@ -1,22 +1,25 @@
import {
Button,
Flex,
DeprecatedIconButton,
Popover,
PopoverContent,
PopoverPortal,
PopoverTrigger,
TextField,
} from "@webstudio-is/design-system";
import { Share1Icon } from "@webstudio-is/icons";
import { useIsShareDialogOpen } from "../../shared/nano-states";
import { theme } from "@webstudio-is/design-system";
import {
ShareProjectDeprecated,
ShareProject,
LinkOptions,
} from "~/shared/share-project";
type ShareButtonProps = {
type ShareButtonDeprecatedProps = {
url: string;
};
const Content = ({ url }: ShareButtonProps) => {
const Content = ({ url }: ShareButtonDeprecatedProps) => {
if (typeof location === "undefined") {
return null;
}
@@ -29,31 +32,12 @@ const Content = ({ url }: ShareButtonProps) => {
event.preventDefault();
}}
>
<form
onSubmit={(event) => {
event.preventDefault();
window.open(url, "_blank");
}}
>
<Flex gap="2">
<TextField
variant="ghost"
readOnly
defaultValue={url}
onFocus={(event) => {
event?.target.select();
}}
/>
<Button aria-label="Open in a new tab" type="submit">
Open
</Button>
</Flex>
</form>
<ShareProjectDeprecated url={url} />
</PopoverContent>
);
};
export const ShareButton = (props: ShareButtonProps) => {
export const ShareButtonDeprecated = (props: ShareButtonDeprecatedProps) => {
const [isOpen, setIsOpen] = useIsShareDialogOpen();
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
@@ -68,3 +52,24 @@ export const ShareButton = (props: ShareButtonProps) => {
</Popover>
);
};
export const ShareButton = () => {
const handleChange = (link: LinkOptions) => {
// @todo implement
};
const handleDelete = (link: LinkOptions) => {
// @todo implement
};
const handleCreate = () => {
// @todo implement
};
return (
<ShareProject
onChange={handleChange}
onDelete={handleDelete}
onCreate={handleCreate}
>
<Button>Share</Button>
</ShareProject>
);
};
@@ -1,13 +1,14 @@
import type { Publish } from "~/shared/pubsub";
import { darkTheme, Flex, type CSS } from "@webstudio-is/design-system";
import { PreviewButton } from "./preview";
import { ShareButton } from "./share";
import { ShareButton, ShareButtonDeprecated } from "./share";
import { PublishButton } from "./publish";
import { SyncStatus } from "./sync-status";
import { Menu } from "./menu";
import { Breakpoints } from "../breakpoints";
import type { Project } from "@webstudio-is/project";
import { theme } from "@webstudio-is/design-system";
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
type TopbarProps = {
css: CSS;
@@ -55,7 +56,8 @@ export const Topbar = ({ css, project, publish, previewUrl }: TopbarProps) => {
>
<SyncStatus />
<PreviewButton publish={publish} />
<ShareButton url={previewUrl} />
<ShareButtonDeprecated url={previewUrl} />
{isFeatureEnabled("share2") && <ShareButton />}
<PublishButton project={project} />
</Flex>
</Flex>
@@ -0,0 +1,2 @@
export { ShareProject as ShareProjectDeprecated } from "./share-project";
export * from "./share-project2";
@@ -0,0 +1,69 @@
import { v4 as uuid } from "uuid";
import type { ComponentStory } from "@storybook/react";
import { Button } from "@webstudio-is/design-system";
import { useState } from "react";
import { type LinkOptions, ShareProject } from "./share-project2";
export default {
component: ShareProject,
};
const initialLinks: Array<LinkOptions> = [
{
url: `https://google.com/${uuid()}`,
name: "View Only",
permission: "view",
},
{
url: `https://google.com/${uuid()}`,
name: "View and Edit",
permission: "edit",
},
{
url: `https://google.com/${uuid()}`,
name: "Build",
permission: "build",
},
];
const useShareProject = (initialLinks: Array<LinkOptions> = []) => {
const [links, setLinks] = useState(initialLinks);
const onChange = (updatedLink: LinkOptions) => {
setLinks(
links.map((link) => (link.url === updatedLink.url ? updatedLink : link))
);
};
const onDelete = (deletedLink: LinkOptions) => {
setLinks(links.filter((link) => link.url !== deletedLink.url));
};
const onCreate = () => {
setLinks([
...links,
{
url: `https://google.com/${uuid()}`,
name: "Custom Link",
permission: "view",
},
]);
};
return { links, onChange, onDelete, onCreate };
};
export const Empty: ComponentStory<typeof ShareProject> = () => {
const props = useShareProject();
return (
<ShareProject {...props} isOpen>
<Button>Share</Button>
</ShareProject>
);
};
export const WithLinks: ComponentStory<typeof ShareProject> = () => {
const props = useShareProject(initialLinks);
return (
<ShareProject {...props} isOpen>
<Button>Share</Button>
</ShareProject>
);
};
@@ -0,0 +1,28 @@
import { Button, Flex, TextField } from "@webstudio-is/design-system";
type ShareProjectProps = { url: string };
export const ShareProject = ({ url }: ShareProjectProps) => {
return (
<form
onSubmit={(event) => {
event.preventDefault();
window.open(url, "_blank");
}}
>
<Flex gap="2">
<TextField
variant="ghost"
readOnly
defaultValue={url}
onFocus={(event) => {
event?.target.select();
}}
/>
<Button aria-label="Open in a new tab" type="submit">
Open
</Button>
</Flex>
</form>
);
};
@@ -0,0 +1,263 @@
import {
Box,
Button,
css,
Flex,
Label,
Popover,
PopoverContent,
PopoverHeader,
PopoverPortal,
PopoverTrigger,
rawTheme,
Separator,
Switch,
TextField,
theme,
Tooltip,
useId,
} from "@webstudio-is/design-system";
import { CopyIcon, InfoIcon, MenuIcon, PlusIcon } from "@webstudio-is/icons";
import { Fragment, type ComponentProps } from "react";
const Item = (props: ComponentProps<typeof Flex>) => (
<Flex
direction="column"
css={{ padding: theme.spacing[7] }}
gap="1"
{...props}
/>
);
type PermissionProps = {
title: string;
info: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
};
const Permission = ({
title,
info,
checked,
onCheckedChange,
}: PermissionProps) => {
const id = useId();
return (
<Flex align="center" gap="1">
<Switch checked={checked} id={id} onCheckedChange={onCheckedChange} />
<Label htmlFor={id}>{title}</Label>
<Tooltip content={info} variant="wrapped">
<InfoIcon color={rawTheme.colors.foregroundSubtle} tabIndex={0} />
</Tooltip>
</Flex>
);
};
type MenuProps = {
permission: Permission;
name: string;
onChangePermission: (permission: Permission) => void;
onChangeName: (name: string) => void;
onDelete: () => void;
};
const Menu = ({
permission,
name,
onChangePermission,
onChangeName,
onDelete,
}: MenuProps) => {
const handleCheckedChange =
(permission: Permission) => (checked: boolean) => {
if (checked) {
onChangePermission(permission);
}
};
return (
<Popover>
<PopoverTrigger asChild>
<Button
prefix={<MenuIcon />}
variant="ghost"
aria-label="Menu Button for options"
></Button>
</PopoverTrigger>
<PopoverContent>
<Item>
<Label>Name</Label>
<TextField
defaultValue="Custom link"
autoFocus
value={name}
onChange={(event) => {
onChangeName(event.target.value);
}}
/>
</Item>
<Separator />
<Item>
<Label>Permissions</Label>
<Permission
checked={permission === "view"}
onCheckedChange={handleCheckedChange("view")}
title="View"
info="Recipients can only view the site"
/>
<Permission
onCheckedChange={handleCheckedChange("edit")}
checked={permission === "edit"}
title="Edit Content"
info="Recipients can view the site and edit content like text and images, but they will not be able to change the styles or structure of your site."
/>
<Permission
onCheckedChange={handleCheckedChange("build")}
checked={permission === "build"}
title="Build"
info="Recipients can view the site and edit content like text and images and change the styles or structure of your site."
/>
</Item>
<Separator />
<Item>
{/* @todo need a menu item that looks like one from dropdown but without DropdownMenu */}
<Button
variant="destructive"
onClick={() => {
onDelete();
}}
>
Delete Link
</Button>
</Item>
</PopoverContent>
</Popover>
);
};
const itemStyle = css({
display: "flex",
alignItems: "center",
gap: theme.spacing[3],
py: theme.spacing[5],
px: theme.spacing[9],
});
type Permission = "view" | "edit" | "build";
export type LinkOptions = {
url: string;
name: string;
permission: Permission;
};
type SharedLinkItemType = LinkOptions & {
onChangePermission: (permission: Permission) => void;
onChangeName: (name: string) => void;
onDelete: () => void;
};
const SharedLinkItem = ({
url,
name,
permission,
onChangePermission,
onChangeName,
onDelete,
}: SharedLinkItemType) => {
return (
<Box className={itemStyle()}>
<Label css={{ flexGrow: 1 }}>{name}</Label>
<Button
prefix={<CopyIcon />}
onClick={() => {
navigator.clipboard.writeText(url);
}}
>
Copy link
</Button>
<Menu
name={name}
permission={permission}
onChangePermission={onChangePermission}
onChangeName={onChangeName}
onDelete={onDelete}
/>
</Box>
);
};
type ShareProjectProps = {
links?: Array<LinkOptions>;
onChange: (link: LinkOptions) => void;
onDelete: (link: LinkOptions) => void;
onCreate: () => void;
children: JSX.Element;
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
};
export const ShareProject = ({
links = [],
onChange,
onDelete,
onCreate,
children,
isOpen,
onOpenChange,
}: ShareProjectProps) => {
const items = links.map((link) => (
<Fragment key={link.url}>
<SharedLinkItem
key={link.url}
{...link}
onChangePermission={(permission) => {
onChange({ ...link, permission });
}}
onChangeName={(name) => {
onChange({ ...link, name });
}}
onDelete={() => {
onDelete(link);
}}
/>
<Separator />
</Fragment>
));
const create = (
<Box className={itemStyle({ css: { py: theme.spacing["9"] } })}>
<Button
variant="neutral"
prefix={<PlusIcon />}
onClick={() => {
onCreate();
}}
>
{links.length === 0 ? "Share a custom link" : "Add another link"}
</Button>
</Box>
);
// @todo this should use FloatingPanel instead of Popover, which we need to refactor first
return (
<Popover modal open={isOpen} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverPortal>
<PopoverContent hideArrow>
<Flex
direction="column"
css={{
width: theme.spacing[33],
}}
>
{items}
{create}
</Flex>
<PopoverHeader title="Share" />
</PopoverContent>
</PopoverPortal>
</Popover>
);
};
@@ -3,7 +3,7 @@
* https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A3274
*/
import type { LabelHTMLAttributes, ReactNode, Ref } from "react";
import type { ComponentProps, ReactNode, Ref } from "react";
import { forwardRef } from "react";
import { typography } from "../__generated__/figma-design-tokens";
import { styled, theme } from "../stitches.config";
@@ -62,7 +62,7 @@ const StyledLabel = styled("label", typography.labelSentenceCase, {
},
});
type Props = LabelHTMLAttributes<HTMLLabelElement> & {
type Props = ComponentProps<typeof StyledLabel> & {
color?: "default" | "preset" | "local" | "remote";
disabled?: boolean;
children: ReactNode;
@@ -22,7 +22,7 @@ const StyledContent = styled(PopoverPrimitive.Content, panelStyles, {
backgroundColor: "white",
minWidth: 200,
minHeight: theme.spacing[13],
maxWidth: 265,
maxWidth: "max-content",
"&:focus": {
outline: "none",
},
@@ -11,26 +11,15 @@ export const Separator = styled(SeparatorPrimitive.Root, {
variants: {
size: {
"1": {
1: {
'&[data-orientation="horizontal"]': {
height: theme.spacing[1],
width: theme.spacing[9],
my: theme.spacing[5],
},
'&[data-orientation="vertical"]': {
width: theme.spacing[1],
height: theme.spacing[9],
},
},
"2": {
'&[data-orientation="horizontal"]': {
height: theme.spacing[2],
width: theme.spacing[17],
},
'&[data-orientation="vertical"]': {
width: theme.spacing[2],
height: theme.spacing[17],
mx: theme.spacing[5],
},
},
auto: {
@@ -4,7 +4,7 @@ import { keyframes, styled } from "../stitches.config";
import {
CheckCircledIcon,
CrossCircledIcon,
InfoCircledIcon,
InfoIcon,
} from "@webstudio-is/icons";
import { Box } from "./box";
import { theme } from "../stitches.config";
@@ -120,7 +120,7 @@ export const Toaster = () => {
>
{toast.type === "success" && <CheckCircledIcon />}
{toast.type === "error" && <CrossCircledIcon />}
{toast.type === "blank" && <InfoCircledIcon />}
{toast.type === "blank" && <InfoIcon />}
</Box>
<StyledTitle>{toast.message}</StyledTitle>
</StyledToast>
@@ -7,10 +7,9 @@ import type { CSS } from "../stitches.config";
import { theme } from "../stitches.config";
export type TooltipProps = ComponentProps<typeof TooltipPrimitive.Root> &
ComponentProps<typeof TooltipPrimitive.Content> & {
ComponentProps<typeof Content> & {
children: React.ReactElement;
content: React.ReactNode;
multiline?: boolean;
delayDuration?: number;
disableHoverableContent?: boolean;
css?: CSS;
@@ -20,16 +19,18 @@ const Content = styled(TooltipPrimitive.Content, {
backgroundColor: theme.colors.hiContrast,
color: theme.colors.loContrast,
borderRadius: theme.borderRadius[4],
padding: `${theme.spacing[3]} ${theme.spacing[5]}`,
padding: theme.spacing[5],
zIndex: theme.zIndices[1],
position: "relative",
variants: {
multiline: {
true: {
// @todo make this part of the design system
maxWidth: 110,
pb: 7,
variant: {
wrapped: {
maxWidth: theme.spacing["29"],
},
large: {
maxWidth: theme.spacing["32"],
padding: theme.spacing[9],
},
},
},
@@ -46,7 +47,6 @@ export const Tooltip = React.forwardRef(
children,
content,
defaultOpen,
multiline,
delayDuration,
disableHoverableContent,
open,
@@ -80,7 +80,6 @@ export const Tooltip = React.forwardRef(
align="center"
sideOffset={5}
{...props}
multiline={multiline}
>
<Paragraph>{content}</Paragraph>
<Box css={{ color: theme.colors.transparentExtreme }}>
@@ -159,7 +159,7 @@ const spacing = {
33: "320px",
34: "384px",
35: "448px",
};
} as const;
const {
styled,
+6
View File
@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8.76075 5.62758C8.76075 5.21336 8.42496 4.87758 8.01075 4.87758C7.59653 4.87758 7.26075 5.21336 7.26075 5.62758V8.45629C7.26075 8.87051 7.59653 9.20629 8.01075 9.20629C8.42496 9.20629 8.76075 8.8705 8.76075 8.45629V5.62758Z" />
<path d="M8.00001 9.62614C7.58579 9.62614 7.25001 9.96193 7.25001 10.3761C7.25001 10.7904 7.58579 11.1261 8.00001 11.1261H8.00672C8.42094 11.1261 8.75672 10.7904 8.75672 10.3761C8.75672 9.96193 8.42094 9.62614 8.00672 9.62614H8.00001Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 2.35001C6.50153 2.35001 5.06443 2.94527 4.00485 4.00485C2.94527 5.06443 2.35001 6.50153 2.35001 8.00001C2.35001 8.74197 2.49615 9.47668 2.78009 10.1622C3.06403 10.8477 3.4802 11.4705 4.00485 11.9952C4.5295 12.5198 5.15235 12.936 5.83784 13.2199C6.52333 13.5039 7.25804 13.65 8.00001 13.65C8.74197 13.65 9.47668 13.5039 10.1622 13.2199C10.8477 12.936 11.4705 12.5198 11.9952 11.9952C12.5198 11.4705 12.936 10.8477 13.2199 10.1622C13.5039 9.47668 13.65 8.74197 13.65 8.00001C13.65 6.50153 13.0547 5.06443 11.9952 4.00485C10.9356 2.94527 9.49848 2.35001 8.00001 2.35001ZM4.92409 4.92409C5.73987 4.10831 6.84631 3.65001 8.00001 3.65001C9.1537 3.65001 10.2601 4.10831 11.0759 4.92409C11.8917 5.73987 12.35 6.84631 12.35 8.00001C12.35 8.57126 12.2375 9.13691 12.0189 9.66468C11.8003 10.1924 11.4799 10.672 11.0759 11.0759C10.672 11.4799 10.1924 11.8003 9.66468 12.0189C9.13691 12.2375 8.57126 12.35 8.00001 12.35C7.42876 12.35 6.8631 12.2375 6.33533 12.0189C5.80757 11.8003 5.32803 11.4799 4.92409 11.0759C4.52016 10.672 4.19974 10.1924 3.98113 9.66468C3.76252 9.13691 3.65001 8.57126 3.65001 8.00001C3.65001 6.84631 4.10831 5.73987 4.92409 4.92409Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+9 -8
View File
@@ -1,12 +1,4 @@
export * from "./auto-scroll";
export * from "./eyecon-closed";
export * from "./eyecon-open";
export * from "./linked";
export * from "./scroll";
export * from "./subscript";
export * from "./superscript";
export * from "./unlinked";
export * from "./webstudio";
export * from "./chevron-big-down";
export * from "./chevron-big-left";
export * from "./chevron-big-right";
@@ -16,3 +8,12 @@ export * from "./chevron-filled-left";
export * from "./chevron-filled-right";
export * from "./chevron-filled-up";
export * from "./dot";
export * from "./eyecon-closed";
export * from "./eyecon-open";
export * from "./info";
export * from "./linked";
export * from "./scroll";
export * from "./subscript";
export * from "./superscript";
export * from "./unlinked";
export * from "./webstudio";
+15
View File
@@ -0,0 +1,15 @@
// Generated from icons/info.svg
import { forwardRef } from "react";
import { IconProps } from "../types";
// prettier-ignore
export const InfoIcon = forwardRef<SVGSVGElement, IconProps>(
({ color = "currentColor", ...props }, forwardedRef) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill={color} {...props} ref={forwardedRef}><path d="M8.76 5.628a.75.75 0 1 0-1.5 0v2.828a.75.75 0 1 0 1.5 0V5.628ZM8 9.626a.75.75 0 1 0 0 1.5h.007a.75.75 0 1 0 0-1.5H8Z" /><path fillRule="evenodd" d="M8 2.35a5.65 5.65 0 1 0 0 11.3 5.65 5.65 0 0 0 0-11.3ZM4.924 4.924a4.35 4.35 0 1 1 6.152 6.152 4.35 4.35 0 0 1-6.152-6.152Z" clipRule="evenodd" /></svg>
);
}
);
InfoIcon.displayName = "InfoIcon";
-1
View File
@@ -24,7 +24,6 @@ export {
HeadingIcon,
TextIcon,
SquareIcon,
InfoCircledIcon,
CrossCircledIcon,
CheckCircledIcon,
TokensIcon,