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
No files matched your search
|
Before Width: | Height: | Size: 508 KiB After Width: | Height: | Size: 505 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -26,7 +26,7 @@ export const LoginButton = ({
|
||||
{...props}
|
||||
type="submit"
|
||||
disabled={disabled}
|
||||
variant="neutral"
|
||||
color="neutral"
|
||||
css={{ width: "100%" }}
|
||||
>
|
||||
<Flex gap="2" align="center">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||