diff --git a/.lostpixel/baseline/components-combobox--simple.png b/.lostpixel/baseline/components-combobox--simple.png
index de57d90089..c5bdaf570a 100644
Binary files a/.lostpixel/baseline/components-combobox--simple.png and b/.lostpixel/baseline/components-combobox--simple.png differ
diff --git a/.lostpixel/baseline/components-select--boundaries.png b/.lostpixel/baseline/components-select--boundaries.png
index 569027a090..7deb2aa008 100644
Binary files a/.lostpixel/baseline/components-select--boundaries.png and b/.lostpixel/baseline/components-select--boundaries.png differ
diff --git a/.lostpixel/baseline/components-select--full-width.png b/.lostpixel/baseline/components-select--full-width.png
index 7ae429abb5..d5df6aab7b 100644
Binary files a/.lostpixel/baseline/components-select--full-width.png and b/.lostpixel/baseline/components-select--full-width.png differ
diff --git a/.lostpixel/baseline/components-select--ghost.png b/.lostpixel/baseline/components-select--ghost.png
index ee09a4f0ee..855a2db426 100644
Binary files a/.lostpixel/baseline/components-select--ghost.png and b/.lostpixel/baseline/components-select--ghost.png differ
diff --git a/.lostpixel/baseline/components-select--simple.png b/.lostpixel/baseline/components-select--simple.png
index f0004e1244..a24abb7ed5 100644
Binary files a/.lostpixel/baseline/components-select--simple.png and b/.lostpixel/baseline/components-select--simple.png differ
diff --git a/.lostpixel/baseline/components-select--with-complex-items.png b/.lostpixel/baseline/components-select--with-complex-items.png
index d76876ff8a..5bf1f06f98 100644
Binary files a/.lostpixel/baseline/components-select--with-complex-items.png and b/.lostpixel/baseline/components-select--with-complex-items.png differ
diff --git a/.lostpixel/baseline/components-text-field--click-capture.png b/.lostpixel/baseline/components-text-field--click-capture.png
index fdabc3984a..470a6171cc 100644
Binary files a/.lostpixel/baseline/components-text-field--click-capture.png and b/.lostpixel/baseline/components-text-field--click-capture.png differ
diff --git a/.lostpixel/baseline/components-text-field--layout.png b/.lostpixel/baseline/components-text-field--layout.png
index e0b8da56d0..3edfd45c13 100644
Binary files a/.lostpixel/baseline/components-text-field--layout.png and b/.lostpixel/baseline/components-text-field--layout.png differ
diff --git a/.lostpixel/baseline/components-text-field--prefix-suffix.png b/.lostpixel/baseline/components-text-field--prefix-suffix.png
index b113514ec3..7a46df00c1 100644
Binary files a/.lostpixel/baseline/components-text-field--prefix-suffix.png and b/.lostpixel/baseline/components-text-field--prefix-suffix.png differ
diff --git a/apps/designer/app/designer/features/sidebar-left/lib/header.tsx b/apps/designer/app/designer/features/sidebar-left/lib/header.tsx
index 0b47b76754..b8817acb35 100644
--- a/apps/designer/app/designer/features/sidebar-left/lib/header.tsx
+++ b/apps/designer/app/designer/features/sidebar-left/lib/header.tsx
@@ -4,31 +4,27 @@ import { Cross1Icon } from "@webstudio-is/icons";
type HeaderProps = {
title: string;
- isClosable?: boolean;
- onClose?: () => void;
+ suffix: React.ReactNode;
};
-export const Header = ({ title, isClosable = true, onClose }: HeaderProps) => {
+export const Header = ({ title, suffix }: HeaderProps) => {
return (
<>
{title}
- {isClosable && (
- onClose?.()}
- size="1"
- css={{ marginRight: "$2" }}
- aria-label="Close"
- >
-
-
- )}
+ {suffix && {suffix}}
>
);
};
+
+export const CloseButton = ({ onClick }: { onClick: () => void }) => (
+
+
+
+);
diff --git a/apps/designer/app/designer/features/sidebar-left/lib/navigator/navigator.tsx b/apps/designer/app/designer/features/sidebar-left/lib/navigator/navigator.tsx
index b5fd3977c7..9e7b0a2d61 100644
--- a/apps/designer/app/designer/features/sidebar-left/lib/navigator/navigator.tsx
+++ b/apps/designer/app/designer/features/sidebar-left/lib/navigator/navigator.tsx
@@ -4,7 +4,7 @@ import { type Publish } from "~/shared/pubsub";
import { useCallback } from "react";
import { useSelectedInstanceData } from "~/designer/shared/nano-states";
import { useRootInstance } from "~/shared/nano-states";
-import { Header } from "../header";
+import { Header, CloseButton } from "../header";
import { InstanceTree } from "~/designer/shared/tree";
declare module "~/shared/pubsub" {
@@ -81,7 +81,10 @@ export const Navigator = ({ publish, isClosable, onClose }: NavigatorProps) => {
if (rootInstance === undefined) return null;
return (
-
+ onClose?.()} />}
+ />
{
- onSetActiveTab("none");
- }}
+ suffix={ onSetActiveTab("none")} />}
/>
>
diff --git a/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx b/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx
index e273be4873..80773e8224 100644
--- a/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx
+++ b/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx
@@ -9,7 +9,7 @@ import { createInstance } from "~/shared/tree-utils";
import type { TabName } from "../../types";
import { ComponentThumb } from "./component-thumb";
import { useCanvasRect, useZoom } from "~/designer/shared/nano-states";
-import { Header } from "../../lib/header";
+import { Header, CloseButton } from "../../lib/header";
const componentNames = (
Object.keys(components) as Array
@@ -132,17 +132,15 @@ export const TabContent = ({ publish, onSetActiveTab }: TabContentProps) => {
});
return (
- <>
+
{
- onSetActiveTab("none");
- }}
+ suffix={ onSetActiveTab("none")} />}
/>
{componentNames.map((component: Instance["component"]) => (
@@ -160,7 +158,7 @@ export const TabContent = ({ publish, onSetActiveTab }: TabContentProps) => {
))}
{dragComponent && }
- >
+
);
};
diff --git a/apps/designer/app/designer/features/sidebar-left/panels/pages/pages.tsx b/apps/designer/app/designer/features/sidebar-left/panels/pages/pages.tsx
index acd8d6aa08..917a24f8aa 100644
--- a/apps/designer/app/designer/features/sidebar-left/panels/pages/pages.tsx
+++ b/apps/designer/app/designer/features/sidebar-left/panels/pages/pages.tsx
@@ -1,10 +1,14 @@
-import { TreeNodeLabel, TreeNode } from "@webstudio-is/design-system";
+import {
+ TreeNodeLabel,
+ TreeNode,
+ IconButton,
+} from "@webstudio-is/design-system";
import { type Publish } from "~/shared/pubsub";
-import { PageIcon } from "@webstudio-is/icons";
+import { NewPageIcon, PageIcon } from "@webstudio-is/icons";
import type { TabName } from "../../types";
-import { Header } from "../../lib/header";
+import { CloseButton, Header } from "../../lib/header";
import { type Page, type Pages } from "@webstudio-is/project";
-import { useMemo } from "react";
+import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { type Config } from "~/config";
import {
@@ -12,6 +16,8 @@ import {
usePages,
useProject,
} from "~/designer/shared/nano-states";
+import { SettingsPanel } from "./settings-panel";
+import { NewPageSettings } from "./settings";
type TabContentProps = {
onSetActiveTab: (tabName: TabName) => void;
@@ -60,20 +66,24 @@ const staticTreeProps = {
}
return (
-
+ <>
+
+ >
);
},
};
const PagesPanel = ({
onClose,
+ onNewPage,
onSelect,
selectedPageId,
}: {
- onClose: () => void;
+ onClose?: () => void;
+ onNewPage?: () => void;
onSelect: (pageId: string) => void;
selectedPageId: string;
}) => {
@@ -86,7 +96,23 @@ const PagesPanel = ({
return (
<>
-
+
+ {onNewPage && (
+ onNewPage()}
+ aria-label="New Page"
+ >
+
+
+ )}
+ {onClose && }
+ >
+ }
+ />
{
navigate(`${props.config.designerPath}/${project.id}?pageId=${pageId}`);
};
- if (currentPageId === undefined) {
+ const [newPageOpen, setNewPageOpen] = useState(false);
+
+ if (currentPageId === undefined || project === undefined) {
return null;
}
return (
- props.onSetActiveTab("none")}
- onSelect={handleSelect}
- selectedPageId={currentPageId}
- />
+ <>
+ props.onSetActiveTab("none")}
+ onNewPage={() => setNewPageOpen((current) => !current)}
+ onSelect={handleSelect}
+ selectedPageId={currentPageId}
+ />
+
+ setNewPageOpen(false)}
+ onSuccess={(page) => {
+ setNewPageOpen(false);
+ handleSelect(page.id);
+ }}
+ />
+
+ >
);
};
diff --git a/apps/designer/app/designer/features/sidebar-left/panels/pages/settings-panel.tsx b/apps/designer/app/designer/features/sidebar-left/panels/pages/settings-panel.tsx
new file mode 100644
index 0000000000..44b7f6aa1f
--- /dev/null
+++ b/apps/designer/app/designer/features/sidebar-left/panels/pages/settings-panel.tsx
@@ -0,0 +1,61 @@
+import {
+ styled,
+ keyframes,
+ Collapsible,
+ Box,
+} from "@webstudio-is/design-system";
+
+const CollapsibleRoot = styled(Collapsible.Root, {
+ position: "absolute",
+ left: "100%",
+ top: 0,
+ bottom: 0,
+ display: "flex",
+ flexDirection: "column",
+ marginLeft: 1,
+});
+
+const openKeyframes = keyframes({
+ from: { width: 0 },
+ to: { width: "var(--radix-collapsible-content-width)" },
+});
+
+const closeKeyframes = keyframes({
+ from: { width: "var(--radix-collapsible-content-width)" },
+ to: { width: 0 },
+});
+
+const CollapsibleContent = styled(Collapsible.Content, {
+ overflow: "hidden",
+ flexGrow: "1",
+ display: "flex",
+ flexDirection: "column",
+ '&[data-state="open"]': { animation: `${openKeyframes} 200ms ease-out` },
+ '&[data-state="closed"]': { animation: `${closeKeyframes} 200ms ease-out` },
+});
+
+export const SettingsPanel = ({
+ children,
+ isOpen,
+}: {
+ children: React.ReactNode;
+ isOpen: boolean;
+}) => {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+};
diff --git a/apps/designer/app/designer/features/sidebar-left/panels/pages/settings.tsx b/apps/designer/app/designer/features/sidebar-left/panels/pages/settings.tsx
new file mode 100644
index 0000000000..2cd0e1eb37
--- /dev/null
+++ b/apps/designer/app/designer/features/sidebar-left/panels/pages/settings.tsx
@@ -0,0 +1,129 @@
+import {
+ IconButton,
+ Button,
+ Box,
+ Label,
+ TextField,
+ useId,
+ styled,
+ Flex,
+ toast,
+ InputErrorsTooltip,
+} from "@webstudio-is/design-system";
+import { useFetcher } from "@remix-run/react";
+import { ChevronDoubleLeftIcon } from "@webstudio-is/icons";
+import type { ZodError } from "zod";
+import { Header } from "../../lib/header";
+import { useEffect, useRef, useState } from "react";
+import { type Page } from "@webstudio-is/project";
+
+const Group = styled(Flex, {
+ marginBottom: "$3",
+ gap: "$2",
+ defaultVariants: { direction: "column" },
+});
+
+export type CreatePageData =
+ | { ok: true; page: Page }
+ | { errors: string | ZodError["formErrors"] };
+
+export const NewPageSettings = ({
+ onClose,
+ onSuccess,
+ projectId,
+}: {
+ onClose?: () => void;
+ onSuccess?: (page: Page) => void;
+ projectId: string;
+}) => {
+ const nameFieldId = useId();
+ const pathFieldId = useId();
+
+ const fetcher = useFetcher();
+
+ const [fieldErrors, setFieldErrors] = useState<
+ ZodError["formErrors"]["fieldErrors"]
+ >({});
+
+ const isSubmitting = fetcher.state !== "idle";
+
+ const prevFetcher = useRef(fetcher);
+ useEffect(() => {
+ if (prevFetcher.current.state !== fetcher.state) {
+ if (fetcher.state === "idle" && fetcher.data !== undefined) {
+ if ("ok" in fetcher.data) {
+ onSuccess?.(fetcher.data.page);
+ } else {
+ const errors =
+ typeof fetcher.data.errors === "string"
+ ? { formErrors: [fetcher.data.errors], fieldErrors: {} }
+ : fetcher.data.errors;
+
+ if (errors.formErrors.length > 0) {
+ for (const message of errors.formErrors) {
+ toast.error(message);
+ }
+ }
+ setFieldErrors(errors.fieldErrors);
+ }
+ }
+ }
+ prevFetcher.current = fetcher;
+ }, [fetcher, onSuccess]);
+
+ return (
+ <>
+
+
+
+ )
+ }
+ />
+
+
+
+
+
+ {
+ setFieldErrors(({ name, ...rest }) => rest);
+ }}
+ />
+
+
+
+
+
+ {
+ setFieldErrors(({ path, ...rest }) => rest);
+ }}
+ />
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/apps/designer/app/designer/features/sidebar-left/sidebar-left.tsx b/apps/designer/app/designer/features/sidebar-left/sidebar-left.tsx
index f82b138aaf..7be0e00aac 100644
--- a/apps/designer/app/designer/features/sidebar-left/sidebar-left.tsx
+++ b/apps/designer/app/designer/features/sidebar-left/sidebar-left.tsx
@@ -77,7 +77,6 @@ export const SidebarLeft = ({ publish, config }: SidebarLeftProps) => {
dragAndDropState.isDragging && dragAndDropState.origin === "panel"
? "hidden"
: "visible",
- overflow: "auto",
}}
>
value.trim())
+ .refine((value) => value !== "", "Can't be empty");
+
+const CreatePageInput = zfd.formData({
+ name: nonEmptyString,
+ path: nonEmptyString
+ .refine((path) => path !== "/", "Can't be just a /")
+ .transform((path) => (path.endsWith("/") ? path.slice(0, -1) : path))
+ .transform((path) => path.replace(/\/\//g, "/"))
+ .refine(
+ (path) => path === "" || path.startsWith("/"),
+ "Must start with a /"
+ )
+ .refine(
+ (path) => /^[-_a-z0-9\\/]*$/.test(path),
+ "Only a-z, 0-9, -, _ and / are allowed"
+ ),
+});
+
+const handlePut = async (
+ projectId: string,
+ request: Request
+): Promise => {
+ const result = CreatePageInput.safeParse(await request.formData());
+ if (result.success === false) {
+ return { errors: result.error.formErrors };
+ }
+ const data = result.data;
+
+ const devBuild = await db.build.loadByProjectId(projectId, "dev");
+
+ const existingPage = utils.pages.findByPath(devBuild.pages, data.path);
+ if (existingPage !== undefined) {
+ return {
+ errors: {
+ formErrors: [],
+ fieldErrors: {
+ path: [`Already used for "${existingPage.name}"`],
+ },
+ },
+ };
+ }
+
+ const updatedBuild = await db.build.addPage(devBuild.id, data);
+
+ const newPage = utils.pages.findByPath(updatedBuild.pages, data.path);
+
+ if (newPage === undefined) {
+ throw new Error("New page not found");
+ }
+
+ return { ok: true, page: newPage };
+};
+
+export const action: ActionFunction = async ({ request, params }) => {
+ try {
+ if (params.projectId === undefined) {
+ throw new Error(`Project ID required`);
+ }
+
+ if (request.method === "PUT") {
+ const result = await handlePut(params.projectId, request);
+ return result;
+ }
+
+ throw new Error(`Method ${request.method} is not supported`);
+ } catch (error) {
+ sentryException({ error });
+ return {
+ errors: error instanceof Error ? error.message : String(error),
+ };
+ }
+};
export type ErrorData = {
errors: string;
@@ -24,11 +107,9 @@ export const loader: LoaderFunction = async ({
return prodBuild.pages;
} catch (error) {
- if (error instanceof Error) {
- return {
- errors: error.message,
- };
- }
+ sentryException({ error });
+ return {
+ errors: error instanceof Error ? error.message : String(error),
+ };
}
- return { errors: "Unexpected error" };
};
diff --git a/apps/designer/docs/test-cases.md b/apps/designer/docs/test-cases.md
index 477b264dd3..cb5960f96b 100644
--- a/apps/designer/docs/test-cases.md
+++ b/apps/designer/docs/test-cases.md
@@ -149,3 +149,13 @@
- Check if gap inputs work.
- Check if tooltips are present on all icons(including gap input icons).
- Check that tooltips have a first time interaction delay.
+
+1. Creating a new page
+
+ - Open the pages panel
+ - Click on the "New Page" button in the panel header
+ - Enter a page name
+ - Enter a page path
+ - Click on the "Create" button
+ - Check than the page is created and selected as the current page
+ - Repeat the proccess and make sure it doens't allow you to create a page with an empty name or a path that's used for another page
diff --git a/packages/design-system/src/components/select.stories.tsx b/packages/design-system/src/components/select.stories.tsx
index 598ddbe691..b72262586f 100644
--- a/packages/design-system/src/components/select.stories.tsx
+++ b/packages/design-system/src/components/select.stories.tsx
@@ -64,7 +64,7 @@ export const WithComplexItems: ComponentStory = () => {
export const Boundaries: ComponentStory = () => {
const items = Array(100)
.fill(0)
- .map((_, i) => `Item ${i}`);
+ .map((_, index) => `Item ${index}`);
const [value, setValue] = React.useState(items[0]);
return (
diff --git a/packages/design-system/src/components/tooltip.tsx b/packages/design-system/src/components/tooltip.tsx
index 211036ad3e..c7308df7df 100644
--- a/packages/design-system/src/components/tooltip.tsx
+++ b/packages/design-system/src/components/tooltip.tsx
@@ -1,12 +1,12 @@
-import React, { Ref } from "react";
+import React, { Ref, ComponentProps, Fragment } from "react";
import { styled } from "../stitches.config";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Box } from "./box";
import { Paragraph } from "./paragraph";
import type { CSS } from "../stitches.config";
-type TooltipProps = React.ComponentProps &
- React.ComponentProps & {
+type TooltipProps = ComponentProps &
+ ComponentProps & {
children: React.ReactElement;
content: React.ReactNode;
multiline?: boolean;
@@ -82,3 +82,31 @@ export const Tooltip = React.forwardRef(function TooltipWrapper(
);
});
+
+export const InputErrorsTooltip = ({
+ errors,
+ children,
+}: {
+ errors?: string[];
+ children: ComponentProps["children"];
+}) => {
+ const content = errors?.map((error, index) => (
+
+ {index > 0 &&
}
+ {error}
+
+ ));
+ return (
+ // We intentionally always pass non empty content to avoid optimization inside Tooltip
+ // where it renders {children} directly if content is empty.
+ // If this optimization accur, the input will remount which will cause focus loss
+ // and current value loss.
+
+ {children}
+
+ );
+};
diff --git a/packages/design-system/src/components/tree/tree-node.tsx b/packages/design-system/src/components/tree/tree-node.tsx
index 5dcaba5de2..1531ffe7de 100644
--- a/packages/design-system/src/components/tree/tree-node.tsx
+++ b/packages/design-system/src/components/tree/tree-node.tsx
@@ -53,8 +53,8 @@ const NestingLines = ({
}) =>
level > 1 ? (
<>
- {Array.from({ length: level - 1 }, (_, i) => (
-
+ {Array.from({ length: level - 1 }, (_, index) => (
+
))}
>
) : null;
diff --git a/packages/design-system/src/components/tree/tree.stories.tsx b/packages/design-system/src/components/tree/tree.stories.tsx
index 88faf15243..65fd9f99f5 100644
--- a/packages/design-system/src/components/tree/tree.stories.tsx
+++ b/packages/design-system/src/components/tree/tree.stories.tsx
@@ -20,31 +20,39 @@ export const StressTest = ({ animate }: { animate: boolean }) => {
canAcceptChildren: true,
children: Array.from(
{ length: 100 },
- (_, i): Item => ({
- id: `box-${i}`,
+ (_, index): Item => ({
+ id: `box-${index}`,
canAcceptChildren: true,
children: [
- { id: `heading-${i}.0`, canAcceptChildren: false, children: [] },
- { id: `paragraph-${i}.0`, canAcceptChildren: false, children: [] },
{
- id: `box-${i}.0`,
+ id: `heading-${index}.0`,
+ canAcceptChildren: false,
+ children: [],
+ },
+ {
+ id: `paragraph-${index}.0`,
+ canAcceptChildren: false,
+ children: [],
+ },
+ {
+ id: `box-${index}.0`,
canAcceptChildren: true,
children: [
{
- id: `box-${i}.0.0`,
+ id: `box-${index}.0.0`,
canAcceptChildren: true,
children: [
{
- id: `box-${i}.0.0.0`,
+ id: `box-${index}.0.0.0`,
canAcceptChildren: true,
children: [
{
- id: `heading-${i}.1`,
+ id: `heading-${index}.1`,
canAcceptChildren: false,
children: [],
},
{
- id: `paragraph-${i}.1`,
+ id: `paragraph-${index}.1`,
canAcceptChildren: false,
children: [],
},
@@ -52,7 +60,11 @@ export const StressTest = ({ animate }: { animate: boolean }) => {
},
],
},
- { id: `box-${i}.0.1`, canAcceptChildren: true, children: [] },
+ {
+ id: `box-${index}.0.1`,
+ canAcceptChildren: true,
+ children: [],
+ },
],
},
],
diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts
index 54bb4137dc..4d6604439f 100644
--- a/packages/design-system/src/index.ts
+++ b/packages/design-system/src/index.ts
@@ -24,7 +24,7 @@ export {
export { Card } from "./components/card";
export { SimpleToggle } from "./components/simple-toggle";
export { ScrollArea } from "./components/scrollbar";
-export { Tooltip } from "./components/tooltip";
+export { Tooltip, InputErrorsTooltip } from "./components/tooltip";
export { Button } from "./components/button";
export { IconButton } from "./components/icon-button";
export { Box } from "./components/box";
diff --git a/packages/icons/src/chevron-double-down.tsx b/packages/icons/src/chevron-double-down.tsx
new file mode 100644
index 0000000000..aa5bedc00a
--- /dev/null
+++ b/packages/icons/src/chevron-double-down.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const ChevronDoubleDownIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+ChevronDoubleDownIcon.displayName = "ChevronDoubleDownIcon";
diff --git a/packages/icons/src/chevron-double-left.tsx b/packages/icons/src/chevron-double-left.tsx
new file mode 100644
index 0000000000..d9b537eeab
--- /dev/null
+++ b/packages/icons/src/chevron-double-left.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const ChevronDoubleLeftIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+ChevronDoubleLeftIcon.displayName = "ChevronDoubleLeftIcon";
diff --git a/packages/icons/src/chevron-double-right.tsx b/packages/icons/src/chevron-double-right.tsx
new file mode 100644
index 0000000000..6d9a633a07
--- /dev/null
+++ b/packages/icons/src/chevron-double-right.tsx
@@ -0,0 +1,29 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const ChevronDoubleRightIcon = React.forwardRef<
+ SVGSVGElement,
+ IconProps
+>(({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+});
+
+ChevronDoubleRightIcon.displayName = "ChevronDoubleRightIcon";
diff --git a/packages/icons/src/chevron-double-up.tsx b/packages/icons/src/chevron-double-up.tsx
new file mode 100644
index 0000000000..2ad395d0fc
--- /dev/null
+++ b/packages/icons/src/chevron-double-up.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const ChevronDoubleUpIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+ChevronDoubleUpIcon.displayName = "ChevronDoubleUpIcon";
diff --git a/packages/icons/src/chevron-down.tsx b/packages/icons/src/chevron-down.tsx
new file mode 100644
index 0000000000..692bbc5449
--- /dev/null
+++ b/packages/icons/src/chevron-down.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const ChevronDownIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+ChevronDownIcon.displayName = "ChevronDownIcon";
diff --git a/packages/icons/src/chevron-left.tsx b/packages/icons/src/chevron-left.tsx
new file mode 100644
index 0000000000..614a73bd2b
--- /dev/null
+++ b/packages/icons/src/chevron-left.tsx
@@ -0,0 +1,27 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const ChevronLeftIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+ChevronLeftIcon.displayName = "ChevronLeftIcon";
diff --git a/packages/icons/src/chevron-right.tsx b/packages/icons/src/chevron-right.tsx
new file mode 100644
index 0000000000..98c8b9ab42
--- /dev/null
+++ b/packages/icons/src/chevron-right.tsx
@@ -0,0 +1,27 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const ChevronRightIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+ChevronRightIcon.displayName = "ChevronRightIcon";
diff --git a/packages/icons/src/chevron-up.tsx b/packages/icons/src/chevron-up.tsx
new file mode 100644
index 0000000000..df3499bc95
--- /dev/null
+++ b/packages/icons/src/chevron-up.tsx
@@ -0,0 +1,27 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const ChevronUpIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+ChevronUpIcon.displayName = "ChevronUpIcon";
diff --git a/packages/icons/src/copy.tsx b/packages/icons/src/copy.tsx
new file mode 100644
index 0000000000..e8ac9a0012
--- /dev/null
+++ b/packages/icons/src/copy.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const CopyIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+CopyIcon.displayName = "CopyIcon";
diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts
index 1897831629..e806ce7d5d 100644
--- a/packages/icons/src/index.ts
+++ b/packages/icons/src/index.ts
@@ -49,3 +49,14 @@ export { SizeIcon } from "./size";
export { UploadIcon } from "./upload";
export { GearIcon, gearIconCssVars } from "./gear";
export * from "./page";
+export { CopyIcon } from "./copy";
+export * from "./new-page";
+export * from "./new-folder";
+export * from "./chevron-double-up";
+export * from "./chevron-double-down";
+export * from "./chevron-double-left";
+export * from "./chevron-double-right";
+export { ChevronUpIcon } from "./chevron-up";
+export { ChevronDownIcon } from "./chevron-down";
+export { ChevronLeftIcon } from "./chevron-left";
+export { ChevronRightIcon } from "./chevron-right";
diff --git a/packages/icons/src/new-folder.tsx b/packages/icons/src/new-folder.tsx
new file mode 100644
index 0000000000..3a6a16fa2a
--- /dev/null
+++ b/packages/icons/src/new-folder.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const NewFolderIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+NewFolderIcon.displayName = "NewFolderIcon";
diff --git a/packages/icons/src/new-page.tsx b/packages/icons/src/new-page.tsx
new file mode 100644
index 0000000000..3b89adab98
--- /dev/null
+++ b/packages/icons/src/new-page.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+import { IconProps } from "./types";
+
+export const NewPageIcon = React.forwardRef(
+ ({ color = "currentColor", ...props }, forwardedRef) => {
+ return (
+
+ );
+ }
+);
+
+NewPageIcon.displayName = "NewPageIcon";
diff --git a/packages/icons/src/trash.tsx b/packages/icons/src/trash.tsx
index fbd189a0d5..c7bcfe075c 100644
--- a/packages/icons/src/trash.tsx
+++ b/packages/icons/src/trash.tsx
@@ -5,8 +5,8 @@ export const TrashIcon = React.forwardRef(
({ color = "currentColor", ...props }, forwardedRef) => {
return (