mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 21:15:18 +09:00
Move breakpoints logic to designer (#782)
We have stores synchronization and can move the logic from canvas to designer. Here refactored this way breakpoints, got rid from direct atom changes without store and removed breakpoints pub sub events.
This commit is contained in:
1 parent
f9f64934db
commit
00da9c8e86
9 files changed
+125
-136
No files matched your search
@@ -28,14 +28,11 @@ import { useTrackSelectedElement } from "./shared/use-track-selected-element";
|
||||
import { WrapperComponentDev } from "./features/wrapper-component";
|
||||
import { useSync } from "./shared/sync";
|
||||
import { useManageProps } from "./shared/props";
|
||||
import {
|
||||
useManageBreakpoints,
|
||||
useInitializeBreakpoints,
|
||||
} from "./shared/breakpoints";
|
||||
import {
|
||||
rootInstanceContainer,
|
||||
useBreakpoints,
|
||||
useRootInstance,
|
||||
useSetBreakpoints,
|
||||
useSetPresetStyles,
|
||||
useSetRootInstance,
|
||||
useSetStyles,
|
||||
@@ -135,7 +132,6 @@ type DesignModeProps = {
|
||||
};
|
||||
|
||||
const DesignMode = ({ treeId, buildId }: DesignModeProps) => {
|
||||
useManageBreakpoints();
|
||||
useManageDesignModeStyles();
|
||||
useManageProps({ treeId });
|
||||
usePublishSelectedInstanceData();
|
||||
@@ -166,9 +162,9 @@ export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
|
||||
throw new Error("Tree is null");
|
||||
}
|
||||
const isDesignerReady = useSubscribeDesignerReady();
|
||||
useInitializeBreakpoints(data.breakpoints);
|
||||
useInitializeDesignTokens(data.designTokens);
|
||||
const assets = useAssets(data.assets);
|
||||
useSetBreakpoints(data.breakpoints);
|
||||
useAllUserProps(data.props);
|
||||
useSetPresetStyles(data.tree.presetStyles);
|
||||
useSetStyles(data.tree.styles);
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import store from "immerhin";
|
||||
import type { Breakpoint } from "@webstudio-is/css-data";
|
||||
import { useSubscribe } from "~/shared/pubsub";
|
||||
import {
|
||||
breakpointsContainer,
|
||||
rootInstanceContainer,
|
||||
stylesContainer,
|
||||
useBreakpoints,
|
||||
} from "~/shared/nano-states";
|
||||
import { addMediaRules } from "./styles";
|
||||
import { useSyncInitializeOnce } from "~/shared/hook-utils";
|
||||
import { filterMutable } from "~/shared/array-utils";
|
||||
|
||||
export const useInitializeBreakpoints = (breakpoints: Array<Breakpoint>) => {
|
||||
const [, setCurrentBreakpoints] = useBreakpoints();
|
||||
useSyncInitializeOnce(() => {
|
||||
setCurrentBreakpoints(breakpoints);
|
||||
addMediaRules(breakpoints);
|
||||
});
|
||||
};
|
||||
|
||||
const useBreakpointChange = () => {
|
||||
useSubscribe("breakpointChange", (breakpoint) => {
|
||||
store.createTransaction([breakpointsContainer], (breakpoints) => {
|
||||
const foundBreakpoint = breakpoints.find(
|
||||
({ id }) => id === breakpoint.id
|
||||
);
|
||||
if (foundBreakpoint) {
|
||||
foundBreakpoint.label = breakpoint.label;
|
||||
foundBreakpoint.minWidth = breakpoint.minWidth;
|
||||
} else {
|
||||
// Its a new breakpoint
|
||||
breakpoints.push(breakpoint);
|
||||
}
|
||||
|
||||
addMediaRules(breakpoints);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const useBreakpointDelete = () => {
|
||||
useSubscribe("breakpointDelete", (breakpoint) => {
|
||||
store.createTransaction(
|
||||
[breakpointsContainer, rootInstanceContainer, stylesContainer],
|
||||
(breakpoints, rootInstance, styles) => {
|
||||
if (rootInstance === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = breakpoints.findIndex(({ id }) => id === breakpoint.id);
|
||||
if (index !== -1) {
|
||||
breakpoints.splice(index, 1);
|
||||
}
|
||||
|
||||
// delete breakpoint styles
|
||||
filterMutable(styles, (style) => style.breakpointId !== breakpoint.id);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useManageBreakpoints = () => {
|
||||
useBreakpointChange();
|
||||
useBreakpointDelete();
|
||||
};
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
} from "@webstudio-is/react-sdk";
|
||||
import {
|
||||
validStaticValueTypes,
|
||||
type Breakpoint,
|
||||
type ValidStaticStyleValue,
|
||||
type StyleValue,
|
||||
type CssRule,
|
||||
@@ -80,6 +79,14 @@ const tokensCssEngine = createCssEngine({ name: "tokens" });
|
||||
const presetStylesEngine = createCssEngine({ name: "presetStyles" });
|
||||
|
||||
export const GlobalStyles = ({ assets }: { assets: Array<Asset> }) => {
|
||||
const [breakpoints] = useBreakpoints();
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
for (const breakpoint of breakpoints) {
|
||||
cssEngine.addMediaRule(breakpoint.id, breakpoint);
|
||||
}
|
||||
}, [breakpoints]);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
for (const style of helperStyles) {
|
||||
helpersCssEngine.addPlaintextRule(style);
|
||||
@@ -159,12 +166,6 @@ const toVarStyleWithFallback = (instanceId: string, style: Style): Style => {
|
||||
return dynamicStyle;
|
||||
};
|
||||
|
||||
export const addMediaRules = (breakpoints: Array<Breakpoint>) => {
|
||||
for (const breakpoint of breakpoints) {
|
||||
cssEngine.addMediaRule(breakpoint.id, breakpoint);
|
||||
}
|
||||
};
|
||||
|
||||
const wrappedRulesMap = new Map<string, StyleRule | PlaintextRule>();
|
||||
|
||||
const addRule = (id: string, cssRule: CssRule) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { type Breakpoint } from "@webstudio-is/css-data";
|
||||
import { type Publish } from "~/shared/pubsub";
|
||||
import ObjectId from "bson-objectid";
|
||||
import store from "immerhin";
|
||||
import type { Breakpoint } from "@webstudio-is/css-data";
|
||||
import { Button, TextField, Flex, Text } from "@webstudio-is/design-system";
|
||||
import { PlusIcon, TrashIcon } from "@webstudio-is/icons";
|
||||
import ObjectId from "bson-objectid";
|
||||
import { useBreakpoints } from "~/shared/nano-states";
|
||||
import { breakpointsContainer, useBreakpoints } from "~/shared/nano-states";
|
||||
import { replaceByOrAppendMutable } from "~/shared/array-utils";
|
||||
|
||||
type BreakpointEditorItemProps = {
|
||||
breakpoint: Breakpoint;
|
||||
@@ -87,16 +88,23 @@ const BreakpointEditorItem = ({
|
||||
};
|
||||
|
||||
type BreakpointsEditorProps = {
|
||||
breakpoints: Array<Breakpoint>;
|
||||
publish: Publish;
|
||||
onDelete: (breakpoint: Breakpoint) => void;
|
||||
};
|
||||
|
||||
export const BreakpointsEditor = ({
|
||||
publish,
|
||||
onDelete,
|
||||
}: BreakpointsEditorProps) => {
|
||||
const [breakpoints, setBreakpoints] = useBreakpoints();
|
||||
export const BreakpointsEditor = ({ onDelete }: BreakpointsEditorProps) => {
|
||||
const [breakpoints] = useBreakpoints();
|
||||
const [addedBreakpoints, setAddedBreakpoints] = useState<Breakpoint[]>([]);
|
||||
const storedBreakpoints = new Set<string>();
|
||||
for (const breakpoint of breakpoints) {
|
||||
storedBreakpoints.add(breakpoint.id);
|
||||
}
|
||||
// filter out new breakpoints which are already store
|
||||
// instead of deleting from state to avoid the case
|
||||
// when both states do not have such breakpoint
|
||||
// and focused input remounts
|
||||
const newBreakpoints = addedBreakpoints.filter(
|
||||
(breakpoint) => storedBreakpoints.has(breakpoint.id) === false
|
||||
);
|
||||
return (
|
||||
<Flex gap="2" direction="column">
|
||||
<Flex
|
||||
@@ -113,8 +121,8 @@ export const BreakpointsEditor = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setBreakpoints([
|
||||
...breakpoints,
|
||||
setAddedBreakpoints((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: ObjectId().toString(),
|
||||
label: "",
|
||||
@@ -125,20 +133,19 @@ export const BreakpointsEditor = ({
|
||||
prefix={<PlusIcon />}
|
||||
/>
|
||||
</Flex>
|
||||
{breakpoints.map((breakpoint) => {
|
||||
{[...breakpoints, ...newBreakpoints].map((breakpoint) => {
|
||||
return (
|
||||
<BreakpointEditorItem
|
||||
key={breakpoint.id}
|
||||
breakpoint={breakpoint}
|
||||
onChange={(updatedBreakpoint) => {
|
||||
publish({ type: "breakpointChange", payload: updatedBreakpoint });
|
||||
const nextBreakpoints = breakpoints.map((breakpoint) => {
|
||||
if (breakpoint.id === updatedBreakpoint.id) {
|
||||
return updatedBreakpoint;
|
||||
}
|
||||
return breakpoint;
|
||||
store.createTransaction([breakpointsContainer], (breakpoints) => {
|
||||
replaceByOrAppendMutable(
|
||||
breakpoints,
|
||||
updatedBreakpoint,
|
||||
({ id }) => id === updatedBreakpoint.id
|
||||
);
|
||||
});
|
||||
setBreakpoints(nextBreakpoints);
|
||||
}}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import store from "immerhin";
|
||||
import { type Breakpoint } from "@webstudio-is/css-data";
|
||||
import { type Publish, useSubscribe } from "~/shared/pubsub";
|
||||
import { useSubscribe } from "~/shared/pubsub";
|
||||
import {
|
||||
Text,
|
||||
DropdownMenu,
|
||||
@@ -23,16 +24,13 @@ import {
|
||||
useSubscribeZoomFromShortcut,
|
||||
} from "./use-subscribe-shortcuts";
|
||||
import { ConfirmationDialog } from "./confirmation-dialog";
|
||||
import { useBreakpoints } from "~/shared/nano-states";
|
||||
import {
|
||||
breakpointsContainer,
|
||||
stylesContainer,
|
||||
useBreakpoints,
|
||||
} from "~/shared/nano-states";
|
||||
import { utils } from "@webstudio-is/project";
|
||||
|
||||
declare module "~/shared/pubsub" {
|
||||
export interface PubsubMap {
|
||||
breakpointDelete: Breakpoint;
|
||||
breakpointChange: Breakpoint;
|
||||
loadBreakpoints: Array<Breakpoint>;
|
||||
}
|
||||
}
|
||||
import { removeByMutable } from "~/shared/array-utils";
|
||||
|
||||
type BreakpointSelectorItemProps = {
|
||||
breakpoint: Breakpoint;
|
||||
@@ -56,18 +54,14 @@ const menuItemCss = {
|
||||
minWidth: 180,
|
||||
};
|
||||
|
||||
type BreakpointsProps = {
|
||||
publish: Publish;
|
||||
};
|
||||
|
||||
export const Breakpoints = ({ publish }: BreakpointsProps) => {
|
||||
export const Breakpoints = () => {
|
||||
const [view, setView] = useState<
|
||||
"selector" | "editor" | "confirmation" | undefined
|
||||
>();
|
||||
const [breakpointToDelete, setBreakpointToDelete] = useState<
|
||||
Breakpoint | undefined
|
||||
>();
|
||||
const [breakpoints, setBreakpoints] = useBreakpoints();
|
||||
const [breakpoints] = useBreakpoints();
|
||||
const [selectedBreakpoint, setSelectedBreakpoint] = useSelectedBreakpoint();
|
||||
const [breakpointPreview, setBreakpointPreview] =
|
||||
useState(selectedBreakpoint);
|
||||
@@ -94,17 +88,20 @@ export const Breakpoints = ({ publish }: BreakpointsProps) => {
|
||||
if (breakpointToDelete === undefined) {
|
||||
return;
|
||||
}
|
||||
const nextBreakpoints = [...breakpoints];
|
||||
const index = breakpoints.indexOf(breakpointToDelete);
|
||||
nextBreakpoints.splice(index, 1);
|
||||
setBreakpoints(nextBreakpoints);
|
||||
const [updatedBreakpoints] = store.createTransaction(
|
||||
[breakpointsContainer, stylesContainer],
|
||||
(breakpoints, styles) => {
|
||||
removeByMutable(breakpoints, ({ id }) => id === breakpointToDelete.id);
|
||||
removeByMutable(
|
||||
styles,
|
||||
(style) => style.breakpointId === breakpointToDelete.id
|
||||
);
|
||||
}
|
||||
);
|
||||
if (breakpointToDelete === selectedBreakpoint) {
|
||||
setSelectedBreakpoint(utils.breakpoints.sort(nextBreakpoints)[0]);
|
||||
setSelectedBreakpoint(utils.breakpoints.sort(updatedBreakpoints)[0]);
|
||||
}
|
||||
publish({
|
||||
type: "breakpointDelete",
|
||||
payload: breakpointToDelete,
|
||||
});
|
||||
|
||||
setBreakpointToDelete(undefined);
|
||||
setView("editor");
|
||||
};
|
||||
@@ -140,8 +137,6 @@ export const Breakpoints = ({ publish }: BreakpointsProps) => {
|
||||
{view === "editor" && (
|
||||
<>
|
||||
<BreakpointsEditor
|
||||
breakpoints={breakpoints}
|
||||
publish={publish}
|
||||
onDelete={(breakpoint) => {
|
||||
setBreakpointToDelete(breakpoint);
|
||||
setView("confirmation");
|
||||
|
||||
@@ -41,7 +41,7 @@ export const Topbar = ({ css, project, publish, previewUrl }: TopbarProps) => {
|
||||
}}
|
||||
>
|
||||
<Menu publish={publish} />
|
||||
<Breakpoints publish={publish} />
|
||||
<Breakpoints />
|
||||
<Flex
|
||||
align="center"
|
||||
css={{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { filterMutable } from "./array-utils";
|
||||
import { removeByMutable, replaceByOrAppendMutable } from "./array-utils";
|
||||
|
||||
test("filterMutable", () => {
|
||||
test("removeByMutable", () => {
|
||||
const array = [
|
||||
{ param: 1 },
|
||||
{ param: 2 },
|
||||
@@ -10,7 +10,7 @@ test("filterMutable", () => {
|
||||
{ param: 5 },
|
||||
{ param: 6 },
|
||||
];
|
||||
filterMutable(array, (item) => item.param % 2 === 0);
|
||||
removeByMutable(array, (item) => item.param % 2 === 1);
|
||||
expect(array).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
@@ -24,7 +24,7 @@ test("filterMutable", () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
filterMutable(array, (item) => item.param === 4);
|
||||
removeByMutable(array, (item) => item.param !== 4);
|
||||
expect(array).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
@@ -33,3 +33,38 @@ test("filterMutable", () => {
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("replaceByOrAppendMutable", () => {
|
||||
const array = [{ param: 1 }, { param: 2 }, { param: 3 }];
|
||||
replaceByOrAppendMutable(array, { param: 6 }, (item) => item.param === 2);
|
||||
expect(array).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"param": 1,
|
||||
},
|
||||
{
|
||||
"param": 6,
|
||||
},
|
||||
{
|
||||
"param": 3,
|
||||
},
|
||||
]
|
||||
`);
|
||||
replaceByOrAppendMutable(array, { param: 8 }, (item) => item.param === 8);
|
||||
expect(array).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"param": 1,
|
||||
},
|
||||
{
|
||||
"param": 6,
|
||||
},
|
||||
{
|
||||
"param": 3,
|
||||
},
|
||||
{
|
||||
"param": 8,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -1,11 +1,26 @@
|
||||
export const filterMutable = <T>(
|
||||
array: T[],
|
||||
predicate: (value: T) => boolean
|
||||
type Predicate<Item> = (value: Item) => boolean;
|
||||
|
||||
export const removeByMutable = <Item>(
|
||||
array: Item[],
|
||||
predicate: Predicate<Item>
|
||||
) => {
|
||||
// reversed order to splice without breaking index
|
||||
for (let index = array.length - 1; index >= 0; index -= 1) {
|
||||
if (predicate(array[index]) === false) {
|
||||
if (predicate(array[index])) {
|
||||
array.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const replaceByOrAppendMutable = <Item>(
|
||||
array: Item[],
|
||||
item: Item,
|
||||
predicate: Predicate<Item>
|
||||
) => {
|
||||
const matchedIndex = array.findIndex(predicate);
|
||||
if (matchedIndex === -1) {
|
||||
array.push(item);
|
||||
} else {
|
||||
array[matchedIndex] = item;
|
||||
}
|
||||
};
|
||||
@@ -38,8 +38,13 @@ export const useSetStyles = (styles: Styles) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const breakpointsContainer = atom<Array<Breakpoint>>([]);
|
||||
export const breakpointsContainer = atom<Breakpoint[]>([]);
|
||||
export const useBreakpoints = () => useValue(breakpointsContainer);
|
||||
export const useSetBreakpoints = (breakpoints: Breakpoint[]) => {
|
||||
useSyncInitializeOnce(() => {
|
||||
breakpointsContainer.set(breakpoints);
|
||||
});
|
||||
};
|
||||
|
||||
const isPreviewModeContainer = atom<boolean>(false);
|
||||
export const useIsPreviewMode = () => useValue(isPreviewModeContainer);
|
||||
|
||||
Reference in new issue
Block a user