Skip to content

Commit 594cc76

Browse files
committed
feat: add categories API for grouping components in side bar
1 parent 6145c32 commit 594cc76

12 files changed

Lines changed: 261 additions & 52 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@ The `Config` object describes which components Puck should render, how they shou
275275
- **fields** (`Field`): The Field objects describing the input data stored against this component.
276276
- **render** (`Component`): Render function for your React component. Receives props as defined in fields.
277277
- **defaultProps** (`object` [optional]): Default props to pass to your component. Will show in fields.
278+
- **categories** (`object`): Component categories for rendering in the side bar or restricting in DropZones
279+
- **[categoryName]** (`object`)
280+
- **components** (`sting[]`, [optional]): Array containing the names of components in this category
281+
- **title** (`sting`, [optional]): Title of the category
282+
- **visible** (`boolean`, [optional]): Whether or not the category should be visible in the side bar
283+
- **defaultExpanded** (`boolean`, [optional]): Whether or not the category should be expanded in the side bar by default
278284

279285
### `Field`
280286

apps/demo/app/[...puckPath]/client.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { Config, Data } from "@measured/puck";
3+
import { Data } from "@measured/puck/types/Config";
44
import { Puck } from "@measured/puck/components/Puck";
55
import { Render } from "@measured/puck/components/Render";
66
import { useEffect, useState } from "react";
@@ -40,7 +40,7 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) {
4040
return (
4141
<div>
4242
<Puck
43-
config={config as Config}
43+
config={config}
4444
data={data}
4545
onPublish={async (data: Data) => {
4646
localStorage.setItem(key, JSON.stringify(data));
@@ -60,7 +60,7 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) {
6060
}
6161

6262
if (data) {
63-
return <Render config={config as Config} data={data} />;
63+
return <Render config={config} data={data} />;
6464
}
6565

6666
return (

apps/demo/config/blocks/ButtonGroup/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @next/next/no-img-element */
22
import React from "react";
3-
import { ComponentConfig } from "@measured/puck";
3+
import { ComponentConfig } from "@measured/puck/types/Config";
44
import styles from "./styles.module.css";
55
import { getClassNameFactory } from "@measured/puck/lib";
66
import { Button } from "@measured/puck/components/Button";

apps/demo/config/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Config, Data } from "@measured/puck";
1+
import { Config, Data } from "@measured/puck/types/Config";
22
import { ButtonGroup, ButtonGroupProps } from "./blocks/ButtonGroup";
33
import { Card, CardProps } from "./blocks/Card";
44
import { Columns, ColumnsProps } from "./blocks/Columns";
@@ -30,6 +30,18 @@ export const conf: Config<Props, RootProps> = {
3030
root: {
3131
render: Root,
3232
},
33+
categories: {
34+
layout: {
35+
components: ["Columns", "Flex", "VerticalSpace"],
36+
},
37+
typography: {
38+
components: ["Heading", "Text"],
39+
},
40+
interactive: {
41+
title: "Actions",
42+
components: ["ButtonGroup"],
43+
},
44+
},
3345
components: {
3446
ButtonGroup,
3547
Card,
Lines changed: 94 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,110 @@
11
import DroppableStrictMode from "../DroppableStrictMode";
2-
import { Config } from "../../types/Config";
32

43
import styles from "./styles.module.css";
54
import getClassNameFactory from "../../lib/get-class-name-factory";
65
import { Draggable } from "../Draggable";
76
import { DragIcon } from "../DragIcon";
7+
import { ReactNode, useState } from "react";
8+
import { useAppContext } from "../Puck/context";
9+
import { ChevronDown, ChevronUp } from "react-feather";
810

911
const getClassName = getClassNameFactory("ComponentList", styles);
12+
const getClassNameItem = getClassNameFactory("ComponentListItem", styles);
1013

11-
export const ComponentList = ({ config }: { config: Config }) => {
14+
const ComponentListItem = ({
15+
component,
16+
index,
17+
}: {
18+
component: string;
19+
index: number;
20+
}) => {
1221
return (
13-
<DroppableStrictMode droppableId="component-list" isDropDisabled>
14-
{(provided, snapshot) => (
22+
<div className={getClassNameItem()}>
23+
<Draggable
24+
key={component}
25+
id={component}
26+
index={index}
27+
showShadow
28+
disableAnimations
29+
className={() => getClassNameItem("draggable")}
30+
>
31+
{() => (
32+
<>
33+
{component}
34+
<div className={getClassNameItem("icon")}>
35+
<DragIcon />
36+
</div>
37+
</>
38+
)}
39+
</Draggable>
40+
</div>
41+
);
42+
};
43+
44+
const ComponentList = ({
45+
children,
46+
title,
47+
defaultExpanded = true,
48+
}: {
49+
children?: ReactNode;
50+
title?: string;
51+
defaultExpanded?: boolean;
52+
}) => {
53+
const { config } = useAppContext();
54+
55+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
56+
57+
return (
58+
<div className={getClassName({ isExpanded })}>
59+
{title && (
1560
<div
16-
{...provided.droppableProps}
17-
ref={provided.innerRef}
18-
className={getClassName({
19-
isDraggingFrom: !!snapshot.draggingFromThisWith,
20-
})}
61+
className={getClassName("title")}
62+
onClick={() => setIsExpanded(!isExpanded)}
63+
title={
64+
isExpanded
65+
? `Collapse${title ? ` ${title}` : ""}`
66+
: `Expand${title ? ` ${title}` : ""}`
67+
}
2168
>
22-
{Object.keys(config.components).map((componentKey, i) => {
23-
return (
24-
<Draggable
25-
key={componentKey}
26-
id={componentKey}
27-
index={i}
28-
showShadow
29-
disableAnimations
30-
className={() => getClassName("item")}
31-
>
32-
{() => (
33-
<>
34-
{componentKey}
35-
<div className={getClassName("itemIcon")}>
36-
<DragIcon />
37-
</div>
38-
</>
39-
)}
40-
</Draggable>
41-
);
42-
})}
43-
{/* Use different element so we don't clash with :last-of-type */}
44-
<span style={{ display: "none" }}>{provided.placeholder}</span>
69+
<div>{title}</div>
70+
<div className={getClassName("titleIcon")}>
71+
{isExpanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
72+
</div>
4573
</div>
4674
)}
47-
</DroppableStrictMode>
75+
<div className={getClassName("content")}>
76+
<DroppableStrictMode
77+
droppableId={`component-list${title ? `:${title}` : ""}`}
78+
isDropDisabled
79+
>
80+
{(provided, snapshot) => (
81+
<div
82+
{...provided.droppableProps}
83+
ref={provided.innerRef}
84+
className={getClassName({
85+
isDraggingFrom: !!snapshot.draggingFromThisWith,
86+
})}
87+
>
88+
{children ||
89+
Object.keys(config.components).map((componentKey, i) => {
90+
return (
91+
<ComponentListItem
92+
key={componentKey}
93+
component={componentKey}
94+
index={i}
95+
/>
96+
);
97+
})}
98+
{/* Use different element so we don't clash with :last-of-type */}
99+
<span style={{ display: "none" }}>{provided.placeholder}</span>
100+
</div>
101+
)}
102+
</DroppableStrictMode>
103+
</div>
104+
</div>
48105
);
49106
};
107+
108+
ComponentList.Item = ComponentListItem;
109+
110+
export { ComponentList };

packages/core/components/ComponentList/styles.module.css

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,48 @@
33
max-width: 100%;
44
}
55

6-
.ComponentList-item {
6+
.ComponentList--isExpanded + .ComponentList {
7+
margin-top: 12px;
8+
}
9+
10+
.ComponentList-content {
11+
display: none;
12+
}
13+
14+
.ComponentList--isExpanded > .ComponentList-content {
15+
display: block;
16+
}
17+
18+
.ComponentList-title {
19+
color: var(--puck-color-grey-4);
20+
display: flex;
21+
font-size: var(--puck-font-size-xxxs);
22+
list-style: none;
23+
padding: 8px;
24+
text-transform: uppercase;
25+
gap: 4px;
26+
border-radius: 4px;
27+
}
28+
29+
.ComponentList--isExpanded .ComponentList-title {
30+
margin-bottom: 4px;
31+
}
32+
33+
.ComponentList-title:hover {
34+
background-color: var(--puck-color-azure-9);
35+
color: var(--puck-color-azure-4);
36+
cursor: pointer;
37+
}
38+
39+
.ComponentList-titleIcon {
40+
margin-left: auto;
41+
}
42+
43+
.ComponentListItem:last-of-type .ComponentListItem-draggable {
44+
margin-bottom: 0px;
45+
}
46+
47+
.ComponentListItem-draggable {
748
background: white;
849
padding: 12px;
950
display: flex;
@@ -17,15 +58,12 @@
1758
margin-bottom: 12px;
1859
}
1960

20-
.ComponentList-item:last-of-type {
21-
margin-bottom: 0px;
22-
}
23-
24-
.ComponentList-itemIcon {
61+
.ComponentListItemIcon {
2562
color: var(--puck-color-grey-4);
2663
}
2764

28-
.ComponentList:not(.ComponentList--isDraggingFrom) .ComponentList-item:hover {
65+
.ComponentList:not(.ComponentList--isDraggingFrom)
66+
.ComponentListItem-draggable:hover {
2967
background-color: var(--puck-color-azure-9);
3068
color: var(--puck-color-azure-4);
3169
}

packages/core/components/DropZone/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function DropZoneEdit({ zone, style }: DropZoneProps) {
8080

8181
const userIsDragging = !!draggedItem;
8282
const draggingOverArea = userIsDragging && zoneArea === draggedSourceArea;
83-
const draggingNewComponent = draggedSourceId === "component-list";
83+
const draggingNewComponent = draggedSourceId?.startsWith("component-list");
8484

8585
if (
8686
!ctx?.config ||

packages/core/components/Puck/context.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContext, useContext } from "react";
2-
import { AppState, UiState } from "../../types/Config";
2+
import { AppState, Config, UiState } from "../../types/Config";
33
import { PuckAction } from "../../reducer";
44
import { getItem } from "../../lib/get-item";
55

@@ -15,11 +15,13 @@ export const defaultAppState: AppState = {
1515
type AppContext = {
1616
state: AppState;
1717
dispatch: (action: PuckAction) => void;
18+
config: Config;
1819
};
1920

2021
export const appContext = createContext<AppContext>({
2122
state: defaultAppState,
2223
dispatch: () => null,
24+
config: { components: {} },
2325
});
2426

2527
export const AppProvider = appContext.Provider;

packages/core/components/Puck/index.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { areaContainsZones } from "../../lib/area-contains-zones";
3232
import { flushZones } from "../../lib/flush-zones";
3333
import { usePuckHistory } from "../../lib/use-puck-history";
3434
import { AppProvider, defaultAppState } from "./context";
35+
import { useComponentList } from "../../lib/use-component-list";
3536

3637
const Field = () => {};
3738

@@ -191,9 +192,11 @@ export function Puck({
191192
DragStart & Partial<DragUpdate>
192193
>();
193194

195+
const componentList = useComponentList(config);
196+
194197
return (
195198
<div className="puck">
196-
<AppProvider value={{ state: appState, dispatch }}>
199+
<AppProvider value={{ state: appState, dispatch, config }}>
197200
<DragDropContext
198201
onDragUpdate={(update) => {
199202
setDraggedItem({ ...draggedItem, ...update });
@@ -213,7 +216,7 @@ export function Puck({
213216

214217
// New component
215218
if (
216-
droppedItem.source.droppableId === "component-list" &&
219+
droppedItem.source.droppableId.startsWith("component-list") &&
217220
droppedItem.destination
218221
) {
219222
dispatch({
@@ -425,7 +428,7 @@ export function Puck({
425428
}}
426429
>
427430
<SidebarSection title="Components">
428-
<ComponentList config={config} />
431+
{componentList ? componentList : <ComponentList />}
429432
</SidebarSection>
430433
<SidebarSection title="Outline">
431434
{ctx?.activeZones &&

packages/core/lib/__tests__/use-puck-history.spec.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const mockedAppStateArray1: AppState = {
7676
},
7777
},
7878
itemSelector: { index: 0, zone: "default-zone" },
79+
componentList: {},
7980
},
8081
};
8182

@@ -118,6 +119,7 @@ const mockedAppStateArray2: AppState = {
118119
},
119120
},
120121
itemSelector: { index: 0, zone: "default-zone" },
122+
componentList: {},
121123
},
122124
};
123125

0 commit comments

Comments
 (0)