Dialog component & other updates to design-system components (#857)

1. Implemented Radix/Dialog with styles of Figma / Floating Panel:
[Demo](https://dialog-and-button--638245578b47d4399fba191a.chromatic.com/?path=/story/library-floating-panel-dialog--dialog).
Closes #831
2. Implemented Radix/Popover with styles of Figma / Floating Panel:
[Demo](https://dialog-and-button--638245578b47d4399fba191a.chromatic.com/?path=/story/library-floating-panel-popover--popover)
3. Aligned Separator with Figma
4. Aligned Title with Figma
5. Fixed some issues in Button. Closes #829
6. Cleaned up how demos under Library look visually
This commit is contained in:
Roman Pominov authored and GitHub committed 2023-01-30 20:02:08 +03:00
1 parent 90ce5a06b1
commit e4b96bc50b
38 files changed
+771 -323

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1 -1
View File
@@ -26,7 +26,7 @@ export const LoginButton = ({
{...props}
type="submit"
disabled={disabled}
variant="neutral"
color="neutral"
css={{ width: "100%" }}
>
<Flex gap="2" align="center">
+1 -1
View File
@@ -33,7 +33,7 @@ const Menu = ({ user }: { user: User }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" aria-label="Menu Button">
<Button color="ghost" aria-label="Menu Button">
<Flex gap="1" align="center" css={{ height: theme.spacing[11] }}>
<Avatar
src={user?.image || undefined}
@@ -77,7 +77,7 @@ const BreakpointEditorItem = ({
/>
<Button
type="button"
variant="ghost"
color="ghost"
onClick={() => {
onDelete(breakpoint);
}}
@@ -120,7 +120,7 @@ export const BreakpointsEditor = ({ onDelete }: BreakpointsEditorProps) => {
>
<Text>Breakpoints</Text>
<Button
variant="ghost"
color="ghost"
onClick={() => {
setAddedBreakpoints((prev) => [
...prev,
@@ -206,7 +206,7 @@ const Property = ({
{required ? (
<Box />
) : (
<Button variant="ghost" onClick={onDelete} prefix={<TrashIcon />} />
<Button color="ghost" onClick={onDelete} prefix={<TrashIcon />} />
)}
</>
);
@@ -256,7 +256,7 @@ export const PropsPanel = ({ selectedInstance, publish }: PropsPanelProps) => {
const addButton = (
<Button
variant="ghost"
color="ghost"
onClick={(event) => {
event.preventDefault();
addEmptyProp();
@@ -1,12 +1,11 @@
import {
Flex,
DeprecatedIconButton,
Text,
Button,
Tooltip,
Separator,
Title,
TitleSuffixSpacer,
} from "@webstudio-is/design-system";
import { Separator } from "@webstudio-is/design-system";
import { CrossIcon } from "@webstudio-is/icons";
import { theme } from "@webstudio-is/design-system";
type HeaderProps = {
title: string;
@@ -16,25 +15,27 @@ type HeaderProps = {
export const Header = ({ title, suffix }: HeaderProps) => {
return (
<>
<Flex
css={{ height: 40, paddingLeft: theme.spacing[9], flexShrink: 0 }}
align="center"
justify="between"
>
<Text variant="title">{title}</Text>
{suffix && (
<Flex css={{ marginRight: theme.spacing[5] }}>{suffix}</Flex>
)}
</Flex>
<Separator css={{ height: 2 }} />
<Title suffix={suffix}>{title}</Title>
<Separator />
</>
);
};
export const CloseButton = ({ onClick }: { onClick: () => void }) => (
<Tooltip content="Close panel" side="bottom">
<DeprecatedIconButton onClick={onClick} size="2" aria-label="Close panel">
<CrossIcon />
</DeprecatedIconButton>
export { TitleSuffixSpacer as HeaderSuffixSpacer };
export const CloseButton = ({
onClick,
label = "Close panel",
}: {
onClick: () => void;
label?: string;
}) => (
<Tooltip content={label} side="bottom">
<Button
onClick={onClick}
color="ghost"
prefix={<CrossIcon />}
aria-label={label}
/>
</Tooltip>
);
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "@remix-run/react";
import {
theme,
DeprecatedIconButton,
TreeItemLabel,
TreeItemBody,
@@ -11,6 +10,8 @@ import {
Flex,
Tooltip,
Box,
Button,
theme,
} from "@webstudio-is/design-system";
import {
ChevronRightIcon,
@@ -204,13 +205,12 @@ const PagesPanel = ({
<>
{onCreateNewPage && (
<Tooltip content="New page" side="bottom">
<DeprecatedIconButton
size="2"
<Button
onClick={() => onCreateNewPage()}
aria-label="New page"
>
<NewPageIcon />
</DeprecatedIconButton>
prefix={<NewPageIcon />}
color="ghost"
/>
</Tooltip>
)}
{onClose && <CloseButton onClick={onClose} />}
@@ -7,7 +7,6 @@ import { useFetcher } from "@remix-run/react";
import { type Page } from "@webstudio-is/project";
import {
theme,
DeprecatedIconButton,
Button,
Box,
DeprecatedLabel,
@@ -34,7 +33,7 @@ import type {
CreatePageData,
} from "~/shared/pages";
import { restPagesPath } from "~/shared/router-utils";
import { Header } from "../../header";
import { Header, HeaderSuffixSpacer } from "../../header";
const Group = styled(Flex, {
marginBottom: theme.spacing[9],
@@ -256,12 +255,6 @@ export const NewPageSettings = ({
);
};
const ButtonContainer = styled("div", {
ml: theme.spacing[5],
display: "flex",
alignItems: "center",
});
const NewPageSettingsView = ({
onSubmit,
isSubmitting,
@@ -280,23 +273,25 @@ const NewPageSettingsView = ({
<>
{onClose && (
<Tooltip content="Cancel" side="bottom">
<DeprecatedIconButton
size="2"
<Button
onClick={onClose}
aria-label="Cancel"
prefix={<ChevronDoubleLeftIcon />}
color="ghost"
// Tab should go:
// trought form fields -> create button -> cancel button
tabIndex={3}
>
<ChevronDoubleLeftIcon />
</DeprecatedIconButton>
/>
</Tooltip>
)}
<ButtonContainer>
<Button pending={isSubmitting} onClick={onSubmit} tabIndex={2}>
{isSubmitting ? "Creating" : "Create page"}
</Button>
</ButtonContainer>
<HeaderSuffixSpacer />
<Button
state={isSubmitting ? "pending" : "auto"}
onClick={onSubmit}
tabIndex={2}
>
{isSubmitting ? "Creating" : "Create page"}
</Button>
</>
}
/>
@@ -461,26 +456,24 @@ const PageSettingsView = ({
<>
{isHomePage === false && (
<Tooltip content="Delete page" side="bottom">
<DeprecatedIconButton
size="2"
<Button
color="ghost"
prefix={<TrashIcon />}
onClick={onDelete}
aria-label="Delete page"
tabIndex={2}
>
<TrashIcon />
</DeprecatedIconButton>
/>
</Tooltip>
)}
{onClose && (
<Tooltip content="Close page settings" side="bottom">
<DeprecatedIconButton
size="2"
<Button
color="ghost"
prefix={<ChevronDoubleLeftIcon />}
onClick={onClose}
aria-label="Close page settings"
tabIndex={2}
>
<ChevronDoubleLeftIcon />
</DeprecatedIconButton>
/>
</Tooltip>
)}
</>
@@ -63,7 +63,10 @@ const Content = ({ project }: PublishButtonProps) => {
{fetcher.data?.errors !== undefined && (
<Text color="error">{fetcher.data?.errors}</Text>
)}
<Button pending={fetcher.state !== "idle"} type="submit">
<Button
state={fetcher.state !== "idle" ? "pending" : "auto"}
type="submit"
>
{fetcher.state !== "idle" ? "Publishing" : "Publish"}
</Button>
</Flex>
@@ -1,9 +1,7 @@
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverPortal,
FloatingPanelPopover as Popover,
theme,
css,
} from "@webstudio-is/design-system";
import {
MutableRefObject,
@@ -13,7 +11,6 @@ import {
useState,
} from "react";
import { FloatingPanelContext } from "./floating-panel-provider";
import { theme } from "@webstudio-is/design-system";
const useSideOffset = ({
isOpen,
@@ -59,6 +56,8 @@ type FloatingPanelProps = {
onOpenChange?: (isOpen: boolean) => void;
};
const contetnStyles = css({ width: theme.spacing[30] });
export const FloatingPanel = ({
title,
content,
@@ -68,8 +67,8 @@ export const FloatingPanel = ({
const { isOpen, handleOpenChange, triggerRef, sideOffset } =
useLogic(onOpenChange);
return (
<Popover open={isOpen} onOpenChange={handleOpenChange} modal>
<PopoverTrigger
<Popover.Root open={isOpen} onOpenChange={handleOpenChange} modal>
<Popover.Trigger
asChild
ref={triggerRef}
onClick={() => {
@@ -77,19 +76,16 @@ export const FloatingPanel = ({
}}
>
{children}
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
sideOffset={sideOffset}
side="left"
hideArrow
align="start"
css={{ width: theme.spacing[30] }}
>
{content}
<PopoverHeader title={title} />
</PopoverContent>
</PopoverPortal>
</Popover>
</Popover.Trigger>
<Popover.Content
sideOffset={sideOffset}
side="left"
align="start"
className={contetnStyles()}
>
{content}
<Popover.Title>{title}</Popover.Title>
</Popover.Content>
</Popover.Root>
);
};
@@ -49,7 +49,7 @@ export const ImageInfo = ({ asset, onDelete }: ImageInfoProps) => {
) : null}
<Box css={{ p: `${theme.spacing[5]} ${theme.spacing[9]}` }}>
<Button
variant="destructive"
color="destructive"
onClick={() => onDelete([id])}
prefix={<TrashIcon />}
>
@@ -81,7 +81,7 @@ const Menu = ({
<PopoverTrigger asChild>
<Button
prefix={<MenuIcon />}
variant="ghost"
color="ghost"
aria-label="Menu Button for options"
></Button>
</PopoverTrigger>
@@ -123,7 +123,7 @@ const Menu = ({
<Item>
{/* @todo need a menu item that looks like one from dropdown but without DropdownMenu */}
<Button
variant="destructive"
color="destructive"
onClick={() => {
onDelete();
}}
@@ -229,7 +229,7 @@ export const ShareProject = ({
const create = (
<Box className={itemStyle({ css: { py: theme.spacing["9"] } })}>
<Button
variant="neutral"
color="neutral"
prefix={<PlusIcon />}
onClick={() => {
onCreate();
@@ -1,6 +1,7 @@
import { type ComponentProps } from "react";
import { MenuIcon, CrossIcon, TrashIcon } from "@webstudio-is/icons";
import { Button as ButtonComponent } from "./button";
import { StorySection, StoryGrid } from "./storybook";
export default {
title: "Library/Button",
@@ -13,18 +14,21 @@ const iconsMap = {
"<TrashIcon>": <TrashIcon />,
} as const;
const Section = ({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) => (
<>
<h3 style={{ fontFamily: "sans-serif" }}>{title}</h3>
<div style={{ display: "flex", gap: 12 }}>{children}</div>
</>
);
const colors: ReadonlyArray<ComponentProps<typeof ButtonComponent>["color"]> = [
"primary",
"neutral",
"destructive",
"positive",
"ghost",
];
const states: ReadonlyArray<ComponentProps<typeof ButtonComponent>["state"]> = [
"auto",
"hover",
"focus",
"pressed",
"pending",
];
export const Button = ({
prefix,
@@ -35,55 +39,51 @@ export const Button = ({
suffix?: keyof typeof iconsMap;
}) => (
<>
<Section title="Configurable">
<StorySection title="Configurable">
<ButtonComponent
prefix={prefix && iconsMap[prefix]}
suffix={suffix && iconsMap[suffix]}
{...rest}
/>
</Section>
</StorySection>
<Section title="Variants">
{variants.map((variant) => (
<ButtonComponent variant={variant} key={variant}>
{variant}
</ButtonComponent>
))}
</Section>
<StorySection title="Colors & States">
<StoryGrid>
{colors.map((color) => (
<StoryGrid horizontal key={color}>
{states.map((state) => (
<ButtonComponent
prefix={<TrashIcon />}
state={state}
color={color}
key={state}
>
{color} {state}
</ButtonComponent>
))}
<ButtonComponent prefix={<TrashIcon />} color={color} disabled>
{color} disabled
</ButtonComponent>
</StoryGrid>
))}
</StoryGrid>
</StorySection>
<Section title="With Icon">
<ButtonComponent prefix={<TrashIcon />}>Button</ButtonComponent>
<ButtonComponent suffix={<TrashIcon />}>Button</ButtonComponent>
<ButtonComponent prefix={<TrashIcon />} />
</Section>
<Section title="Disabled">
<ButtonComponent disabled>Button</ButtonComponent>
<ButtonComponent disabled prefix={<TrashIcon />}>
Button
</ButtonComponent>
<ButtonComponent disabled prefix={<TrashIcon />} />
</Section>
<Section title="Pending">
<ButtonComponent pending>Button</ButtonComponent>
<ButtonComponent pending prefix={<TrashIcon />}>
Button
</ButtonComponent>
<ButtonComponent pending prefix={<TrashIcon />} />
</Section>
<StorySection title="Icon">
<StoryGrid horizontal>
<ButtonComponent prefix={<TrashIcon />}>Button</ButtonComponent>
<ButtonComponent suffix={<TrashIcon />}>Button</ButtonComponent>
<ButtonComponent prefix={<TrashIcon />} />
</StoryGrid>
</StorySection>
</>
);
const variants: ReadonlyArray<
Extract<ComponentProps<typeof ButtonComponent>["variant"], string>
> = ["primary", "neutral", "destructive", "positive", "ghost"];
Button.argTypes = {
children: { defaultValue: "Button", control: "text" },
variant: {
color: {
defaultValue: "primary",
control: { type: "inline-radio", options: variants },
control: { type: "inline-radio", options: colors },
},
prefix: {
defaultValue: "undefined",
@@ -94,5 +94,11 @@ Button.argTypes = {
control: { type: "inline-radio", options: Object.keys(iconsMap) },
},
disabled: { defaultValue: false, control: "boolean" },
pending: { defaultValue: false, control: "boolean" },
state: {
defaultValue: "auto",
control: {
type: "inline-radio",
options: states,
},
},
};
@@ -3,33 +3,77 @@
* https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A2709
*/
import React, { forwardRef, type Ref, type ComponentProps } from "react";
import {
forwardRef,
type Ref,
type ComponentProps,
type ReactNode,
} from "react";
import { typography } from "./typography";
import { styled, theme } from "../stitches.config";
const colors = [
"primary",
"destructive",
"positive",
"neutral",
"ghost",
] as const;
type ButtonColor = typeof colors[number];
type ButtonState = "auto" | "hover" | "focus" | "pressed" | "pending";
const bg: Record<ButtonColor, string> = {
primary: theme.colors.backgroundPrimary,
neutral: theme.colors.backgroundNeutralMain,
destructive: theme.colors.backgroundDestructiveMain,
positive: theme.colors.backgroundSuccessMain,
ghost: theme.colors.backgroundHover,
};
const fg: Record<ButtonColor, string> = {
primary: theme.colors.foregroundContrastMain,
destructive: theme.colors.foregroundContrastMain,
positive: theme.colors.foregroundContrastMain,
neutral: theme.colors.foregroundMain,
ghost: theme.colors.foregroundMain,
};
// CSS supports multiple gradients as backgrounds but not multiple colors
const backgroundColors = ({
overlay,
base,
}: {
overlay: string;
base: string;
}) =>
const backgroundColors = (base: string, overlay: string) =>
`linear-gradient(${overlay}, ${overlay}), linear-gradient(${base}, ${base})`;
const backgroundStyle = (baseColor: string) => ({
background: baseColor,
"&:hover": {
background: backgroundColors({
base: baseColor,
overlay: theme.colors.backgroundButtonHover,
}),
const perColorStyle = (variant: ButtonColor) => ({
background: variant === "ghost" ? "transparent" : bg[variant],
color: fg[variant],
"&[data-button-state=auto]:hover, &[data-button-state=hover]": {
background: backgroundColors(
bg[variant],
theme.colors.backgroundButtonHover
),
},
"&:active": {
background: backgroundColors({
base: baseColor,
overlay: theme.colors.backgroundButtonPressed,
}),
"&[data-button-state=auto]:focus-visible, &[data-button-state=focus]": {
outline: `2px solid ${theme.colors.borderFocus}`,
outlineOffset: "1px",
},
"&[data-button-state=auto]:active, &[data-button-state=pressed]": {
background: backgroundColors(
bg[variant],
theme.colors.backgroundButtonPressed
),
},
"&[data-button-state=disabled]": {
background: theme.colors.backgroundButtonDisabled,
color: theme.colors.foregroundDisabled,
},
"&[data-button-state=pending]": {
cursor: "wait",
},
});
@@ -41,50 +85,23 @@ const StyledButton = styled("button", {
alignItems: "center",
justifyContent: "center",
gap: theme.spacing[2],
color: theme.colors.foregroundContrastMain,
padding: `0 ${theme.spacing[4]}`,
height: theme.spacing[12],
borderRadius: theme.borderRadius[4],
"&:focus-visible": {
outline: `2px solid ${theme.colors.borderFocus}`,
outlineOffset: "1px",
},
whiteSpace: "nowrap",
variants: {
// "variant" is used instead of "type" as in Figma,
// because type is already taken for type=submit etc.
variant: {
primary: { ...backgroundStyle(theme.colors.backgroundPrimary) },
neutral: {
...backgroundStyle(theme.colors.backgroundNeutralMain),
color: theme.colors.foregroundMain,
},
destructive: {
...backgroundStyle(theme.colors.backgroundDestructiveMain),
},
positive: { ...backgroundStyle(theme.colors.backgroundSuccessMain) },
ghost: {
...backgroundStyle(theme.colors.backgroundHover),
background: "transparent",
color: theme.colors.foregroundMain,
},
},
pending: {
true: {
cursor: "wait",
},
false: {
"&[disabled]": {
background: theme.colors.backgroundButtonDisabled,
color: theme.colors.foregroundDisabled,
},
},
color: {
primary: perColorStyle("primary"),
destructive: perColorStyle("destructive"),
positive: perColorStyle("positive"),
neutral: perColorStyle("neutral"),
ghost: perColorStyle("ghost"),
},
},
defaultVariants: {
variant: "primary",
color: "primary",
},
});
@@ -93,19 +110,24 @@ const TextContainer = styled("span", typography.labelTitleCase, {
});
type ButtonProps = {
pending?: boolean;
state?: ButtonState;
color?: ButtonColor;
// prefix/suffix is primarily for Icons
// We don't want all the noise from StyledButton,
// so we're cherry-picking just the props we need
css?: ComponentProps<typeof StyledButton>["css"];
// prefix/suffix are primarily for Icons
// this is a replacement for icon/icon-left/icon-right in Figma
prefix?: React.ReactNode;
suffix?: React.ReactNode;
} & Omit<ComponentProps<typeof StyledButton>, "pending" | "prefix">;
prefix?: ReactNode;
suffix?: ReactNode;
} & Omit<ComponentProps<"button">, "prefix">;
export const Button = forwardRef(
(
{
pending = false,
disabled = false,
disabled,
state = "auto",
prefix,
suffix,
children,
@@ -116,15 +138,15 @@ export const Button = forwardRef(
return (
<StyledButton
{...restProps}
pending={pending}
disabled={disabled || pending}
disabled={disabled || state === "pending"}
data-button-state={disabled ? "disabled" : state}
ref={ref}
>
{prefix}
{children && (
<TextContainer>
{children}
{pending ? "…" : ""}
{state === "pending" ? "…" : ""}
</TextContainer>
)}
{suffix}
@@ -0,0 +1,52 @@
import { Button } from "../button";
import { css, theme } from "../../stitches.config";
import { typography } from "../typography";
import * as Dialog from "./dialog";
export default {
title: "Library/Floating Panel/Dialog",
};
const bodyStyles = css({
padding: theme.spacing[9],
});
const descriptionStyles = css(typography.regular, {
marginTop: 0,
marginBottom: theme.spacing[9],
});
const buttonsStyle = css({
display: "flex",
gap: theme.spacing[5],
justifyContent: "flex-end",
});
const DialogDemo = () => (
<Dialog.Root defaultOpen>
<Dialog.Trigger asChild>
<Button>Open</Button>
</Dialog.Trigger>
<Dialog.Content>
<div className={bodyStyles()}>
<Dialog.Description asChild>
<p className={descriptionStyles()}>This is a description</p>
</Dialog.Description>
<div className={buttonsStyle()}>
<Dialog.Close asChild>
<Button color="ghost">Cancel</Button>
</Dialog.Close>
<Dialog.Close asChild>
<Button color="positive">Save</Button>
</Dialog.Close>
</div>
</div>
{/* Title is at the end intentionally,
* to make the close button last in the tab order
*/}
<Dialog.Title>Title</Dialog.Title>
</Dialog.Content>
</Dialog.Root>
);
export { DialogDemo as Dialog };
@@ -0,0 +1,92 @@
import React, { type ReactNode, type ComponentProps, type Ref } from "react";
import * as Primitive from "@radix-ui/react-dialog";
import { css, theme, keyframes } from "../../stitches.config";
import { Title } from "../title";
import { floatingPanelStyles, CloseButton, TitleSlot } from "./shared";
export const Root = Primitive.Root;
export const Trigger = Primitive.Trigger;
// Wrap a close button with this
// https://www.radix-ui.com/docs/primitives/components/dialog#close
export const Close = Primitive.Close;
// An optional accessible description to be announced when the dialog is opened
// https://www.radix-ui.com/docs/primitives/components/dialog#description
export const Description = Primitive.Description;
export const Content = React.forwardRef(
(
{ children, className, ...props }: ComponentProps<typeof Primitive.Content>,
forwardedRef: Ref<HTMLDivElement>
) => {
return (
<Primitive.Portal>
<Primitive.Overlay className={overlayStyles()} />
<Primitive.Content
className={contentStyles({ className })}
{...props}
ref={forwardedRef}
>
{children}
</Primitive.Content>
</Primitive.Portal>
);
}
);
Content.displayName = "Content";
const ContentTitle = ({
children,
closeLabel = "Close dialog",
}: {
children: ReactNode;
closeLabel?: string;
}) => (
<TitleSlot>
<Title
suffix={
<Primitive.Close asChild>
<CloseButton aria-label={closeLabel} />
</Primitive.Close>
}
>
<Primitive.Title className={titleStyles()}>{children}</Primitive.Title>
</Title>
</TitleSlot>
);
export { ContentTitle as Title };
// Styles specific to dialog
// (as opposed to be common for all floating panels)
const overlayShow = keyframes({
from: { opacity: 0 },
to: { opacity: 1 },
});
const overlayStyles = css({
backgroundColor: "rgba(17, 24, 28, 0.66)",
position: "fixed",
inset: 0,
animation: `${overlayShow} 150ms ${theme.easing.easeOut}`,
});
const contentShow = keyframes({
from: { opacity: 0, transform: "translate(-50%, -48%) scale(0.96)" },
to: { opacity: 1, transform: "translate(-50%, -50%) scale(1)" },
});
const contentStyles = css(floatingPanelStyles, {
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "90vw",
maxWidth: theme.spacing[35],
maxHeight: "85vh",
animation: `${contentShow} 150ms ${theme.easing.easeOut}`,
});
const titleStyles = css({
// Resetting H2 styles (Primitive.Title is H2)
all: "unset",
});
@@ -0,0 +1,7 @@
/**
* Implementations of the "Floating Panel" component from:
* https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A2679&t=6Q0l4j0CBvXkuKYp-0
*/
export * as FloatingPanelDialog from "./dialog";
export * as FloatingPanelPopover from "./popover";
@@ -0,0 +1,50 @@
import { Button } from "../button";
import { css, theme } from "../../stitches.config";
import { typography } from "../typography";
import * as Popover from "./popover";
export default {
title: "Library/Floating Panel/Popover",
};
const bodyStyles = css({
padding: theme.spacing[9],
});
const descriptionStyles = css(typography.regular, {
marginTop: 0,
marginBottom: theme.spacing[9],
});
const buttonsStyle = css({
display: "flex",
gap: theme.spacing[5],
justifyContent: "flex-end",
});
const PopoverDemo = () => (
<Popover.Root defaultOpen>
<Popover.Trigger asChild>
<Button>Open</Button>
</Popover.Trigger>
<Popover.Content>
<div className={bodyStyles()}>
<p className={descriptionStyles()}>This is a description</p>
<div className={buttonsStyle()}>
<Popover.Close asChild>
<Button color="ghost">Cancel</Button>
</Popover.Close>
<Popover.Close asChild>
<Button color="positive">Save</Button>
</Popover.Close>
</div>
</div>
{/* Title is at the end intentionally,
* to make the close button last in the tab order
*/}
<Popover.Title>Title</Popover.Title>
</Popover.Content>
</Popover.Root>
);
export { PopoverDemo as Popover };
@@ -0,0 +1,56 @@
import React, { type ComponentProps, type Ref, type ReactNode } from "react";
import * as Primitive from "@radix-ui/react-popover";
import { css, theme } from "../../stitches.config";
import { Title } from "../title";
import { floatingPanelStyles, CloseButton, TitleSlot } from "./shared";
export const Root = Primitive.Root;
const contentStyles = css(floatingPanelStyles, {
minWidth: theme.spacing[28],
maxWidth: "max-content",
});
export const Content = React.forwardRef(
(
{ children, className, ...props }: ComponentProps<typeof Primitive.Content>,
ref: Ref<HTMLDivElement>
) => (
<Primitive.Portal>
<Primitive.Content
sideOffset={4}
collisionPadding={4}
className={contentStyles({ className })}
{...props}
ref={ref}
>
{children}
</Primitive.Content>
</Primitive.Portal>
)
);
Content.displayName = "Content";
const PopoverTitle = ({
children,
closeLabel = "Close dialog",
}: {
children: ReactNode;
closeLabel?: string;
}) => (
<TitleSlot>
<Title
suffix={
<Primitive.Close asChild>
<CloseButton aria-label={closeLabel} />
</Primitive.Close>
}
>
{children}
</Title>
</TitleSlot>
);
export { PopoverTitle as Title };
export const Trigger = Primitive.Trigger;
export const Close = Primitive.Close;
@@ -0,0 +1,38 @@
import { forwardRef, Ref, type ReactNode, type ComponentProps } from "react";
import { CrossIcon } from "@webstudio-is/icons";
import { css, theme } from "../../stitches.config";
import { Button } from "../button";
import { Separator } from "../separator";
export const floatingPanelStyles = css({
border: `1px solid ${theme.colors.borderMain}`,
boxShadow: theme.shadows.menuDropShadow,
background: theme.colors.backgroundPanel,
borderRadius: theme.borderRadius[4],
display: "flex",
flexDirection: "column",
"&:focus": {
// override browser default
outline: "none",
},
});
const titleSlotStyles = css({
// We put title at the bottom in DOM to make the close button last in the TAB order
// But visually we want it to be first
order: -1,
});
export const TitleSlot = ({ children }: { children: ReactNode }) => (
<div className={titleSlotStyles()}>
{children}
<Separator />
</div>
);
export const CloseButton = forwardRef(
(props: ComponentProps<typeof Button>, ref: Ref<HTMLButtonElement>) => (
<Button color="ghost" prefix={<CrossIcon />} {...props} ref={ref} />
)
);
CloseButton.displayName = "CloseButton";
@@ -1,24 +1,11 @@
import type { ReactNode } from "react";
import type { ComponentStory } from "@storybook/react";
import { StorySection, StoryGrid } from "./storybook";
import { Label } from "./label";
export default {
title: "Library/Label",
};
const Section = ({
title,
children,
}: {
title: string;
children: ReactNode;
}) => (
<>
<h3 style={{ fontFamily: "sans-serif" }}>{title}</h3>
<div style={{ display: "flex", gap: 12 }}>{children}</div>
</>
);
const colors = ["default", "preset", "local", "remote"] as const;
const LabelStory: ComponentStory<typeof Label> = ({
@@ -28,34 +15,38 @@ const LabelStory: ComponentStory<typeof Label> = ({
}) => {
return (
<>
<Section title="Configurable">
<StorySection title="Configurable">
<Label color={color} disabled={disabled}>
{children}
</Label>
</Section>
</StorySection>
<Section title="Colors">
{colors.map((color) => (
<Label key={color} color={color}>
{color}
</Label>
))}
</Section>
<StorySection title="Colors">
<StoryGrid horizontal>
{colors.map((color) => (
<Label key={color} color={color}>
{color}
</Label>
))}
</StoryGrid>
</StorySection>
<Section title="With checkbox">
<StorySection title="With checkbox">
<input id="checkbox1" type="checkbox"></input>
<Label htmlFor="checkbox1">Label text</Label>
</Section>
</StorySection>
<Section title="Disabled">
<Label disabled={true}>Label text</Label>
<div>
<input id="checkbox2" type="checkbox" disabled={true}></input>
<Label htmlFor="checkbox2" disabled={true}>
Label text
</Label>
</div>
</Section>
<StorySection title="Disabled">
<StoryGrid horizontal>
<Label disabled={true}>Label text</Label>
<div>
<input id="checkbox2" type="checkbox" disabled={true}></input>
<Label htmlFor="checkbox2" disabled={true}>
Label text
</Label>
</div>
</StoryGrid>
</StorySection>
</>
);
};
@@ -43,6 +43,7 @@ import {
} from "@webstudio-is/icons";
import { useState } from "react";
import { DeprecatedIconButton } from "./__DEPRECATED__/icon-button";
import { StorySection } from "./storybook";
const DropdownDemo = ({ withIndicator }: { withIndicator: boolean }) => {
const [isApple, setIsApple] = useState(true);
@@ -247,9 +248,9 @@ const ComplexSelectDemo = () => {
);
};
export const Menu = () => (
export const Demo = () => (
<>
<Section title="Dropdown menu">
<StorySection title="Dropdown menu">
<div style={{ display: "flex", paddingBottom: 360 }}>
<div style={{ paddingLeft: 100, paddingRight: 100 }}>
<DropdownDemo withIndicator={true} />
@@ -258,33 +259,23 @@ export const Menu = () => (
<DropdownDemo withIndicator={false} />
</div>
</div>
</Section>
<Section title="Select menu (Combobox component)">
<ComboboxDemo />
</Section>
<Section title="Basic select menu (Select component)">
</StorySection>
<StorySection title="Select menu (Combobox component)">
<div style={{ width: 200 }}>
<ComboboxDemo />
</div>
</StorySection>
<StorySection title="Basic select menu (Select component)">
<BasicSelectDemo />
</Section>
<Section title="Complex select menu (Select component)">
</StorySection>
<StorySection title="Complex select menu (Select component)">
<ComplexSelectDemo />
</Section>
</StorySection>
</>
);
Demo.storyName = "Menu, Menu Item";
export default {
title: "Library/Menu",
component: Menu,
title: "Library/Menu, Menu Item",
};
const Section = ({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) => (
<>
<h3 style={{ fontFamily: "sans-serif" }}>{title}</h3>
<div style={{ display: "flex", gap: 12 }}>{children}</div>
</>
);
@@ -12,6 +12,8 @@
* - Component: "Menu Item Large"
* - Type of "Menu" component: "Dropdown w/large items"
* - Type of "Menu" component: "Context menu"
*
* @todo: group everything under a folder same as floating-panel?
*/
import { css, styled, theme } from "../stitches.config";
@@ -0,0 +1,9 @@
import { Separator } from "./separator";
export default {
title: "Library/Separator",
};
const SeparatorStory = () => <Separator />;
export { SeparatorStory as Separator };
@@ -1,39 +1,17 @@
import { styled } from "../stitches.config";
/**
* Implementation of the "Separator" component from:
* https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A2613
*/
import { styled, theme } from "../stitches.config";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { theme } from "../stitches.config";
export const Separator = styled(SeparatorPrimitive.Root, {
border: "none",
margin: 0,
flexShrink: 0,
backgroundColor: theme.colors.slate7,
backgroundColor: theme.colors.borderMain,
cursor: "default",
variants: {
size: {
1: {
'&[data-orientation="horizontal"]': {
height: theme.spacing[1],
my: theme.spacing[5],
},
'&[data-orientation="vertical"]': {
width: theme.spacing[1],
mx: theme.spacing[5],
},
},
auto: {
'&[data-orientation="horizontal"]': {
height: theme.spacing[1],
},
'&[data-orientation="vertical"]': {
width: theme.spacing[1],
},
},
},
},
defaultVariants: {
size: "auto",
},
'&[data-orientation="horizontal"]': { height: theme.spacing[1] },
'&[data-orientation="vertical"]': { width: theme.spacing[1] },
});
@@ -0,0 +1,54 @@
/**
* Some helpers for stories
*/
import { type ReactNode } from "react";
import { theme, css } from "../stitches.config";
import { typography } from "./typography";
const sectionStyle = css({
marginBottom: theme.spacing[10],
variants: {
withBorder: {
true: {
border: `1px dashed ${theme.colors.borderMain}`,
padding: theme.spacing[5],
marginBottom: theme.spacing[7],
},
},
},
});
const titleStyle = css(typography.title, {
marginTop: 0,
marginBottom: theme.spacing[5],
color: theme.colors.foregroundMain,
});
export const StorySection = ({
title,
withBorder,
children,
}: {
title: string;
withBorder?: boolean;
children: ReactNode;
}) => (
<section className={sectionStyle({ withBorder })}>
<h3 className={titleStyle()}>{title}</h3>
{children}
</section>
);
const gridStyle = css({
display: "flex",
flexWrap: "wrap",
gap: theme.spacing[7],
flexDirection: "column",
variants: { horizontal: { true: { flexDirection: "row" } } },
});
export const StoryGrid = ({
children,
horizontal,
}: {
children: ReactNode;
horizontal?: boolean;
}) => <div className={gridStyle({ horizontal })}>{children}</div>;
@@ -0,0 +1,44 @@
import { Title, TitleSuffixSpacer } from "./title";
import { Button } from "./button";
import { CrossIcon, CopyIcon } from "@webstudio-is/icons";
import { StoryGrid } from "./storybook";
export default {
title: "Library/Title",
};
const TitleStory = () => (
<StoryGrid>
<Title>Without buttons</Title>
<Title suffix={<Button prefix={<CrossIcon />} color="ghost" />}>
One icon button
</Title>
<Title
suffix={
<>
<Button prefix={<CrossIcon />} color="ghost" />
<Button prefix={<CopyIcon />} color="ghost" />
</>
}
>
Many icon buttons
</Title>
<Title
suffix={
<>
<Button prefix={<CrossIcon />} color="ghost" />
<Button prefix={<CopyIcon />} color="ghost" />
<TitleSuffixSpacer />
<Button>Button</Button>
</>
}
>
Icon and regular buttons
</Title>
</StoryGrid>
);
export { TitleStory as Title };
@@ -0,0 +1,50 @@
/**
* Implementation of the "Title" component from:
* https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=2%3A12361
*/
import { forwardRef, type ReactNode, type Ref } from "react";
import { theme, styled, type CSS } from "../stitches.config";
import { typography } from "./typography";
type TitleProps = {
children: ReactNode;
suffix?: ReactNode;
className?: string;
css?: CSS;
};
const Container = styled("div", typography.title, {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
height: theme.spacing[15],
paddingLeft: theme.spacing[9],
paddingRight: theme.spacing[5],
color: theme.colors.foregroundMain,
});
const SuffixSlot = styled("div", {
display: "flex",
alignItems: "center",
marginLeft: theme.spacing[5],
});
// we can't just set a gap on SuffixSlot,
// because we want no gap between icon-buttons
export const TitleSuffixSpacer = styled("div", {
width: theme.spacing[5],
});
export const Title = forwardRef(
(
{ children, suffix, className, css }: TitleProps,
ref: Ref<HTMLDivElement>
) => (
<Container className={className} css={css} ref={ref}>
<div>{children}</div>
{suffix && <SuffixSlot>{suffix}</SuffixSlot>}
</Container>
)
);
Title.displayName = "Title";
@@ -1,17 +1,16 @@
import { typography } from "./typography";
import { StorySection } from "./storybook";
export const Typography = () => (
<div>
<>
{Object.entries(typography).map(([key, value]) => (
<div
key={key}
style={{ border: "dotted 1px", padding: "4px 10px", margin: "10px" }}
>
<p className={typography.regular()}>{key}:</p>
<p className={value()}>The quick brown fox jumps over the lazy dog</p>
</div>
<StorySection withBorder key={key} title={key}>
<div className={value()}>
The quick brown fox jumps over the lazy dog
</div>
</StorySection>
))}
</div>
</>
);
export default {
+36 -22
View File
@@ -1,20 +1,29 @@
export * from "./stitches.config";
export * from "./components/toast";
export * from "./components/storybook";
// Aligned with Figma
export * from "./components/typography";
export * from "./components/title";
export * from "./components/separator";
export * from "./components/button";
export * from "./components/label";
export * from "./components/select"; // menu is aligned, but trigger is not
export * from "./components/combobox";
export * from "./components/dropdown-menu";
export * from "./components/floating-panel";
export * from "./components/icon-button"; // mostly aligned, but needs a demo and to use tokens
// Not aligned
export * from "./components/toast";
export * as Collapsible from "@radix-ui/react-collapsible";
export { AccessibleIcon } from "@radix-ui/react-accessible-icon";
export * as Portal from "@radix-ui/react-portal";
export * from "@radix-ui/react-id";
export { DeprecatedLabel } from "./components/__DEPRECATED__/label";
export { Label } from "./components/label";
export * from "./components/toggle-group";
export * from "./components/progress-radial";
export * from "./components/select";
export * from "./components/text-field";
export * from "./components/text-area";
export { SearchField } from "./components/search-field";
export { Flex } from "./components/flex";
export { Grid } from "./components/grid";
export { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/tabs";
export {
SidebarTabs,
@@ -30,29 +39,34 @@ export {
EnhancedTooltipProvider,
useEnhancedTooltipProps,
} from "./components/enhanced-tooltip";
export { Button } from "./components/button";
export { DeprecatedButton } from "./components/__DEPRECATED__/button";
export { DeprecatedIconButton } from "./components/__DEPRECATED__/icon-button";
export { IconButton } from "./components/icon-button";
export { Box } from "./components/box";
export * from "./components/popover";
export { DeprecatedHeading } from "./components/__DEPRECATED__/heading";
export { Paragraph } from "./components/paragraph";
export { Link } from "./components/link";
export { Panel } from "./components/panel";
export * from "./components/dropdown-menu";
export * from "./components/switch";
export * from "./components/slider";
export * from "./components/radio";
export * from "./components/combobox";
export * from "./components/checkbox";
export * from "./components/avatar";
export * from "./components/primitives/dnd";
export * from "./components/icon-button-with-menu";
export * from "./components/primitives/numeric-gesture-control";
export * from "./components/separator";
export * from "./components/tree";
export * from "./components/__DEPRECATED__/text";
export * from "./components/text";
export * from "./components/list";
export * from "./components/typography";
export * from "./components/popover";
// No need to align
export * from "@radix-ui/react-id";
export * as Portal from "@radix-ui/react-portal";
export { Box } from "./components/box";
export { Flex } from "./components/flex";
export { Grid } from "./components/grid";
export * from "./components/primitives/dnd";
export * from "./components/primitives/numeric-gesture-control";
// Deprecated
export * from "./components/__DEPRECATED__/text";
export { DeprecatedButton } from "./components/__DEPRECATED__/button";
export { DeprecatedIconButton } from "./components/__DEPRECATED__/icon-button";
export { DeprecatedHeading } from "./components/__DEPRECATED__/heading";
export { DeprecatedLabel } from "./components/__DEPRECATED__/label";