mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 04:55:06 +09:00
breadcrumbs #4
This commit is contained in:
1 parent
8c5252173b
commit
de1bf2fcaa
11 files changed
+155
-65
No files matched your search
@@ -0,0 +1,58 @@
|
||||
import { ChevronRightIcon } from "~/shared/icons";
|
||||
import { Button, Flex, Text } from "~/shared/design-system";
|
||||
import { useSelectedInstancePath } from "../instance/use-selected-instance-path";
|
||||
import { Instance } from "@webstudio-is/sdk";
|
||||
import { Publish } from "../canvas-iframe";
|
||||
|
||||
type BreadcrumbProps = {
|
||||
component: Instance["component"];
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const Breadcrumb = ({ component, onClick }: BreadcrumbProps) => {
|
||||
return (
|
||||
<Flex align="center">
|
||||
<Button ghost css={{ color: "$loContrast", px: 0 }} onClick={onClick}>
|
||||
{component}
|
||||
</Button>{" "}
|
||||
<ChevronRightIcon />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyState = () => (
|
||||
<Text variant="loContrast" size="1">
|
||||
No instance selected
|
||||
</Text>
|
||||
);
|
||||
|
||||
type BreadcrumbsProps = {
|
||||
publish: Publish;
|
||||
};
|
||||
export const Breadcrumbs = ({ publish }: BreadcrumbsProps) => {
|
||||
const selectedInstancePath = useSelectedInstancePath();
|
||||
return (
|
||||
<Flex
|
||||
as="footer"
|
||||
align="center"
|
||||
css={{ height: "$5", background: "$hiContrast", padding: "$1" }}
|
||||
>
|
||||
{selectedInstancePath.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
selectedInstancePath.map((instance) => (
|
||||
<Breadcrumb
|
||||
key={instance.id}
|
||||
component={instance.component}
|
||||
onClick={() => {
|
||||
publish<"focusElement", Instance["id"]>({
|
||||
type: "focusElement",
|
||||
payload: instance.id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./Breadcrumbs";
|
||||
@@ -1,10 +1,7 @@
|
||||
import { forwardRef } from "react";
|
||||
import { css } from "~/shared/design-system";
|
||||
import { CSS, css } from "~/shared/design-system";
|
||||
|
||||
const iframeStyle = css({
|
||||
gridArea: "canvas",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "none",
|
||||
variants: {
|
||||
pointerEvents: {
|
||||
@@ -22,12 +19,17 @@ type CanvasIframeProps = {
|
||||
pointerEvents: "all" | "none";
|
||||
src: string;
|
||||
title: string;
|
||||
css: CSS;
|
||||
};
|
||||
|
||||
export const CanvasIframe = forwardRef<HTMLIFrameElement, CanvasIframeProps>(
|
||||
({ pointerEvents = "all", ...rest }, ref) => {
|
||||
({ pointerEvents = "all", css, ...rest }, ref) => {
|
||||
return (
|
||||
<iframe {...rest} ref={ref} className={iframeStyle({ pointerEvents })} />
|
||||
<iframe
|
||||
{...rest}
|
||||
ref={ref}
|
||||
className={iframeStyle({ pointerEvents, css })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
+48
-31
@@ -1,18 +1,23 @@
|
||||
import { useState } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import type { Project } from "@webstudio-is/sdk";
|
||||
import type { Config } from "~/config";
|
||||
import type { SelectedInstanceData } from "~/shared/component";
|
||||
import type { Project } from "@webstudio-is/sdk";
|
||||
import { Box, Grid, type CSS } from "~/shared/design-system";
|
||||
import { Box, Flex, Grid, type CSS } from "~/shared/design-system";
|
||||
import interStyles from "~/shared/font-faces/inter.css";
|
||||
import { SidebarLeft } from "./sidebar-left";
|
||||
import { SidebarRight } from "./inspector";
|
||||
import { CanvasIframe, useSubscribe, usePublish } from "./canvas-iframe";
|
||||
import { useIsPreviewMode, useRootInstance } from "./nano-values";
|
||||
import {
|
||||
useIsPreviewMode,
|
||||
useRootInstance,
|
||||
useSelectedInstanceData,
|
||||
} from "./nano-values";
|
||||
import { Topbar } from "./topbar";
|
||||
import designerStyles from "./designer.css";
|
||||
import interStyles from "~/shared/font-faces/inter.css";
|
||||
import { useSync } from "./sync";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
|
||||
export const links = () => {
|
||||
return [
|
||||
@@ -21,21 +26,19 @@ export const links = () => {
|
||||
];
|
||||
};
|
||||
|
||||
const useSelectedInstanceData = (): SelectedInstanceData | undefined => {
|
||||
const [selectedInstanceData, setSelectedInstanceData] =
|
||||
useState<SelectedInstanceData>();
|
||||
useSubscribe<"selectInstance", SelectedInstanceData>(
|
||||
"selectInstance",
|
||||
setSelectedInstanceData
|
||||
);
|
||||
return selectedInstanceData;
|
||||
};
|
||||
|
||||
const useSubscribeRootInstance = () => {
|
||||
const [, setValue] = useRootInstance();
|
||||
useSubscribe<"loadRootInstance">("loadRootInstance", setValue);
|
||||
};
|
||||
|
||||
const useSubscribeSelectedInstanceData = () => {
|
||||
const [, setValue] = useSelectedInstanceData();
|
||||
useSubscribe<"selectInstance", SelectedInstanceData>(
|
||||
"selectInstance",
|
||||
setValue
|
||||
);
|
||||
};
|
||||
|
||||
type SidePanelProps = {
|
||||
children: JSX.Element;
|
||||
isPreviewMode: boolean;
|
||||
@@ -52,6 +55,7 @@ const SidePanel = ({
|
||||
if (isPreviewMode === true) return null;
|
||||
return (
|
||||
<Box
|
||||
as="aside"
|
||||
css={{
|
||||
gridArea,
|
||||
display: "flex",
|
||||
@@ -69,6 +73,18 @@ const SidePanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Main = ({ children }: { children: Array<JSX.Element> }) => (
|
||||
<Flex
|
||||
as="main"
|
||||
direction="column"
|
||||
css={{
|
||||
gridArea: "main",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
type ChromeWrapperProps = {
|
||||
children: Array<JSX.Element>;
|
||||
isPreviewMode: boolean;
|
||||
@@ -81,7 +97,7 @@ const ChromeWrapper = ({ children, isPreviewMode }: ChromeWrapperProps) => {
|
||||
gridTemplateRows: "auto 1fr",
|
||||
gridTemplateAreas: `
|
||||
"header header"
|
||||
"sidebar canvas"
|
||||
"sidebar main"
|
||||
`,
|
||||
}
|
||||
: {
|
||||
@@ -89,7 +105,7 @@ const ChromeWrapper = ({ children, isPreviewMode }: ChromeWrapperProps) => {
|
||||
gridTemplateRows: "auto 1fr",
|
||||
gridTemplateAreas: `
|
||||
"header header header"
|
||||
"sidebar canvas inspector"
|
||||
"sidebar main inspector"
|
||||
`,
|
||||
};
|
||||
return (
|
||||
@@ -112,12 +128,13 @@ type DesignerProps = {
|
||||
};
|
||||
|
||||
export const Designer = ({ config, project }: DesignerProps) => {
|
||||
const [isDragging, setIsDragging] = useState<boolean>();
|
||||
const selectedInstanceData = useSelectedInstanceData();
|
||||
const [publish, iframeRef] = usePublish();
|
||||
const [isPreviewMode] = useIsPreviewMode();
|
||||
useSync({ config, project });
|
||||
useSubscribeRootInstance();
|
||||
useSubscribeSelectedInstanceData();
|
||||
const [isDragging, setIsDragging] = useState<boolean>();
|
||||
const [publish, iframeRef] = usePublish();
|
||||
const [isPreviewMode] = useIsPreviewMode();
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ChromeWrapper isPreviewMode={isPreviewMode}>
|
||||
@@ -126,7 +143,6 @@ export const Designer = ({ config, project }: DesignerProps) => {
|
||||
iframeRef={iframeRef}
|
||||
onDragChange={setIsDragging}
|
||||
publish={publish}
|
||||
selectedInstanceData={selectedInstanceData}
|
||||
/>
|
||||
</SidePanel>
|
||||
<Topbar
|
||||
@@ -135,21 +151,22 @@ export const Designer = ({ config, project }: DesignerProps) => {
|
||||
project={project}
|
||||
publish={publish}
|
||||
/>
|
||||
<CanvasIframe
|
||||
ref={iframeRef}
|
||||
src={`${config.canvasPath}/${project.id}`}
|
||||
pointerEvents={isDragging ? "none" : "all"}
|
||||
title={project.title}
|
||||
/>
|
||||
<Main>
|
||||
<CanvasIframe
|
||||
ref={iframeRef}
|
||||
src={`${config.canvasPath}/${project.id}`}
|
||||
pointerEvents={isDragging ? "none" : "all"}
|
||||
title={project.title}
|
||||
css={{ height: "100%" }}
|
||||
/>
|
||||
<Breadcrumbs publish={publish} />
|
||||
</Main>
|
||||
<SidePanel
|
||||
gridArea="inspector"
|
||||
isPreviewMode={isPreviewMode}
|
||||
css={{ overflow: "hidden" }}
|
||||
>
|
||||
<SidebarRight
|
||||
publish={publish}
|
||||
selectedInstanceData={selectedInstanceData}
|
||||
/>
|
||||
<SidebarRight publish={publish} />
|
||||
</SidePanel>
|
||||
</ChromeWrapper>
|
||||
</DndProvider>
|
||||
|
||||
@@ -10,12 +10,11 @@ import {
|
||||
import { StylePanel } from "~/designer/style-panel";
|
||||
import { SettingsPanel } from "~/designer/settings-panel";
|
||||
import type { Publish } from "~/designer/canvas-iframe";
|
||||
import type { SelectedInstanceData } from "~/shared/component";
|
||||
import { BrushIcon, GearIcon } from "~/shared/icons";
|
||||
import { useSelectedInstanceData } from "../nano-values";
|
||||
|
||||
type SidebarRightProps = {
|
||||
publish: Publish;
|
||||
selectedInstanceData?: SelectedInstanceData;
|
||||
};
|
||||
|
||||
const contentStyle = {
|
||||
@@ -23,10 +22,9 @@ const contentStyle = {
|
||||
overflow: "auto",
|
||||
};
|
||||
|
||||
export const SidebarRight = ({
|
||||
publish,
|
||||
selectedInstanceData,
|
||||
}: SidebarRightProps) => {
|
||||
export const SidebarRight = ({ publish }: SidebarRightProps) => {
|
||||
const [selectedInstanceData] = useSelectedInstanceData();
|
||||
|
||||
if (selectedInstanceData === undefined) {
|
||||
return (
|
||||
<Box css={{ p: "$2" }}>
|
||||
@@ -36,6 +34,7 @@ export const SidebarRight = ({
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="style" css={{ width: "100%", gap: "$2" }}>
|
||||
<TabsList>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Instance } from "@webstudio-is/sdk";
|
||||
import { useMemo } from "react";
|
||||
import { getInstancePath } from "~/shared/tree-utils";
|
||||
import { useRootInstance, useSelectedInstanceData } from "../nano-values";
|
||||
|
||||
export const useSelectedInstancePath = (): Array<Instance> => {
|
||||
const [rootInstance] = useRootInstance();
|
||||
const [selectedInstanceData] = useSelectedInstanceData();
|
||||
return useMemo(
|
||||
() =>
|
||||
selectedInstanceData !== undefined && rootInstance !== undefined
|
||||
? getInstancePath(rootInstance, selectedInstanceData.id)
|
||||
: [],
|
||||
[selectedInstanceData, rootInstance]
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,16 @@
|
||||
import { createValueContainer, useValue } from "react-nano-state";
|
||||
import { type Instance } from "@webstudio-is/sdk";
|
||||
import { SelectedInstanceData } from "~/shared/component";
|
||||
|
||||
const rootInstanceContainer = createValueContainer<Instance | undefined>();
|
||||
export const useRootInstance = () => useValue(rootInstanceContainer);
|
||||
|
||||
const selectedInstanceDataContainer = createValueContainer<
|
||||
SelectedInstanceData | undefined
|
||||
>();
|
||||
export const useSelectedInstanceData = () =>
|
||||
useValue(selectedInstanceDataContainer);
|
||||
|
||||
const isPreviewModeContainer = createValueContainer<boolean>(false);
|
||||
export const useIsPreviewMode = () => useValue(isPreviewModeContainer);
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
} from "~/shared/design-system";
|
||||
import { TriangleRightIcon, TriangleDownIcon } from "~/shared/icons";
|
||||
import { primitives, type SelectedInstanceData } from "~/shared/component";
|
||||
import { getInstancePath } from "~/shared/tree-utils";
|
||||
import { type Publish } from "~/designer/canvas-iframe";
|
||||
import { useSelectedInstancePath } from "~/designer/instance/use-selected-instance-path";
|
||||
import { useRootInstance } from "~/designer/nano-values";
|
||||
|
||||
const openKeyframes = keyframes({
|
||||
from: { height: 0 },
|
||||
@@ -37,7 +38,7 @@ const CollapsibleContent = styled(Collapsible.Content, {
|
||||
type TreeProps = {
|
||||
instance: Instance;
|
||||
selectedInstanceId?: Instance["id"];
|
||||
selectedInstancePath: Array<Instance["id"]>;
|
||||
selectedInstancePath: Array<Instance>;
|
||||
level?: number;
|
||||
onSelect: (instance: Instance) => void;
|
||||
};
|
||||
@@ -49,9 +50,7 @@ const Tree = ({
|
||||
onSelect,
|
||||
selectedInstancePath,
|
||||
}: TreeProps) => {
|
||||
const [isOpen, setIsOpen] = useState(
|
||||
selectedInstancePath.includes(instance.id)
|
||||
);
|
||||
const [isOpen, setIsOpen] = useState(selectedInstancePath.includes(instance));
|
||||
|
||||
// Text nodes have only one child which is a string.
|
||||
const showChildren =
|
||||
@@ -122,25 +121,16 @@ const Tree = ({
|
||||
};
|
||||
|
||||
type TabContentProps = {
|
||||
rootInstance?: Instance;
|
||||
publish: Publish;
|
||||
selectedInstanceData?: SelectedInstanceData;
|
||||
};
|
||||
|
||||
export const TabContent = ({
|
||||
rootInstance,
|
||||
publish,
|
||||
selectedInstanceData,
|
||||
}: TabContentProps) => {
|
||||
const selectedInstancePath = useMemo(
|
||||
() =>
|
||||
selectedInstanceData !== undefined && rootInstance !== undefined
|
||||
? getInstancePath(rootInstance, selectedInstanceData.id).map(
|
||||
({ id }) => id
|
||||
)
|
||||
: [],
|
||||
[selectedInstanceData, rootInstance]
|
||||
);
|
||||
const [rootInstance] = useRootInstance();
|
||||
const selectedInstancePath = useSelectedInstancePath();
|
||||
|
||||
if (rootInstance === undefined) return null;
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
SidebarTabsTrigger,
|
||||
} from "~/shared/design-system";
|
||||
import { useSubscribe, type Publish } from "~/designer/canvas-iframe";
|
||||
import type { SelectedInstanceData } from "~/shared/component";
|
||||
import { useRootInstance } from "../nano-values";
|
||||
import { useSelectedInstanceData } from "../nano-values";
|
||||
import * as panels from "./panels";
|
||||
import type { TabName } from "./types";
|
||||
|
||||
@@ -27,18 +26,16 @@ type SidebarLeftProps = {
|
||||
onDragChange: (isDragging: boolean) => void;
|
||||
iframeRef: MutableRefObject<HTMLIFrameElement | null>;
|
||||
publish: Publish;
|
||||
selectedInstanceData?: SelectedInstanceData;
|
||||
};
|
||||
|
||||
export const SidebarLeft = ({
|
||||
onDragChange,
|
||||
iframeRef,
|
||||
publish,
|
||||
selectedInstanceData,
|
||||
}: SidebarLeftProps) => {
|
||||
const [selectedInstanceData] = useSelectedInstanceData();
|
||||
const [activeTab, setActiveTab] = useState<TabName>("none");
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [rootInstance] = useRootInstance();
|
||||
const { TabContent } = activeTab === "none" ? none : panels[activeTab];
|
||||
|
||||
useSubscribe<"clickCanvas">("clickCanvas", () => {
|
||||
@@ -84,7 +81,6 @@ export const SidebarLeft = ({
|
||||
}}
|
||||
>
|
||||
<TabContent
|
||||
rootInstance={rootInstance}
|
||||
selectedInstanceData={selectedInstanceData}
|
||||
publish={publish}
|
||||
iframeRef={iframeRef}
|
||||
|
||||
@@ -18,6 +18,7 @@ type TopbarProps = {
|
||||
export const Topbar = ({ config, css, project, publish }: TopbarProps) => {
|
||||
return (
|
||||
<Flex
|
||||
as="header"
|
||||
align="center"
|
||||
justify="between"
|
||||
css={{
|
||||
|
||||
@@ -98,6 +98,9 @@ export const Text = styled("div", {
|
||||
contrast: {
|
||||
color: "$hiContrast",
|
||||
},
|
||||
loContrast: {
|
||||
color: "$loContrast",
|
||||
},
|
||||
},
|
||||
gradient: {
|
||||
true: {
|
||||
|
||||
Reference in new issue
Block a user