From d65e62c46efc31ee7a19e377d049cac34ca935f0 Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Wed, 26 Feb 2025 10:24:24 +0000 Subject: [PATCH] refactor: tidy up implementation after performance changes --- apps/docs/components/Preview/index.tsx | 2 +- .../AutoField/fields/ArrayField/index.tsx | 15 +- packages/core/components/AutoField/index.tsx | 2 +- .../core/components/ComponentList/index.tsx | 7 +- .../core/components/DragDropContext/index.tsx | 13 +- .../components/DraggableComponent/index.tsx | 18 +- packages/core/components/DropZone/context.tsx | 2 +- packages/core/components/DropZone/index.tsx | 7 +- .../DropZone/lib/use-content-with-preview.ts | 2 +- .../components/DropZone/lib/use-drag-axis.ts | 2 +- .../DropZone/lib/use-min-empty-height.ts | 5 +- packages/core/components/LayerTree/index.tsx | 4 +- packages/core/components/MenuBar/index.tsx | 10 +- .../Puck/components/Canvas/index.tsx | 2 +- .../Puck/components/Components/index.tsx | 2 +- .../Puck/components/Fields/index.tsx | 41 +-- .../Puck/components/Outline/index.tsx | 7 +- .../Puck/components/Preview/index.tsx | 8 +- packages/core/components/Puck/index.tsx | 212 +++++++++----- .../core/components/SidebarSection/index.tsx | 2 +- .../components/ViewportControls/index.tsx | 2 +- .../lib/__tests__/insert-component.spec.tsx | 2 +- .../lib/__tests__/is-child-of-zone.spec.tsx | 55 ---- .../core/lib/__tests__/resolve-data.spec.tsx | 74 ++--- .../lib/__tests__/use-breadcrumbs.spec.tsx | 192 ++++++------- packages/core/lib/get-item.ts | 12 +- packages/core/lib/resolve-data.ts | 10 +- packages/core/lib/use-breadcrumbs.ts | 8 +- packages/core/lib/use-component-list.tsx | 2 +- packages/core/lib/use-parent.ts | 13 +- packages/core/lib/use-preview-mode-hotkeys.ts | 9 +- packages/core/lib/use-puck.ts | 41 +-- packages/core/reducer/__tests__/data.spec.tsx | 2 +- .../core/reducer/__tests__/state.spec.tsx | 2 +- packages/core/store/index.ts | 267 ++++++++++++++++++ .../slices/__tests__/fields.spec.tsx} | 151 +++++----- .../slices/__tests__/history.spec.tsx} | 110 ++++---- .../slices/__tests__/nodes.spec.tsx} | 96 +++---- .../slices/__tests__/permissions.spec.tsx} | 133 ++++----- .../field-store.ts => store/slices/fields.ts} | 77 ++--- packages/core/store/slices/history.ts | 192 +++++++++++++ .../node-store.ts => store/slices/nodes.ts} | 139 ++++----- .../slices/permissions.ts} | 85 +++--- packages/core/stores/app-store.ts | 196 ------------- packages/core/stores/history-store.ts | 184 ------------ 45 files changed, 1284 insertions(+), 1133 deletions(-) delete mode 100644 packages/core/lib/__tests__/is-child-of-zone.spec.tsx create mode 100644 packages/core/store/index.ts rename packages/core/{stores/__tests__/field-store.spec.tsx => store/slices/__tests__/fields.spec.tsx} (72%) rename packages/core/{stores/__tests__/history-store.spec.tsx => store/slices/__tests__/history.spec.tsx} (62%) rename packages/core/{stores/__tests__/node-store.spec.tsx => store/slices/__tests__/nodes.spec.tsx} (59%) rename packages/core/{stores/__tests__/permissions-store.spec.tsx => store/slices/__tests__/permissions.spec.tsx} (76%) rename packages/core/{stores/field-store.ts => store/slices/fields.ts} (51%) create mode 100644 packages/core/store/slices/history.ts rename packages/core/{stores/node-store.ts => store/slices/nodes.ts} (57%) rename packages/core/{stores/permissions-store.ts => store/slices/permissions.ts} (73%) delete mode 100644 packages/core/stores/app-store.ts delete mode 100644 packages/core/stores/history-store.ts diff --git a/apps/docs/components/Preview/index.tsx b/apps/docs/components/Preview/index.tsx index 22706bbd52..21b4d408be 100644 --- a/apps/docs/components/Preview/index.tsx +++ b/apps/docs/components/Preview/index.tsx @@ -86,7 +86,7 @@ const ConfigPreviewInner = ({ {componentConfig.render && (
{componentConfig.render({ - ...appState.data["content"][0].props, + ...appState.data["content"][0]?.props, puck: { renderDropZone: () =>
, isEditing: false }, })}
diff --git a/packages/core/components/AutoField/fields/ArrayField/index.tsx b/packages/core/components/AutoField/fields/ArrayField/index.tsx index 1019aaf339..05b4a40366 100644 --- a/packages/core/components/AutoField/fields/ArrayField/index.tsx +++ b/packages/core/components/AutoField/fields/ArrayField/index.tsx @@ -7,10 +7,9 @@ import { reorder, replace } from "../../../../lib"; import { useCallback, useEffect, useState } from "react"; import { DragIcon } from "../../../DragIcon"; import { ArrayState, ItemWithId } from "../../../../types"; -import { getAppStore, useAppStore } from "../../../../stores/app-store"; +import { useAppStore, useAppStoreApi } from "../../../../store"; import { Sortable, SortableProvider } from "../../../Sortable"; import { NestedFieldProvider, useNestedFieldContext } from "../../context"; -import { usePermissionsStore } from "../../../../stores/permissions-store"; const getClassName = getClassNameFactory("ArrayField", styles); const getClassNameItem = getClassNameFactory("ArrayFieldItem", styles); @@ -47,9 +46,11 @@ export const ArrayField = ({ setLocalState({ arrayState, value }); }, [value, thisArrayState]); + const appStore = useAppStoreApi(); + const mapArrayStateToUi = useCallback( (partialArrayState: Partial) => { - const state = getAppStore().state; + const state = appStore.getState().state; return { arrayState: { ...state.ui.arrayState, @@ -57,7 +58,7 @@ export const ArrayField = ({ }, }; }, - [arrayState] + [arrayState, appStore] ); const getHighestIndex = useCallback(() => { @@ -105,8 +106,8 @@ export const ArrayField = ({ const [isDragging, setIsDragging] = useState(false); - const canEdit = usePermissionsStore( - (s) => s.getPermissions({ item: getAppStore().selectedItem }).edit + const canEdit = useAppStore( + (s) => s.permissions.getPermissions({ item: s.selectedItem }).edit ); const forceReadOnly = !canEdit; @@ -138,7 +139,7 @@ export const ArrayField = ({ move.target ); - const state = getAppStore().state; + const state = appStore.getState().state; const newUi = { arrayState: { diff --git a/packages/core/components/AutoField/index.tsx b/packages/core/components/AutoField/index.tsx index 8e88abae70..49cd268ebe 100644 --- a/packages/core/components/AutoField/index.tsx +++ b/packages/core/components/AutoField/index.tsx @@ -22,7 +22,7 @@ import { } from "./fields"; import { Lock } from "lucide-react"; import { ObjectField } from "./fields/ObjectField"; -import { useAppStore } from "../../stores/app-store"; +import { useAppStore } from "../../store"; import { useSafeId } from "../../lib/use-safe-id"; import { NestedFieldContext } from "./context"; diff --git a/packages/core/components/ComponentList/index.tsx b/packages/core/components/ComponentList/index.tsx index 6277880470..b9f0ce84d9 100644 --- a/packages/core/components/ComponentList/index.tsx +++ b/packages/core/components/ComponentList/index.tsx @@ -1,10 +1,9 @@ import styles from "./styles.module.css"; import getClassNameFactory from "../../lib/get-class-name-factory"; import { ReactNode } from "react"; -import { useAppStore } from "../../stores/app-store"; +import { useAppStore } from "../../store"; import { ChevronDown, ChevronUp } from "lucide-react"; import { Drawer } from "../Drawer"; -import { usePermissionsStore } from "../../stores/permissions-store"; const getClassName = getClassNameFactory("ComponentList", styles); @@ -17,9 +16,9 @@ const ComponentListItem = ({ index?: number; // TODO deprecate }) => { const overrides = useAppStore((s) => s.overrides); - const canInsert = usePermissionsStore( + const canInsert = useAppStore( (s) => - s.getPermissions({ + s.permissions.getPermissions({ type: name, }).insert ); diff --git a/packages/core/components/DragDropContext/index.tsx b/packages/core/components/DragDropContext/index.tsx index 2adf52d822..1f845d2be1 100644 --- a/packages/core/components/DragDropContext/index.tsx +++ b/packages/core/components/DragDropContext/index.tsx @@ -1,5 +1,5 @@ import { DragDropProvider } from "@dnd-kit/react"; -import { getAppStore, useAppStore } from "../../stores/app-store"; +import { useAppStore, useAppStoreApi } from "../../store"; import { createContext, Dispatch, @@ -29,7 +29,6 @@ import { generateId } from "../../lib/generate-id"; import { createStore } from "zustand"; import { getDeepDir } from "../../lib/get-deep-dir"; import { useSensors } from "../../lib/dnd/use-sensors"; -import { useNodeStore } from "../../stores/node-store"; const DEBUG = false; @@ -108,6 +107,7 @@ const DragDropContextClient = ({ const dispatch = useAppStore((s) => s.dispatch); const resolveData = useAppStore((s) => s.resolveData); const metadata = useAppStore((s) => s.metadata); + const appStore = useAppStoreApi(); const id = useId(); @@ -331,7 +331,7 @@ const DragDropContextClient = ({ if (thisPreview) { zoneStore.setState({ previewIndex: {} }); - const state = getAppStore().state; + const state = appStore.getState().state; if (thisPreview.type === "insert") { insertComponent( @@ -427,7 +427,7 @@ const DragDropContextClient = ({ targetIndex = 0; } - const path = useNodeStore.getState().nodes[target.id]?.path || []; + const path = appStore.getState().nodes.nodes[target.id]?.path || []; // Abort if dragging over self or descendant if ( @@ -464,7 +464,8 @@ const DragDropContextClient = ({ const item = getItem( initialSelector.current, - getAppStore().state.data + appStore.getState().state.data, + {} ); if (item) { @@ -496,7 +497,7 @@ const DragDropContextClient = ({ if (source && source.type !== "void") { const sourceData = source.data as ComponentDndData; - const { data } = getAppStore().state; + const { data } = appStore.getState().state; const item = getItem( { diff --git a/packages/core/components/DraggableComponent/index.tsx b/packages/core/components/DraggableComponent/index.tsx index 66acb29bc1..159088b66d 100644 --- a/packages/core/components/DraggableComponent/index.tsx +++ b/packages/core/components/DraggableComponent/index.tsx @@ -14,7 +14,7 @@ import styles from "./styles.module.css"; import "./styles.css"; import getClassNameFactory from "../../lib/get-class-name-factory"; import { Copy, CornerLeftUp, Trash } from "lucide-react"; -import { useAppStore } from "../../stores/app-store"; +import { useAppStore, useAppStoreApi } from "../../store"; import { Loader } from "../Loader"; import { ActionBar } from "../ActionBar"; @@ -28,8 +28,6 @@ import { useSortableSafe } from "../../lib/dnd/dnd-kit/safe"; import { getDeepScrollPosition } from "../../lib/get-deep-scroll-position"; import { ZoneStoreContext } from "../DropZone/context"; import { useContextStore } from "../../lib/use-context-store"; -import { useNodeStore } from "../../stores/node-store"; -import { usePermissionsStore } from "../../stores/permissions-store"; import { useShallow } from "zustand/react/shallow"; const getClassName = getClassNameFactory("DraggableComponent", styles); @@ -150,12 +148,12 @@ export const DraggableComponent = ({ const containsActiveZone = Object.values(localZones).filter(Boolean).length > 0; - const path = useNodeStore((s) => s.nodes[id]?.path); + const path = useAppStore((s) => s.nodes.nodes[id]?.path); - const item = useNodeStore((s) => s.nodes[id]?.data); + const item = useAppStore((s) => s.nodes.nodes[id]?.data); - const permissions = usePermissionsStore( - useShallow((s) => s.getPermissions({ item })) + const permissions = useAppStore( + useShallow((s) => s.permissions.getPermissions({ item })) ); const userIsDragging = useContextStore( @@ -273,7 +271,7 @@ export const DraggableComponent = ({ } }, [ref.current, userIsDragging]); - const registerNode = useNodeStore((s) => s.registerNode); + const registerNode = useAppStore((s) => s.nodes.registerNode); useEffect(() => { registerNode(id, { methods: { sync }, element: ref.current ?? null }); @@ -302,8 +300,10 @@ export const DraggableComponent = ({ [index, zoneCompound, id] ); + const appStore = useAppStoreApi(); + const onSelectParent = useCallback(() => { - const { nodes } = useNodeStore.getState(); + const { nodes } = appStore.getState().nodes; const node = nodes[id]; const parentNode = node?.parentId ? nodes[node?.parentId] : null; diff --git a/packages/core/components/DropZone/context.tsx b/packages/core/components/DropZone/context.tsx index 97fe1e4267..7a8b8c84cc 100644 --- a/packages/core/components/DropZone/context.tsx +++ b/packages/core/components/DropZone/context.tsx @@ -7,7 +7,7 @@ import { useState, } from "react"; import type { Draggable } from "@dnd-kit/dom"; -import { useAppStore } from "../../stores/app-store"; +import { useAppStore } from "../../store"; import { createStore, StoreApi } from "zustand"; export type PathData = Record; diff --git a/packages/core/components/DropZone/index.tsx b/packages/core/components/DropZone/index.tsx index 7850eb1425..637e571c0a 100644 --- a/packages/core/components/DropZone/index.tsx +++ b/packages/core/components/DropZone/index.tsx @@ -18,7 +18,7 @@ import { ZoneStoreContext, dropZoneContext, } from "./context"; -import { useAppStore } from "../../stores/app-store"; +import { useAppStore } from "../../store"; import { DropZoneProps } from "./types"; import { Content, DragAxis, PuckContext } from "../../types"; @@ -33,7 +33,6 @@ import { useContentIdsWithPreview } from "./lib/use-content-with-preview"; import { useDragAxis } from "./lib/use-drag-axis"; import { useContextStore } from "../../lib/use-context-store"; import { useShallow } from "zustand/react/shallow"; -import { useNodeStore } from "../../stores/node-store"; import { renderContext } from "../Render"; const getClassName = getClassNameFactory("DropZone", styles); @@ -217,7 +216,9 @@ const DropZoneEdit = forwardRef( activeZones, } = ctx!; - const path = useNodeStore((s) => (areaId ? s.nodes[areaId]?.path : null)); + const path = useAppStore((s) => + areaId ? s.nodes.nodes[areaId]?.path : null + ); let zoneCompound = rootDroppableId; diff --git a/packages/core/components/DropZone/lib/use-content-with-preview.ts b/packages/core/components/DropZone/lib/use-content-with-preview.ts index 971cab4e37..9b4e4e93db 100644 --- a/packages/core/components/DropZone/lib/use-content-with-preview.ts +++ b/packages/core/components/DropZone/lib/use-content-with-preview.ts @@ -4,7 +4,7 @@ import { useRenderedCallback } from "../../../lib/dnd/use-rendered-callback"; import { insert } from "../../../lib/insert"; import { ZoneStoreContext } from "../context"; import { useContextStore } from "../../../lib/use-context-store"; -import { useAppStore } from "../../../stores/app-store"; +import { useAppStore } from "../../../store"; export const useContentIdsWithPreview = ( contentIds: string[], diff --git a/packages/core/components/DropZone/lib/use-drag-axis.ts b/packages/core/components/DropZone/lib/use-drag-axis.ts index 44233da46f..cfa5d88121 100644 --- a/packages/core/components/DropZone/lib/use-drag-axis.ts +++ b/packages/core/components/DropZone/lib/use-drag-axis.ts @@ -1,6 +1,6 @@ import { RefObject, useCallback, useEffect, useState } from "react"; import { DragAxis } from "../../../types"; -import { useAppStore } from "../../../stores/app-store"; +import { useAppStore } from "../../../store"; const GRID_DRAG_AXIS: DragAxis = "dynamic"; const FLEX_ROW_DRAG_AXIS: DragAxis = "x"; diff --git a/packages/core/components/DropZone/lib/use-min-empty-height.ts b/packages/core/components/DropZone/lib/use-min-empty-height.ts index 56b789ce85..b27c60f2d1 100644 --- a/packages/core/components/DropZone/lib/use-min-empty-height.ts +++ b/packages/core/components/DropZone/lib/use-min-empty-height.ts @@ -1,7 +1,7 @@ import { RefObject, useEffect, useState } from "react"; import { ZoneStoreContext } from "./../context"; import { useContextStore } from "../../../lib/use-context-store"; -import { useNodeStore } from "../../../stores/node-store"; +import { useAppStoreApi } from "../../../store"; export const useMinEmptyHeight = ({ zoneCompound, @@ -12,6 +12,7 @@ export const useMinEmptyHeight = ({ userMinEmptyHeight: number; ref: RefObject; }) => { + const appStore = useAppStoreApi(); const [prevHeight, setPrevHeight] = useState(0); const [isAnimating, setIsAnimating] = useState(false); const { draggedItem, isZone } = useContextStore(ZoneStoreContext, (s) => { @@ -36,7 +37,7 @@ export const useMinEmptyHeight = ({ setPrevHeight(0); setTimeout(() => { - const { nodes } = useNodeStore.getState(); + const { nodes } = appStore.getState().nodes; Object.entries(nodes).forEach(([_, node]) => { node?.methods.sync(); diff --git a/packages/core/components/LayerTree/index.tsx b/packages/core/components/LayerTree/index.tsx index 58288992e6..701d8cd5ab 100644 --- a/packages/core/components/LayerTree/index.tsx +++ b/packages/core/components/LayerTree/index.tsx @@ -11,7 +11,7 @@ import { findZonesForArea } from "../../lib/find-zones-for-area"; import { getZoneId } from "../../lib/get-zone-id"; import { getFrame } from "../../lib/get-frame"; import { onScrollEnd } from "../../lib/on-scroll-end"; -import { useNodeStore } from "../../stores/node-store"; +import { useAppStore } from "../../store"; const getClassName = getClassNameFactory("LayerTree", styles); const getClassNameLayer = getClassNameFactory("Layer", styles); @@ -37,7 +37,7 @@ export const LayerTree = ({ const ctx = useContext(dropZoneContext); // TODO change this for performance - const nodes = useNodeStore((s) => s.nodes); + const nodes = useAppStore((s) => s.nodes.nodes); return ( <> diff --git a/packages/core/components/MenuBar/index.tsx b/packages/core/components/MenuBar/index.tsx index 7e022c00b8..85ed300fed 100644 --- a/packages/core/components/MenuBar/index.tsx +++ b/packages/core/components/MenuBar/index.tsx @@ -7,7 +7,7 @@ import { PuckAction } from "../../reducer"; import type { Data } from "../../types"; import styles from "./styles.module.css"; -import { useHistoryStore } from "../../stores/history-store"; +import { useAppStore } from "../../store"; const getClassName = getClassNameFactory("MenuBar", styles); @@ -22,10 +22,10 @@ export function MenuBar({ renderHeaderActions?: () => ReactElement; setMenuOpen: Dispatch>; }) { - const back = useHistoryStore((s) => s.back); - const forward = useHistoryStore((s) => s.forward); - const hasFuture = useHistoryStore((s) => s.hasFuture()); - const hasPast = useHistoryStore((s) => s.hasPast()); + const back = useAppStore((s) => s.history.back); + const forward = useAppStore((s) => s.history.forward); + const hasFuture = useAppStore((s) => s.history.hasFuture()); + const hasPast = useAppStore((s) => s.history.hasPast()); return (
(value: any, updatedUi?: Partial) => { + (fieldName: string, appStore: StoreApi) => + (value: any, updatedUi?: Partial) => { let currentProps; const { dispatch, resolveData, config, state, selectedItem } = - getAppStore(); + appStore.getState(); const { data, ui } = state; const { itemSelector } = ui; @@ -120,7 +118,7 @@ const createOnChange = }; const FieldsChild = ({ fieldName }: { fieldName: string }) => { - const field = useFieldStore((s) => s.fields[fieldName]); + const field = useAppStore((s) => s.fields.fields[fieldName]); const isReadOnly = useAppStore( (s) => ((s.selectedItem @@ -145,17 +143,21 @@ const FieldsChild = ({ fieldName }: { fieldName: string }) => { : `root_${field.type}_${fieldName}`; }); - const permissions = usePermissionsStore( + const permissions = useAppStore( useShallow((s) => { - const { selectedItem } = getAppStore(); + const { selectedItem, permissions } = s; return selectedItem - ? s.getPermissions({ item: selectedItem }) - : s.getPermissions({ root: true }); + ? permissions.getPermissions({ item: selectedItem }) + : permissions.getPermissions({ root: true }); }) ); - const onChange = useCallback(createOnChange(fieldName), [fieldName]); + const appStore = useAppStoreApi(); + + const onChange = useCallback(createOnChange(fieldName, appStore), [ + fieldName, + ]); if (!field || !id) return null; @@ -183,11 +185,14 @@ export const Fields = ({ wrapFields = true }: { wrapFields?: boolean }) => { return (loadingCount ?? 0) > 0; }); const itemSelector = useAppStore(useShallow((s) => s.state.ui.itemSelector)); + const id = useAppStore((s) => s.selectedItem?.props.id); + const appStore = useAppStoreApi(); + useRegisterFieldsSlice(appStore, id); - useRegisterFieldStore(); - - const fieldsLoading = useFieldStore((s) => s.loading); - const fieldNames = useFieldStore(useShallow((s) => Object.keys(s.fields))); + const fieldsLoading = useAppStore((s) => s.fields.loading); + const fieldNames = useAppStore( + useShallow((s) => Object.keys(s.fields.fields)) + ); const isLoading = fieldsLoading || componentResolving; diff --git a/packages/core/components/Puck/components/Outline/index.tsx b/packages/core/components/Puck/components/Outline/index.tsx index 3131be00d0..0c6cff76f5 100644 --- a/packages/core/components/Puck/components/Outline/index.tsx +++ b/packages/core/components/Puck/components/Outline/index.tsx @@ -2,7 +2,7 @@ import { areaContainsZones } from "../../../../lib/area-contains-zones"; import { findZonesForArea } from "../../../../lib/find-zones-for-area"; import { rootDroppableId } from "../../../../lib/root-droppable-id"; import { LayerTree } from "../../../LayerTree"; -import { getAppStore, useAppStore } from "../../../../stores/app-store"; +import { useAppStore, useAppStoreApi } from "../../../../store"; import { dropZoneContext } from "../../../DropZone"; import { useCallback, useMemo } from "react"; import { ItemSelector } from "../../../../lib/get-item"; @@ -13,17 +13,18 @@ export const Outline = () => { const outlineOverride = useAppStore((s) => s.overrides.outline); const { data, ui } = state; const { itemSelector } = ui; + const appStore = useAppStoreApi(); const setItemSelector = useCallback( (newItemSelector: ItemSelector | null) => { - const { dispatch } = getAppStore(); + const { dispatch } = appStore.getState(); dispatch({ type: "setUi", ui: { itemSelector: newItemSelector }, }); }, - [] + [appStore] ); const Wrapper = useMemo(() => outlineOverride || "div", [outlineOverride]); diff --git a/packages/core/components/Puck/components/Preview/index.tsx b/packages/core/components/Puck/components/Preview/index.tsx index 97e13b6cb1..b53160766a 100644 --- a/packages/core/components/Puck/components/Preview/index.tsx +++ b/packages/core/components/Puck/components/Preview/index.tsx @@ -1,7 +1,7 @@ import { DropZonePure } from "../../../DropZone"; import { rootDroppableId } from "../../../../lib/root-droppable-id"; import { RefObject, useCallback, useEffect, useRef, useMemo } from "react"; -import { useAppStore } from "../../../../stores/app-store"; +import { useAppStore } from "../../../../store"; import AutoFrame, { autoFrameContext } from "../../../AutoFrame"; import styles from "./styles.module.css"; import { getClassNameFactory } from "../../../../lib"; @@ -114,6 +114,12 @@ export const Preview = ({ id = "puck-preview" }: { id?: string }) => { ); + useEffect(() => { + if (!iframe.enabled) { + setStatus("READY"); + } + }, [iframe.enabled]); + return (
{ ); }; -export function Puck< +type PuckProps< UserConfig extends Config = Config, G extends UserGenerics = UserGenerics ->({ - children, - config, - data: initialData, - ui: initialUi, - onChange, - onPublish, - onAction, - permissions = {}, - plugins, - overrides, - renderHeader, - renderHeaderActions, - headerTitle, - headerPath, - viewports = defaultViewports, - iframe: _iframe, - dnd, - initialHistory: _initialHistory, - metadata, -}: { +> = { children?: ReactNode; config: UserConfig; data: Partial; @@ -129,14 +115,48 @@ export function Puck< }; initialHistory?: InitialHistory; metadata?: Metadata; -}) { +}; + +const propsContext = createContext>({}); + +function PropsProvider( + props: PuckProps +) { + return ( + + {props.children} + + ); +} + +const usePuckContext = () => + useContext(propsContext as Context); + +function PuckProvider< + UserConfig extends Config = Config, + G extends UserGenerics = UserGenerics +>({ children }: PropsWithChildren) { + const { + config, + data: initialData, + ui: initialUi, + onChange, + permissions = {}, + plugins, + overrides, + viewports = defaultViewports, + iframe: _iframe, + initialHistory: _initialHistory, + metadata, + } = usePuckContext(); + const iframe: IframeConfig = { enabled: true, waitForStyles: true, ..._iframe, }; - useInjectGlobalCss(iframe.enabled); + const [appStore] = useState(createAppStore); const [generatedAppState] = useState(() => { const initial = { ...defaultAppState.ui, ...initialUi }; @@ -248,13 +268,79 @@ export function Puck< _initialHistory?.index || blendedHistories.length - 1; const initialAppState = blendedHistories[initialHistoryIndex].state; - useRegisterHistoryStore({ + useRegisterHistorySlice(appStore, { histories: blendedHistories, index: initialHistoryIndex, initialAppState, }); - const dispatch = useAppStore((s) => s.dispatch); + useEffect(() => { + appStore.subscribe((s) => { + if (onChange) onChange(s.state.data as G["UserData"]); + }); + }, []); + + // Load all plugins into the overrides + const loadedOverrides = useLoadedOverrides({ + overrides: overrides, + plugins: plugins, + }); + + useEffect(() => { + appStore.setState({ + state: initialAppState, + config, + plugins: plugins || [], + overrides: loadedOverrides, + viewports, + iframe, + }); + }, []); + + useEffect(() => { + appStore.setState({ metadata }); + }, [metadata]); + + useRegisterNodesSlice(appStore); + useRegisterPermissionsSlice(appStore, permissions); + + useEffect(() => { + const { state, resolveData } = appStore.getState(); + + resolveData(state); + }, []); + + return ( + + {children} + + ); +} + +function PuckLayout< + UserConfig extends Config = Config, + G extends UserGenerics = UserGenerics +>({ children }: PropsWithChildren) { + const { + onChange, + onPublish, + onAction, + renderHeader, + renderHeaderActions, + headerTitle, + headerPath, + iframe: _iframe, + dnd, + initialHistory: _initialHistory, + } = usePuckContext(); + + const iframe: IframeConfig = { + enabled: true, + waitForStyles: true, + ..._iframe, + }; + + useInjectGlobalCss(iframe.enabled); const leftSideBarVisible = useAppStore((s) => s.state.ui.leftSideBarVisible); const rightSideBarVisible = useAppStore( @@ -263,18 +349,22 @@ export function Puck< const [menuOpen, setMenuOpen] = useState(false); + const appStore = useAppStoreApi(); + useEffect(() => { // TODO use selector - useAppStore.subscribe((s) => { + return appStore.subscribe((s) => { if (onChange) onChange(s.state.data as G["UserData"]); }); - }, []); + }, [appStore]); // DEPRECATED const rootProps = useAppStore( (s) => s.state.data.root.props || s.state.data.root.props ); + const dispatch = useAppStore((s) => s.dispatch); + const toggleSidebars = useCallback( (sidebar: "left" | "right") => { const widerViewport = window.matchMedia("(min-width: 638px)").matches; @@ -370,24 +460,20 @@ export function Puck< return DefaultOverride; }, [renderHeader]); - // Load all plugins into the overrides - const loadedOverrides = useLoadedOverrides({ - overrides: overrides, - plugins: plugins, - }); + const overrides = useAppStore((s) => s.overrides); const CustomPuck = useMemo( - () => loadedOverrides.puck || DefaultOverride, - [loadedOverrides] + () => overrides.puck || DefaultOverride, + [overrides] ); const CustomHeader = useMemo( - () => loadedOverrides.header || defaultHeaderRender, - [loadedOverrides] + () => overrides.header || defaultHeaderRender, + [overrides] ); const CustomHeaderActions = useMemo( - () => loadedOverrides.headerActions || defaultHeaderActionsRender, - [loadedOverrides] + () => overrides.headerActions || defaultHeaderActionsRender, + [overrides] ); const [mounted, setMounted] = useState(false); @@ -396,21 +482,6 @@ export function Puck< setMounted(true); }, []); - useEffect(() => { - useAppStore.setState({ - state: initialAppState, - config, - plugins: plugins || [], - overrides: loadedOverrides, - viewports, - iframe, - }); - }, []); - - useRegisterNodeStore(); - - useRegisterPermissionsStore(permissions); - const ready = useAppStore((s) => s.status === "READY"); useMonitorHotkeys(); @@ -427,12 +498,6 @@ export function Puck< usePreviewModeHotkeys(); - useEffect(() => { - const { state, resolveData } = getAppStore(); - - resolveData(state); - }, []); - return (
@@ -453,7 +518,7 @@ export function Puck<