Skip to content

Commit

Permalink
feat: introduce new improved outline UI
Browse files Browse the repository at this point in the history
Supports the DropZone API, higlights which item is selected and syncs hover between the DropZone area and the item.
  • Loading branch information
chrisvxd committed Sep 15, 2023
1 parent 3cf9081 commit d0f5776
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 0 deletions.
144 changes: 144 additions & 0 deletions packages/core/components/LayerTree/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import styles from "./styles.module.css";
import getClassNameFactory from "../../lib/get-class-name-factory";
import { Data } from "../../types/Config";
import { ItemSelector } from "../../lib/get-item";
import { scrollIntoView } from "../../lib/scroll-into-view";
import { ChevronDown, Grid, Layers, Type } from "react-feather";
import { rootDroppableId } from "../../lib/root-droppable-id";
import { useContext } from "react";
import { dropZoneContext } from "../DropZone/context";
import { findDropzonesForArea } from "../../lib/find-dropzones-for-area";

const getClassName = getClassNameFactory("LayerTree", styles);
const getClassNameLayer = getClassNameFactory("Layer", styles);

export const LayerTree = ({
data,
dropzoneContent,
itemSelector,
setItemSelector,
dropzone,
label,
}: {
data: Data;
dropzoneContent: Data["content"];
itemSelector: ItemSelector | null;
setItemSelector: (item: ItemSelector | null) => void;
dropzone?: string;
label?: string;
}) => {
const dropzones = data.dropzones || {};

const ctx = useContext(dropZoneContext);

return (
<>
{label && (
<div className={getClassName("dropzoneTitle")}>
<div className={getClassName("dropzoneIcon")}>
<Layers size="16" />
</div>{" "}
{label}
</div>
)}
<ul className={getClassName()}>
{dropzoneContent.length === 0 && (
<div className={getClassName("helper")}>No items</div>
)}
{dropzoneContent.map((item, i) => {
const isSelected =
itemSelector?.index === i &&
(itemSelector.dropzone === dropzone ||
(itemSelector.dropzone === rootDroppableId && !dropzone));

const dropzonesForItem = findDropzonesForArea(data, item.props.id);
const containsDropzone = Object.keys(dropzonesForItem).length > 0;

const isHovering = ctx?.hoveringComponent === item.props.id;

const {
setHoveringArea = () => {},
setHoveringComponent = () => {},
} = ctx || {};

return (
<li
className={getClassNameLayer({
isSelected,
isHovering,
containsDropzone,
})}
key={`${item.props.id}_${i}`}
>
<div className={getClassNameLayer("inner")}>
<div
className={getClassNameLayer("clickable")}
onClick={() => {
if (isSelected) {
setItemSelector(null);
return;
}

setItemSelector({
index: i,
dropzone,
});

const id = dropzoneContent[i].props.id;

scrollIntoView(
document.querySelector(
`[data-rbd-drag-handle-draggable-id="draggable-${id}"]`
) as HTMLElement
);
}}
onMouseOver={(e) => {
e.stopPropagation();
setHoveringArea(item.props.id);
setHoveringComponent(item.props.id);
}}
onMouseOut={(e) => {
e.stopPropagation();
setHoveringArea(null);
setHoveringComponent(null);
}}
>
{containsDropzone && (
<div
className={getClassNameLayer("chevron")}
title={isSelected ? "Collapse" : "Expand"}
>
<ChevronDown size="12" />
</div>
)}
<div className={getClassNameLayer("title")}>
<div className={getClassNameLayer("icon")}>
{item.type === "Text" || item.type === "Heading" ? (
<Type size="16" />
) : (
<Grid size="16" />
)}
</div>
{item.type}
</div>
</div>
</div>
{containsDropzone &&
Object.keys(dropzonesForItem).map((dropzoneKey, idx) => (
<div key={idx} className={getClassNameLayer("dropzones")}>
<LayerTree
data={data}
dropzoneContent={dropzones[dropzoneKey]}
setItemSelector={setItemSelector}
itemSelector={itemSelector}
dropzone={dropzoneKey}
/>
</div>
))}
</li>
);
})}
</ul>
</>
);
};
102 changes: 102 additions & 0 deletions packages/core/components/LayerTree/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
.LayerTree {
color: var(--puck-color-grey-2);
font-family: var(--puck-font-stack);
font-size: var(--puck-font-size-xxs);
margin: 0;
position: relative;
list-style: none;
padding: 0;
}

.LayerTree-dropzoneTitle {
color: var(--puck-color-grey-4);
font-size: var(--puck-font-size-xxxs);
text-transform: uppercase;
}

.LayerTree-helper {
text-align: center;
color: var(--puck-color-grey-6);
font-family: var(--puck-font-stack);
}

.Layer {
position: relative;
border: 1px solid transparent;
}

.Layer-inner {
padding-left: 20px;
padding-right: 8px;
border-radius: 3px;
}

.Layer--containsDropzone > .Layer-inner {
padding-left: 8px;
}

.Layer-clickable {
align-items: center;
display: flex;
}

.Layer-inner:hover {
cursor: pointer;
}

.Layer:not(.Layer--isSelected) > .Layer-inner:hover,
.Layer--isHovering > .Layer-inner {
color: var(--puck-color-blue);
background: var(--puck-color-azure-85);
}

.Layer--isSelected {
background: var(--puck-color-azure-9);
border-color: var(--puck-color-azure-7);
border-radius: 4px;
}

.Layer--isSelected > .Layer-inner {
background: var(--puck-color-azure-85);
font-weight: 600;
}

.Layer--isSelected > .Layer-inner > .Layer-clickable > .Layer-chevron,
.Layer:has(.Layer--isSelected)
> .Layer-inner
> .Layer-clickable
> .Layer-chevron {
transform: scaleY(-1);
}

.Layer-dropzones {
display: none;
margin-left: 20px;
}

.Layer--isSelected > .Layer-dropzones,
.Layer:has(.Layer--isSelected) > .Layer-dropzones {
display: block;
}

.Layer-dropzones > .LayerTree {
margin-left: 16px;
}

.Layer-title,
.LayerTree-dropzoneTitle {
display: flex;
gap: 8px;
align-items: center;
margin: 8px 4px;
}

.Layer-icon {
color: var(--puck-color-rose-6);
margin-top: 4px;
}

.Layer-dropzoneIcon {
color: var(--puck-color-grey-7);
margin-top: 4px;
}

0 comments on commit d0f5776

Please sign in to comment.