diff --git a/apps/demo/app/[...puckPath]/client.tsx b/apps/demo/app/[...puckPath]/client.tsx index e0d73c1e1b..06f79ed770 100644 --- a/apps/demo/app/[...puckPath]/client.tsx +++ b/apps/demo/app/[...puckPath]/client.tsx @@ -21,6 +21,8 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) { if (!isClient) return null; + const params = new URL(window.location.href).searchParams; + if (isEdit) { return (
@@ -32,6 +34,9 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) { }} plugins={[headingAnalyzer]} headerPath={path} + iframe={{ + enabled: params.get("disableIframe") === "true" ? false : true, + }} overrides={{ headerActions: ({ children }) => ( <> diff --git a/apps/demo/app/custom-ui/[...puckPath]/client.tsx b/apps/demo/app/custom-ui/[...puckPath]/client.tsx index d74fee2e2d..a15a5803bd 100644 --- a/apps/demo/app/custom-ui/[...puckPath]/client.tsx +++ b/apps/demo/app/custom-ui/[...puckPath]/client.tsx @@ -281,13 +281,15 @@ const CustomDrawer = () => { const { getPermissions } = usePuck(); return ( - +
{Object.keys(config.components).map((componentKey, componentIndex) => { @@ -299,19 +301,8 @@ const CustomDrawer = () => { - {({ children }) => ( -
- {children} -
- )} -
+ /> ); })}
@@ -377,7 +368,7 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) { outline: ({ children }) => (
{children}
), - actionBar: ({ children, label }) => { + actionBar: ({ children, label, parentAction }) => { const { getPermissions, selectedItem, refreshPermissions } = // Disable rules of hooks since this is a render function // eslint-disable-next-line react-hooks/rules-of-hooks @@ -397,7 +388,11 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) { if (!selectedItem) return ( - + + + {parentAction} + {label && } + {children} ); @@ -405,7 +400,11 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) { const isLocked = !!lockedComponents[selectedItem.props.id]; return ( - + + + {parentAction} + {label && } + {children} {globalPermissions.lockable && ( diff --git a/apps/demo/config/blocks/Blank/index.tsx b/apps/demo/config/blocks/Blank/index.tsx index 2ddd037d55..78e698a420 100644 --- a/apps/demo/config/blocks/Blank/index.tsx +++ b/apps/demo/config/blocks/Blank/index.tsx @@ -3,11 +3,11 @@ import { ComponentConfig } from "@/core"; import styles from "./styles.module.css"; import { getClassNameFactory } from "@/core/lib"; -const getClassName = getClassNameFactory("Hero", styles); +const getClassName = getClassNameFactory("Blank", styles); -export type HeroProps = {}; +export type BlankProps = {}; -export const Hero: ComponentConfig = { +export const Blank: ComponentConfig = { fields: {}, defaultProps: {}, render: () => { diff --git a/apps/demo/config/blocks/Blank/styles.module.css b/apps/demo/config/blocks/Blank/styles.module.css index f105fe365f..e7216445f4 100644 --- a/apps/demo/config/blocks/Blank/styles.module.css +++ b/apps/demo/config/blocks/Blank/styles.module.css @@ -1,4 +1,4 @@ -.Hero { +.Blank { background: hotpink; padding: 16px; } diff --git a/apps/demo/config/blocks/Button/index.tsx b/apps/demo/config/blocks/Button/index.tsx new file mode 100644 index 0000000000..8645569609 --- /dev/null +++ b/apps/demo/config/blocks/Button/index.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { ComponentConfig } from "@/core/types"; +import { Button as _Button } from "@/core/components/Button"; + +export type ButtonProps = { + label: string; + href: string; + variant: "primary" | "secondary"; +}; + +export const Button: ComponentConfig = { + label: "Button", + fields: { + label: { type: "text" }, + href: { type: "text" }, + variant: { + type: "radio", + options: [ + { label: "primary", value: "primary" }, + { label: "secondary", value: "secondary" }, + ], + }, + }, + defaultProps: { + label: "Button", + href: "#", + variant: "primary", + }, + render: ({ href, variant, label, puck }) => { + return ( +
+ <_Button + href={puck.isEditing ? "#" : href} + variant={variant} + size="large" + tabIndex={puck.isEditing ? -1 : undefined} + > + {label} + +
+ ); + }, +}; diff --git a/apps/demo/config/blocks/ButtonGroup/index.tsx b/apps/demo/config/blocks/ButtonGroup/index.tsx deleted file mode 100644 index ff2cc2747c..0000000000 --- a/apps/demo/config/blocks/ButtonGroup/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -import React from "react"; -import { ComponentConfig } from "@/core/types"; -import styles from "./styles.module.css"; -import { getClassNameFactory } from "@/core/lib"; -import { Button } from "@/core/components/Button"; -import { Section } from "../../components/Section"; - -const getClassName = getClassNameFactory("ButtonGroup", styles); - -export type ButtonGroupProps = { - align?: string; - buttons: { label: string; href: string; variant: "primary" | "secondary" }[]; -}; - -export const ButtonGroup: ComponentConfig = { - label: "Button Group", - fields: { - buttons: { - type: "array", - getItemSummary: (item) => item.label || "Button", - arrayFields: { - label: { type: "text" }, - href: { type: "text" }, - variant: { - type: "radio", - options: [ - { label: "primary", value: "primary" }, - { label: "secondary", value: "secondary" }, - ], - }, - }, - defaultItemProps: { - label: "Button", - href: "#", - variant: "primary", - }, - }, - align: { - type: "radio", - options: [ - { label: "left", value: "left" }, - { label: "center", value: "center" }, - ], - }, - }, - defaultProps: { - buttons: [{ label: "Learn more", href: "#", variant: "primary" }], - }, - render: ({ align, buttons, puck }) => { - return ( -
-
- {buttons.map((button, i) => ( - - ))} -
-
- ); - }, -}; diff --git a/apps/demo/config/blocks/ButtonGroup/styles.module.css b/apps/demo/config/blocks/ButtonGroup/styles.module.css deleted file mode 100644 index 39c7a41fb8..0000000000 --- a/apps/demo/config/blocks/ButtonGroup/styles.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.ButtonGroup-actions { - display: flex; - gap: 24px; - flex-wrap: wrap; -} - -.ButtonGroup--center .ButtonGroup-actions { - justify-content: center; -} diff --git a/apps/demo/config/blocks/Card/index.tsx b/apps/demo/config/blocks/Card/index.tsx index 33b16a6456..bb1d4550b2 100644 --- a/apps/demo/config/blocks/Card/index.tsx +++ b/apps/demo/config/blocks/Card/index.tsx @@ -5,6 +5,7 @@ import styles from "./styles.module.css"; import { getClassNameFactory } from "@/core/lib"; import dynamic from "next/dynamic"; import dynamicIconImports from "lucide-react/dynamicIconImports"; +import { withLayout, WithLayout } from "../../components/Layout"; const getClassName = getClassNameFactory("Card", styles); @@ -24,14 +25,14 @@ const iconOptions = Object.keys(dynamicIconImports).map((iconName) => ({ value: iconName, })); -export type CardProps = { +export type CardProps = WithLayout<{ title: string; description: string; icon?: string; mode: "flat" | "card"; -}; +}>; -export const Card: ComponentConfig = { +const CardInner: ComponentConfig = { fields: { title: { type: "text" }, description: { type: "textarea" }, @@ -56,10 +57,15 @@ export const Card: ComponentConfig = { render: ({ title, icon, description, mode }) => { return (
-
{icon && icons[icon]}
-
{title}
-
{description}
+
+
{icon && icons[icon]}
+ +
{title}
+
{description}
+
); }, }; + +export const Card = withLayout(CardInner); diff --git a/apps/demo/config/blocks/Card/styles.module.css b/apps/demo/config/blocks/Card/styles.module.css index e8d2f04806..c37f49d967 100644 --- a/apps/demo/config/blocks/Card/styles.module.css +++ b/apps/demo/config/blocks/Card/styles.module.css @@ -1,24 +1,24 @@ .Card { - display: flex; - flex-direction: column; - align-items: center; - margin-left: auto; - margin-right: auto; - gap: 16px; - width: 100%; + height: 100%; } .Card--card { background: white; box-shadow: rgba(140, 152, 164, 0.25) 0px 3px 6px 0px; border-radius: 8px; - flex: 1; max-width: 100%; - margin-left: unset; - margin-right: unset; - padding: 24px; +} + +.Card-inner { + align-items: center; + display: flex; + gap: 16px; + flex-direction: column; +} + +.Card--card .Card-inner { align-items: flex-start; - width: auto; + padding: 24px; } .Card-icon { diff --git a/apps/demo/config/blocks/Columns/index.tsx b/apps/demo/config/blocks/Columns/index.tsx deleted file mode 100644 index 36a38650ca..0000000000 --- a/apps/demo/config/blocks/Columns/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from "react"; -import { ComponentConfig } from "@/core/types"; -import styles from "./styles.module.css"; -import { getClassNameFactory } from "@/core/lib"; -import { DropZone } from "@/core/components/DropZone"; -import { Section } from "../../components/Section"; -import { generateId } from "@/core/lib/generate-id"; - -const getClassName = getClassNameFactory("Columns", styles); - -export type ColumnsProps = { - distribution: "auto" | "manual"; - columns: { - span?: number; - id?: string; - }[]; -}; - -export const Columns: ComponentConfig = { - // Dynamically generate an ID for each column - resolveData: ({ props }, { lastData }) => { - if (lastData?.props.columns.length === props.columns.length) { - return { props }; - } - - return { - props: { - ...props, - columns: props.columns.map((column) => ({ - ...column, - id: column.id ?? generateId(), - })), - }, - }; - }, - fields: { - distribution: { - type: "radio", - options: [ - { - value: "auto", - label: "Auto", - }, - { - value: "manual", - label: "Manual", - }, - ], - }, - columns: { - type: "array", - getItemSummary: (col) => - `Column (span ${ - col.span ? Math.max(Math.min(col.span, 12), 1) : "auto" - })`, - arrayFields: { - span: { - label: "Span (1-12)", - type: "number", - min: 0, - max: 12, - }, - }, - }, - }, - defaultProps: { - distribution: "auto", - columns: [{}, {}], - }, - render: ({ columns, distribution }) => { - return ( -
-
- {columns.map(({ span, id }, idx) => ( -
- -
- ))} -
-
- ); - }, -}; diff --git a/apps/demo/config/blocks/Columns/styles.module.css b/apps/demo/config/blocks/Columns/styles.module.css deleted file mode 100644 index c7ee50a066..0000000000 --- a/apps/demo/config/blocks/Columns/styles.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.Columns { - display: flex; - gap: 24px; - grid-template-columns: repeat(12, 1fr); - flex-direction: column; - min-height: 0; /* NEW */ - min-width: 0; /* NEW; needed for Firefox */ -} - -@media (min-width: 768px) { - .Columns { - display: grid; - } -} diff --git a/apps/demo/config/blocks/Flex/index.tsx b/apps/demo/config/blocks/Flex/index.tsx index abe6272f44..d9316b1af3 100644 --- a/apps/demo/config/blocks/Flex/index.tsx +++ b/apps/demo/config/blocks/Flex/index.tsx @@ -4,52 +4,76 @@ import styles from "./styles.module.css"; import { getClassNameFactory } from "@/core/lib"; import { DropZone } from "@/core/components/DropZone"; import { Section } from "../../components/Section"; +import { WithLayout, withLayout } from "../../components/Layout"; const getClassName = getClassNameFactory("Flex", styles); -export type FlexProps = { - items: { minItemWidth?: number }[]; - minItemWidth: number; -}; +export type FlexProps = WithLayout<{ + justifyContent: "start" | "center" | "end"; + direction: "row" | "column"; + gap: number; + wrap: "wrap" | "nowrap"; +}>; -export const Flex: ComponentConfig = { +const FlexInternal: ComponentConfig = { fields: { - items: { - type: "array", - arrayFields: { - minItemWidth: { - label: "Minimum Item Width", - type: "number", - min: 0, - }, - }, - getItemSummary: (_, id = -1) => `Item ${id + 1}`, + direction: { + label: "Direction", + type: "radio", + options: [ + { label: "Row", value: "row" }, + { label: "Column", value: "column" }, + ], + }, + justifyContent: { + label: "Justify Content", + type: "radio", + options: [ + { label: "Start", value: "start" }, + { label: "Center", value: "center" }, + { label: "End", value: "end" }, + ], }, - minItemWidth: { - label: "Minimum Item Width", + gap: { + label: "Gap", type: "number", min: 0, }, + wrap: { + label: "Wrap", + type: "radio", + options: [ + { label: "true", value: "wrap" }, + { label: "false", value: "nowrap" }, + ], + }, }, defaultProps: { - items: [{}, {}], - minItemWidth: 356, + justifyContent: "start", + direction: "row", + gap: 24, + wrap: "wrap", + layout: { + grow: true, + }, }, - render: ({ items, minItemWidth }) => { + render: ({ justifyContent, direction, gap, wrap }) => { return ( -
-
- {items.map((item, idx) => ( -
- -
- ))} -
+
+
); }, }; + +export const Flex = withLayout(FlexInternal); diff --git a/apps/demo/config/blocks/Flex/styles.module.css b/apps/demo/config/blocks/Flex/styles.module.css index a40652db83..64523e8109 100644 --- a/apps/demo/config/blocks/Flex/styles.module.css +++ b/apps/demo/config/blocks/Flex/styles.module.css @@ -1,9 +1,7 @@ .Flex { display: flex; - gap: 24px; - min-height: 0; - min-width: 0; flex-wrap: wrap; + height: 100%; } .Flex-item { diff --git a/apps/demo/config/blocks/Grid/index.tsx b/apps/demo/config/blocks/Grid/index.tsx new file mode 100644 index 0000000000..158248756f --- /dev/null +++ b/apps/demo/config/blocks/Grid/index.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { ComponentConfig } from "@/core/types"; +import styles from "./styles.module.css"; +import { getClassNameFactory } from "@/core/lib"; +import { DropZone } from "@/core/components/DropZone"; +import { Section } from "../../components/Section"; + +const getClassName = getClassNameFactory("Grid", styles); + +export type GridProps = { + numColumns: number; + gap: number; +}; + +export const Grid: ComponentConfig = { + fields: { + numColumns: { + type: "number", + label: "Number of columns", + min: 1, + max: 12, + }, + gap: { + label: "Gap", + type: "number", + min: 0, + }, + }, + defaultProps: { + numColumns: 4, + gap: 24, + }, + render: ({ gap, numColumns }) => { + return ( +
+ +
+ ); + }, +}; diff --git a/apps/demo/config/blocks/Grid/styles.module.css b/apps/demo/config/blocks/Grid/styles.module.css new file mode 100644 index 0000000000..b906547e32 --- /dev/null +++ b/apps/demo/config/blocks/Grid/styles.module.css @@ -0,0 +1,11 @@ +.Grid { + display: flex; + flex-direction: column; + width: auto; +} + +@media (min-width: 768px) { + .Grid { + display: grid; + } +} diff --git a/apps/demo/config/blocks/Heading/index.tsx b/apps/demo/config/blocks/Heading/index.tsx index 81bd0e93fa..357aebd849 100644 --- a/apps/demo/config/blocks/Heading/index.tsx +++ b/apps/demo/config/blocks/Heading/index.tsx @@ -4,14 +4,14 @@ import { ComponentConfig } from "@/core"; import { Heading as _Heading } from "@/core/components/Heading"; import type { HeadingProps as _HeadingProps } from "@/core/components/Heading"; import { Section } from "../../components/Section"; +import { WithLayout, withLayout } from "../../components/Layout"; -export type HeadingProps = { +export type HeadingProps = WithLayout<{ align: "left" | "center" | "right"; text?: string; level?: _HeadingProps["rank"]; size: _HeadingProps["size"]; - padding?: string; -}; +}>; const sizeOptions = [ { value: "xxxl", label: "XXXL" }, @@ -33,7 +33,7 @@ const levelOptions = [ { label: "6", value: "6" }, ]; -export const Heading: ComponentConfig = { +const HeadingInternal: ComponentConfig = { fields: { text: { type: "textarea", @@ -54,17 +54,18 @@ export const Heading: ComponentConfig = { { label: "Right", value: "right" }, ], }, - padding: { type: "text" }, }, defaultProps: { align: "left", text: "Heading", - padding: "24px", size: "m", + layout: { + padding: "8px", + }, }, - render: ({ align, text, size, level, padding }) => { + render: ({ align, text, size, level }) => { return ( -
+
<_Heading size={size} rank={level as any}> {text} @@ -74,3 +75,5 @@ export const Heading: ComponentConfig = { ); }, }; + +export const Heading = withLayout(HeadingInternal); diff --git a/apps/demo/config/blocks/Hero/index.tsx b/apps/demo/config/blocks/Hero/index.tsx index 514b4f1336..4604eb351f 100644 --- a/apps/demo/config/blocks/Hero/index.tsx +++ b/apps/demo/config/blocks/Hero/index.tsx @@ -221,12 +221,12 @@ export const Hero: ComponentConfig = { return (
{image?.mode === "background" && ( <> diff --git a/apps/demo/config/blocks/Space/index.tsx b/apps/demo/config/blocks/Space/index.tsx new file mode 100644 index 0000000000..d0fe4dec49 --- /dev/null +++ b/apps/demo/config/blocks/Space/index.tsx @@ -0,0 +1,46 @@ +import React from "react"; + +import { ComponentConfig } from "@/core"; +import { spacingOptions } from "../../options"; +import { getClassNameFactory } from "@/core/lib"; + +import styles from "./styles.module.css"; + +const getClassName = getClassNameFactory("Space", styles); + +export type SpaceProps = { + direction?: "" | "vertical" | "horizontal"; + size: string; +}; + +export const Space: ComponentConfig = { + label: "Space", + fields: { + size: { + type: "select", + options: spacingOptions, + }, + direction: { + type: "radio", + options: [ + { value: "vertical", label: "Vertical" }, + { value: "horizontal", label: "Horizontal" }, + { value: "", label: "Both" }, + ], + }, + }, + defaultProps: { + direction: "", + size: "24px", + }, + inline: true, + render: ({ direction, size, puck }) => { + return ( +
+ ); + }, +}; diff --git a/apps/demo/config/blocks/Space/styles.module.css b/apps/demo/config/blocks/Space/styles.module.css new file mode 100644 index 0000000000..25e6a99ba1 --- /dev/null +++ b/apps/demo/config/blocks/Space/styles.module.css @@ -0,0 +1,14 @@ +.Space { + display: block; + height: var(--size); + width: var(--size); +} + +.Space--vertical { + width: 100%; +} + +.Space--horizontal { + width: var(--size); + height: 100%; +} diff --git a/apps/demo/config/blocks/Text/index.tsx b/apps/demo/config/blocks/Text/index.tsx index 37d1619b10..940051e5eb 100644 --- a/apps/demo/config/blocks/Text/index.tsx +++ b/apps/demo/config/blocks/Text/index.tsx @@ -2,17 +2,18 @@ import React from "react"; import { ComponentConfig } from "@/core"; import { Section } from "../../components/Section"; +import { WithLayout, withLayout } from "../../components/Layout"; -export type TextProps = { +export type TextProps = WithLayout<{ align: "left" | "center" | "right"; text?: string; padding?: string; size?: "s" | "m"; color: "default" | "muted"; maxWidth?: string; -}; +}>; -export const Text: ComponentConfig = { +const TextInner: ComponentConfig = { fields: { text: { type: "textarea" }, size: { @@ -37,19 +38,17 @@ export const Text: ComponentConfig = { { label: "Muted", value: "muted" }, ], }, - padding: { type: "text" }, maxWidth: { type: "text" }, }, defaultProps: { align: "left", text: "Text", - padding: "24px", size: "m", color: "default", }, - render: ({ align, color, text, size, padding, maxWidth }) => { + render: ({ align, color, text, size, maxWidth }) => { return ( -
+
= { fontSize: size === "m" ? "20px" : "16px", fontWeight: 300, maxWidth, - marginLeft: "auto", - marginRight: "auto", justifyContent: align === "center" ? "center" @@ -76,3 +73,5 @@ export const Text: ComponentConfig = { ); }, }; + +export const Text = withLayout(TextInner); diff --git a/apps/demo/config/blocks/VerticalSpace/index.tsx b/apps/demo/config/blocks/VerticalSpace/index.tsx deleted file mode 100644 index 8befe58e39..0000000000 --- a/apps/demo/config/blocks/VerticalSpace/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; - -import { ComponentConfig } from "@/core"; -import { spacingOptions } from "../../options"; - -export type VerticalSpaceProps = { - size: string; -}; - -export const VerticalSpace: ComponentConfig = { - label: "Vertical Space", - fields: { - size: { - type: "select", - options: spacingOptions, - }, - }, - defaultProps: { - size: "24px", - }, - render: ({ size }) => { - return
; - }, -}; diff --git a/apps/demo/config/components/Footer/index.tsx b/apps/demo/config/components/Footer/index.tsx index 116f27d22a..318f4b4acd 100644 --- a/apps/demo/config/components/Footer/index.tsx +++ b/apps/demo/config/components/Footer/index.tsx @@ -58,19 +58,21 @@ const Footer = ({ children }: { children: ReactNode }) => { return (