feat: WIP realtime collaboration to builder (#5726)

https://github.com/webstudio-is/webstudio/issues/46
This commit is contained in:
Oleg Isonen authored and GitHub committed 2026-04-27 23:19:21 +02:00
1 parent 7cfaf5533b
commit b006947a85
231 files changed
+8729 -2152

No files matched your search

+2 -2
View File
@@ -27,8 +27,6 @@ import {
$authPermit,
$authToken,
$isPreviewMode,
$pages,
$project,
subscribeResources,
$authTokenPermissions,
$isDesignMode,
@@ -38,6 +36,7 @@ import {
$stagingUsername,
$stagingPassword,
} from "~/shared/nano-states";
import { $project } from "~/shared/sync/data-stores";
import { $settings, type Settings } from "./shared/client-settings";
import { builderUrl, getCanvasUrl } from "~/shared/router-utils";
import { BlockingAlerts } from "./features/blocking-alerts";
@@ -52,6 +51,7 @@ import {
$isCloneDialogOpen,
$loadingState,
} from "./shared/nano-states";
import { $pages } from "~/shared/sync/data-stores";
import { CloneProjectDialog } from "~/shared/clone-project";
import type { TokenPermissions } from "@webstudio-is/authorization-token";
import { useToastErrors } from "~/shared/error/toast-error";
@@ -7,7 +7,8 @@ import { TopbarLayout } from "~/builder/shared/topbar-layout";
import { AddressBarPopover } from "./address-bar";
import { $dataSources, $pages } from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { $awareness, $selectedPage } from "~/shared/awareness";
import { $selectedPage } from "~/shared/nano-states";
import { $selectedPageId } from "~/shared/nano-states";
import { $currentSystem } from "~/shared/system";
registerContainers();
@@ -67,7 +68,7 @@ export default {
} satisfies Meta;
export const AddressBar: StoryFn = () => {
$awareness.set({ pageId: "dynamicId" });
$selectedPageId.set("dynamicId");
return (
<StorySection title="Address Bar">
<TopbarLayout
@@ -34,7 +34,7 @@ import {
matchPathnamePattern,
tokenizePathnamePattern,
} from "~/builder/shared/url-pattern";
import { $selectedPage, $selectedPagePath } from "~/shared/awareness";
import { $selectedPage, $selectedPagePath } from "~/shared/nano-states";
import { $currentSystem, updateCurrentSystem } from "~/shared/system";
const $selectedPageHistory = computed(
@@ -1,6 +1,6 @@
import { StorySection } from "@webstudio-is/design-system";
import { BreakpointsSelector as BreakpointsSelectorComponent } from "./breakpoints-selector";
import { $breakpoints } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import { $selectedBreakpointId } from "~/shared/nano-states/breakpoints";
const breakpointsMap = new Map([
@@ -15,8 +15,8 @@ import { CascadeIndicator } from "./cascade-indicator";
import {
$selectedBreakpoint,
$selectedBreakpointId,
$breakpoints,
} from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import { groupBreakpoints, isBaseBreakpoint } from "~/shared/breakpoints";
import { setCanvasWidth } from "../../shared/calc-canvas-width";
import { $canvasWidth } from "~/builder/shared/nano-states";
@@ -3,12 +3,10 @@ import { useEffect } from "react";
import { initialBreakpoints, coreMetas } from "@webstudio-is/sdk";
import { createDefaultPages } from "@webstudio-is/project-build";
import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas";
import {
$breakpoints,
$pages,
$registeredComponentMetas,
} from "~/shared/nano-states";
import { $awareness } from "~/shared/awareness";
import { $registeredComponentMetas } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import { $pages } from "~/shared/sync/data-stores";
import { $selectedPageId } from "~/shared/nano-states";
import { registerContainers } from "~/shared/sync/sync-stores";
import { StorySection } from "@webstudio-is/design-system";
import { CommandPanel as CommandPanelComponent } from "./command-panel";
@@ -57,7 +55,7 @@ pages.pages.push({
meta: {},
});
$pages.set(pages);
$awareness.set({ pageId: pages.homePage.id });
$selectedPageId.set(pages.homePage.id);
export const CommandPanel: StoryFn = () => {
useEffect(() => {
@@ -9,10 +9,10 @@ import { computed } from "nanostores";
import { compareMedia } from "@webstudio-is/css-engine";
import type { Breakpoint } from "@webstudio-is/sdk";
import {
$breakpoints,
$selectedBreakpoint,
$selectedBreakpointId,
} from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import { closeCommandPanel, $isCommandPanelOpen } from "../command-state";
import type { BaseOption } from "../shared/types";
import { setCanvasWidth } from "~/builder/shared/calc-canvas-width";
@@ -19,7 +19,7 @@ import {
insertWebstudioFragmentAt,
} from "~/shared/instance-utils";
import { humanizeString } from "~/shared/string-utils";
import { $selectedPage } from "~/shared/awareness";
import { $selectedPage } from "~/shared/nano-states";
import {
getInstanceLabel,
InstanceIcon,
@@ -13,12 +13,10 @@ import {
import { matchSorter } from "match-sorter";
import { computed } from "nanostores";
import { elementComponent, tags } from "@webstudio-is/sdk";
import {
$instances,
$props,
$registeredComponentMetas,
} from "~/shared/nano-states";
import { $selectedInstancePath } from "~/shared/awareness";
import { $registeredComponentMetas } from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $props } from "~/shared/sync/data-stores";
import { $selectedInstancePath } from "~/shared/nano-states";
import {
getInstanceLabel,
InstanceIcon,
@@ -17,12 +17,9 @@ import {
useSelectedAction,
} from "@webstudio-is/design-system";
import type { Instance, StyleSource } from "@webstudio-is/sdk";
import {
$styleSources,
$styles,
$breakpoints,
} from "~/shared/sync/data-stores";
import { $styleSources } from "~/shared/sync/data-stores";
import { $selectedStyleSources } from "~/shared/nano-states";
import { $styles, $breakpoints } from "~/shared/sync/data-stores";
import { findDuplicateTokens } from "~/shared/style-source-utils";
import { $styleSourceUsages } from "~/builder/shared/style-source-actions";
import { InstanceList, showInstance } from "../shared/instance-list";
@@ -11,7 +11,11 @@ import { parseComponentName } from "@webstudio-is/sdk";
import type { Instance } from "@webstudio-is/sdk";
import { $instances, $pages } from "~/shared/sync/data-stores";
import { getInstanceLabel } from "~/builder/shared/instance-label";
import { $awareness, findAwarenessByInstanceId } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { findPageAndSelectorByInstanceId } from "~/shared/instance-utils";
import { closeCommandPanel, $isCommandPanelOpen } from "../command-state";
import type { BaseOption } from "../shared/types";
import { setActiveSidebarPanel } from "~/builder/shared/nano-states";
@@ -79,13 +83,14 @@ export const InstancesGroup = ({ options }: { options: InstanceOption[] }) => {
const pages = $pages.get();
const instances = $instances.get();
if (pages && instances) {
const awareness = findAwarenessByInstanceId(
const awareness = findPageAndSelectorByInstanceId(
pages,
instances,
instance.id
);
if (awareness) {
$awareness.set(awareness);
$selectedPageId.set(awareness.pageId);
$selectedInstanceSelector.set(awareness.instanceSelector);
setActiveSidebarPanel("auto");
}
}
@@ -9,7 +9,8 @@ import { computed } from "nanostores";
import type { Page } from "@webstudio-is/sdk";
import { $pages } from "~/shared/sync/data-stores";
import { $editingPageId } from "~/shared/nano-states";
import { $selectedPage, selectPage } from "~/shared/awareness";
import { $selectedPage } from "~/shared/nano-states";
import { selectPage } from "~/shared/nano-states";
import { setActiveSidebarPanel } from "~/builder/shared/nano-states";
import { closeCommandPanel, $isCommandPanelOpen } from "../command-state";
import type { BaseOption } from "../shared/types";
@@ -9,13 +9,11 @@ import {
import { computed } from "nanostores";
import { elementComponent, tags } from "@webstudio-is/sdk";
import type { Instance } from "@webstudio-is/sdk";
import {
$instances,
$props,
$registeredComponentMetas,
} from "~/shared/nano-states";
import { $registeredComponentMetas } from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $props } from "~/shared/sync/data-stores";
import { insertWebstudioFragmentAt } from "~/shared/instance-utils";
import { $selectedInstancePath } from "~/shared/awareness";
import { $selectedInstancePath } from "~/shared/nano-states";
import { InstanceIcon } from "~/builder/shared/instance-label";
import { isTreeSatisfyingContentModel } from "~/shared/content-model";
import { closeCommandPanel, $isCommandPanelOpen } from "../command-state";
@@ -4,14 +4,12 @@ import * as baseMetas from "@webstudio-is/sdk-components-react/metas";
import * as animationMetas from "@webstudio-is/sdk-components-animation/metas";
import { createDefaultPages } from "@webstudio-is/project-build";
import { $, renderData } from "@webstudio-is/template";
import {
$instances,
$pages,
$props,
$registeredComponentMetas,
} from "~/shared/nano-states";
import { $registeredComponentMetas } from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $pages, $props } from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { $awareness, selectInstance } from "~/shared/awareness";
import { $selectedPageId } from "~/shared/nano-states";
import { selectInstance } from "~/shared/nano-states";
import { __testing__ } from "./wrap-group";
const { canWrapInstance } = __testing__;
@@ -25,7 +23,7 @@ const metas = new Map(
beforeEach(() => {
$registeredComponentMetas.set(metas);
$pages.set(createDefaultPages({ rootInstanceId: "" }));
$awareness.set({ pageId: "" });
$selectedPageId.set("");
});
describe("canWrapInstance for components", () => {
@@ -19,12 +19,10 @@ import type {
Props,
WsComponentMeta,
} from "@webstudio-is/sdk";
import {
$instances,
$props,
$registeredComponentMetas,
} from "~/shared/nano-states";
import { $selectedInstancePath } from "~/shared/awareness";
import { $registeredComponentMetas } from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $props } from "~/shared/sync/data-stores";
import { $selectedInstancePath } from "~/shared/nano-states";
import {
getInstanceLabel,
InstanceIcon,
@@ -18,9 +18,12 @@ import { $instances, $pages } from "~/shared/sync/data-stores";
import { getInstanceLabel } from "~/builder/shared/instance-label";
import { buildInstancePath } from "~/shared/instance-utils";
import { $commandContent } from "~/builder/features/command-panel/command-state";
import { findAwarenessByInstanceId } from "~/shared/awareness";
import { $awareness } from "~/shared/awareness";
import { findPageAndSelectorByInstanceId } from "~/shared/instance-utils";
import { $activeInspectorPanel } from "~/builder/shared/nano-states";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { useAutoSelectFirstItem } from "./auto-select";
import { InstancePathFooter } from "./instance-path-footer";
@@ -53,7 +56,11 @@ export const InstanceList = ({
continue;
}
const path = buildInstancePath(instanceId, pages, instances);
const awareness = findAwarenessByInstanceId(pages, instances, instanceId);
const awareness = findPageAndSelectorByInstanceId(
pages,
instances,
instanceId
);
const page = pages.pages.find((p) => p.id === awareness.pageId);
usedInInstances.push({
label: getInstanceLabel(instance),
@@ -154,8 +161,13 @@ export const showInstance = (
if (pagesData === undefined) {
return;
}
const awareness = findAwarenessByInstanceId(pagesData, instances, instanceId);
$awareness.set(awareness);
const { pageId, instanceSelector } = findPageAndSelectorByInstanceId(
pagesData,
instances,
instanceId
);
$selectedPageId.set(pageId);
$selectedInstanceSelector.set(instanceSelector);
if (panel !== undefined) {
$activeInspectorPanel.set(panel);
}
@@ -36,7 +36,7 @@ import {
insertWebstudioFragmentAt,
} from "~/shared/instance-utils";
import type { Publish } from "~/shared/pubsub";
import { $selectedPage } from "~/shared/awareness";
import { $selectedPage } from "~/shared/nano-states";
import { mapGroupBy } from "~/shared/shim";
import {
getInstanceLabel,
@@ -3,7 +3,8 @@ import { useStore } from "@nanostores/react";
import { ChevronRightIcon } from "@webstudio-is/icons";
import { theme, Button, Flex, Text } from "@webstudio-is/design-system";
import { $textEditingInstanceSelector } from "~/shared/nano-states";
import { $selectedInstancePath, selectInstance } from "~/shared/awareness";
import { $selectedInstancePath } from "~/shared/nano-states";
import { selectInstance } from "~/shared/nano-states";
import { getInstanceLabel } from "~/builder/shared/instance-label";
export const Breadcrumbs = () => {
@@ -35,7 +35,7 @@ import { builderApi } from "~/shared/builder-api";
import { insertPageCopyMutable } from "~/shared/page-utils";
import { Card } from "./card";
import type { MarketplaceOverviewItem } from "~/shared/marketplace/types";
import { selectPage } from "~/shared/awareness";
import { selectPage } from "~/shared/nano-states";
const isBody = (instance: Instance) =>
instance.component === "Body" ||
@@ -12,7 +12,7 @@ import { CollapsibleSection } from "~/builder/shared/collapsible-section";
import { highlightCss } from "~/shared/code-highlight";
import type { ComputedStyleDecl } from "~/shared/style-object-model";
import { $computedStyleDeclarations } from "~/builder/features/style-panel/shared/model";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance } from "~/shared/nano-states";
const preStyle = css(textVariants.mono, {
margin: 0,
@@ -40,27 +40,26 @@ import {
$blockChildOutline,
$editingItemSelector,
$hoveredInstanceSelector,
$instances,
$isContentMode,
$props,
$propsIndex,
$propValuesByInstanceSelector,
$registeredComponentMetas,
$selectedInstanceSelector,
getIndexedInstanceId,
type ItemDropTarget,
$propValuesByInstanceSelectorWithMemoryProps,
} from "~/shared/nano-states";
import { $instances, $props } from "~/shared/sync/data-stores";
import { isDescendantOrSelf, type InstanceSelector } from "~/shared/tree-utils";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { reparentInstance, toggleInstanceShow } from "~/shared/instance-utils";
import { emitCommand } from "~/builder/shared/commands";
import { useContentEditable } from "~/shared/dom-hooks";
import {
$selectedInstanceSelector,
$selectedPage,
getInstanceKey,
selectInstance,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import { selectInstance } from "~/shared/nano-states";
import {
findClosestContainer,
isRichTextContent,
@@ -1,4 +1,4 @@
import { $pages } from "~/shared/nano-states/pages";
import { $pages } from "~/shared/sync/data-stores";
import { PageSettings as PageSettingsComponent } from "./page-settings";
import {
Grid,
@@ -68,15 +68,14 @@ import {
} from "@webstudio-is/icons";
import { useIds } from "~/shared/form-utils";
import {
$assets,
$instances,
$pages,
$publishedOrigin,
$project,
$permissions,
$isDesignMode,
} from "~/shared/nano-states";
import { $assets } from "~/shared/sync/data-stores";
import { $project } from "~/shared/sync/data-stores";
import { $openProjectSettings } from "~/shared/nano-states/project-settings";
import { $instances, $pages } from "~/shared/sync/data-stores";
import {
BindingControl,
BindingPopover,
@@ -92,7 +91,7 @@ import {
validatePathnamePattern,
} from "~/builder/shared/url-pattern";
import { useUnmount } from "~/shared/hook-utils/use-mount";
import { selectInstance } from "~/shared/awareness";
import { selectInstance } from "~/shared/nano-states";
import { computeExpression } from "~/shared/data-variables";
import { $currentSystem } from "~/shared/system";
import { Card } from "../marketplace/card";
@@ -23,15 +23,15 @@ import {
reparentPageOrFolderMutable,
deletePageMutable,
} from "./page-utils";
import { $dataSourceVariables } from "~/shared/nano-states";
import {
$dataSourceVariables,
$dataSources,
$pages,
$project,
$resources,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { $awareness } from "~/shared/awareness";
import { $selectedPageId } from "~/shared/nano-states";
import { updateCurrentSystem } from "~/shared/system";
import { $resourcesCache, getResourceKey } from "~/shared/resources";
@@ -464,7 +464,7 @@ test("page root scope should rely on selected page", () => {
meta: {},
});
$pages.set(pages);
$awareness.set({ pageId: "pageId" });
$selectedPageId.set("pageId");
$dataSources.set(
toMap([
{
@@ -506,7 +506,7 @@ test("page root scope should use variable and resource values", () => {
homePageId: "homePageId",
})
);
$awareness.set({ pageId: "homePageId" });
$selectedPageId.set("homePageId");
$dataSources.set(
toMap([
{
@@ -574,7 +574,7 @@ test("page root scope should provide page system variable value", () => {
systemDataSourceId: "systemId",
})
);
$awareness.set({ pageId: "homePageId" });
$selectedPageId.set("homePageId");
$dataSources.set(
toMap([
{
@@ -21,18 +21,16 @@ import {
deleteInstanceMutable,
updateWebstudioData,
} from "~/shared/instance-utils";
import {
$dataSources,
$pages,
$variableValuesByInstanceSelector,
} from "~/shared/nano-states";
import { $variableValuesByInstanceSelector } from "~/shared/nano-states";
import { $dataSources } from "~/shared/sync/data-stores";
import { $pages } from "~/shared/sync/data-stores";
import { insertPageCopyMutable } from "~/shared/page-utils";
import {
$selectedPage,
getInstanceKey,
getInstancePath,
selectPage,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import { selectPage } from "~/shared/nano-states";
/**
* When page or folder needs to be deleted or moved to a different parent,
@@ -36,8 +36,8 @@ import {
$editingPageId,
$isContentMode,
$isDesignMode,
$pages,
} from "~/shared/nano-states";
import { $pages } from "~/shared/sync/data-stores";
import {
getAllChildrenAndSelf,
reparentOrphansMutable,
@@ -66,7 +66,8 @@ import {
import { atom, computed } from "nanostores";
import { isPathnamePattern } from "~/builder/shared/url-pattern";
import { updateWebstudioData } from "~/shared/instance-utils";
import { $selectedPage, selectPage } from "~/shared/awareness";
import { $selectedPage } from "~/shared/nano-states";
import { selectPage } from "~/shared/nano-states";
const ItemSuffix = ({
isParentSelected,
@@ -43,28 +43,25 @@ import {
} from "@webstudio-is/design-system";
import { validateProjectDomain, type Project } from "@webstudio-is/project";
import {
$awareness,
$selectedPagePath,
findAwarenessByInstanceId,
type Awareness,
} from "~/shared/awareness";
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { findPageAndSelectorByInstanceId } from "~/shared/instance-utils";
import { $selectedPageId } from "~/shared/nano-states";
import {
$authTokenPermissions,
$dataSources,
$editingPageId,
$instances,
$pages,
$project,
$publishedOrigin,
$permissions,
$stagingUsername,
$stagingPassword,
$publisherHost,
} from "~/shared/nano-states";
import { $publisherHost } from "~/shared/sync/data-stores";
import {
$publishDialog,
setActiveSidebarPanel,
} from "../../shared/nano-states";
import { $project } from "~/shared/sync/data-stores";
import { Domains, PENDING_TIMEOUT, getPublishStatusAndText } from "./domains";
import { CollapsibleDomainSection } from "./collapsible-domain-section";
import {
@@ -83,6 +80,7 @@ import { isPathnamePattern, type Templates } from "@webstudio-is/sdk";
import { DomainCheckbox, domainToPublishName } from "./domain-checkbox";
import { CopyToClipboard } from "~/shared/copy-to-clipboard";
import { $openProjectSettings } from "~/shared/nano-states/project-settings";
import { $dataSources, $instances, $pages } from "~/shared/sync/data-stores";
import { RelativeTime } from "~/builder/shared/relative-time";
import cmsUpgradeBanner from "~/shared/cms-upgrade-banner.svg?url";
@@ -327,7 +325,11 @@ const $restrictedFeatures = computed(
const features = new Map<
string,
| undefined
| { awareness?: Awareness; view?: "pageSettings"; info?: ReactNode }
| {
navigate?: { pageId: string; instanceSelector: string[] };
view?: "pageSettings";
info?: ReactNode;
}
>();
if (pages === undefined) {
return features;
@@ -342,16 +344,16 @@ const $restrictedFeatures = computed(
if (!permissions.allowDynamicData) {
// pages with dynamic paths
for (const page of [pages.homePage, ...pages.pages]) {
const awareness = {
const navigate = {
pageId: page.id,
instanceSelector: [page.rootInstanceId],
};
// allow catch all for 404 pages on free plan
if (isPathnamePattern(page.path) && page.path !== "/*") {
features.set("Dynamic path", { awareness, view: "pageSettings" });
features.set("Dynamic path", { navigate, view: "pageSettings" });
}
if (page.meta.redirect && page.meta.redirect !== `""`) {
features.set("Redirect", { awareness, view: "pageSettings" });
features.set("Redirect", { navigate, view: "pageSettings" });
}
}
// has resource variables
@@ -359,7 +361,11 @@ const $restrictedFeatures = computed(
if (dataSource.type === "resource") {
const instanceId = dataSource.scopeInstanceId ?? "";
features.set("Resource variable", {
awareness: findAwarenessByInstanceId(pages, instances, instanceId),
navigate: findPageAndSelectorByInstanceId(
pages,
instances,
instanceId
),
});
}
}
@@ -410,7 +416,11 @@ const Publish = ({
restrictedFeatures: Map<
string,
| undefined
| { awareness?: Awareness; view?: "pageSettings"; info?: ReactNode }
| {
navigate?: { pageId: string; instanceSelector: string[] };
view?: "pageSettings";
info?: ReactNode;
}
>;
}) => {
const { maxDailyPublishesPerUser } = useStore($permissions);
@@ -850,18 +860,21 @@ const UpgradeBanner = ({ hasCustomDomains }: { hasCustomDomains: boolean }) => {
<Text variant="regularBold">Following Pro features are used:</Text>
<Text as="ul" color="destructive" css={{ paddingLeft: "1em" }}>
{Array.from(restrictedFeatures).map(
([message, { awareness, view, info } = {}], index) => (
([message, { navigate, view, info } = {}], index) => (
<li key={index}>
<Flex align="center" gap="1">
{awareness ? (
{navigate ? (
<button
className={buttonLinkClass}
type="button"
onClick={() => {
$awareness.set(awareness);
$selectedPageId.set(navigate.pageId);
$selectedInstanceSelector.set(
navigate.instanceSelector
);
if (view === "pageSettings") {
setActiveSidebarPanel("pages");
$editingPageId.set(awareness.pageId);
$editingPageId.set(navigate.pageId);
}
}}
>
@@ -26,19 +26,16 @@ import {
validatePrimitiveValue,
type BindingVariant,
} from "~/builder/shared/binding-popover";
import {
$dataSources,
$props,
$resources,
$variableValuesByInstanceSelector,
} from "~/shared/nano-states";
import { $variableValuesByInstanceSelector } from "~/shared/nano-states";
import { $dataSources } from "~/shared/sync/data-stores";
import { $props, $resources } from "~/shared/sync/data-stores";
import { computeExpression } from "~/shared/data-variables";
import { updateWebstudioData } from "~/shared/instance-utils";
import {
$selectedInstance,
$selectedInstanceKeyWithRoot,
$selectedPage,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import {
UrlField,
MethodField,
@@ -4,14 +4,12 @@ import { useStore } from "@nanostores/react";
import { Box, Combobox, Select, theme } from "@webstudio-is/design-system";
import { elementsByTag } from "@webstudio-is/html-data";
import { tags } from "@webstudio-is/sdk";
import { $selectedInstance, $selectedInstancePath } from "~/shared/awareness";
import { $selectedInstance, $selectedInstancePath } from "~/shared/nano-states";
import { updateWebstudioData } from "~/shared/instance-utils";
import { isTreeSatisfyingContentModel } from "~/shared/content-model";
import {
$instances,
$props,
$registeredComponentMetas,
} from "~/shared/nano-states";
import { $registeredComponentMetas } from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $props } from "~/shared/sync/data-stores";
import { type ControlProps, VerticalLayout } from "../shared";
import { FieldLabel } from "../property-label";
@@ -16,7 +16,7 @@ import { AlertIcon } from "@webstudio-is/icons";
import type { Prop } from "@webstudio-is/sdk";
import { showAttribute } from "@webstudio-is/react-sdk";
import { updateWebstudioData } from "~/shared/instance-utils";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance } from "~/shared/nano-states";
import { $props } from "~/shared/sync/data-stores";
import {
$selectedInstanceInitialPropNames,
@@ -10,7 +10,7 @@ import {
type StyleSourceSelections,
} from "@webstudio-is/sdk";
import { isBaseBreakpoint } from "~/shared/nano-states";
import { isBaseBreakpoint } from "~/shared/breakpoints-utils";
import { camelCaseProperty } from "@webstudio-is/css-data";
export const setListedCssProperty =
@@ -3,10 +3,10 @@ import { nanoid } from "nanoid";
import { useState } from "react";
import {
$hoveredInstanceSelector,
$instances,
$registeredComponentMetas,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { $selectedInstanceSelector } from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { getInstanceStyleDecl } from "~/builder/features/style-panel/shared/model";
import { updateWebstudioData } from "~/shared/instance-utils";
import { toValue } from "@webstudio-is/css-engine";
@@ -1,16 +1,16 @@
import { describe, it, expect } from "vitest";
import { describe, test, expect } from "vitest";
import { matchMediaBreakpoints } from "./match-media-breakpoints";
import type { IsEqual } from "type-fest";
describe("matchMediaBreakpoints", () => {
it("returns undefined when values array is undefined", () => {
test("returns undefined when values array is undefined", () => {
const matchingBreakpointIds = ["mobile", "tablet", "desktop"];
const matcher = matchMediaBreakpoints(matchingBreakpointIds);
expect(matcher(undefined)).toBeUndefined();
});
it("returns undefined when no matching breakpoints are found", () => {
test("returns undefined when no matching breakpoints are found", () => {
const matchingBreakpointIds = ["mobile", "tablet", "desktop"];
const values: Array<[string, number]> = [
["other-breakpoint", 100],
@@ -22,7 +22,7 @@ describe("matchMediaBreakpoints", () => {
expect(matcher(values)).toBeUndefined();
});
it("returns the value of the last matching breakpoint", () => {
test("returns the value of the last matching breakpoint", () => {
const matchingBreakpointIds = ["mobile", "tablet"];
const values: Array<[string, number]> = [
["mobile", 320],
@@ -35,7 +35,7 @@ describe("matchMediaBreakpoints", () => {
expect(matcher(values)).toBe(768);
});
it("preserves the value type", () => {
test("preserves the value type", () => {
const matchingBreakpointIds = ["mobile", "tablet", "desktop"];
const stringValues: Array<[string, string]> = [["mobile", "small"]];
@@ -9,15 +9,11 @@ import {
} from "@webstudio-is/design-system";
import { PropsSection as PropsSectionComponent } from "./props-section";
import { usePropsLogic } from "./use-props-logic";
import {
$assets,
$instances,
$pages,
$props,
registerComponentLibrary,
} from "~/shared/nano-states";
import { registerComponentLibrary } from "~/shared/nano-states";
import { $assets } from "~/shared/sync/data-stores";
import { $instances, $pages, $props } from "~/shared/sync/data-stores";
import { createDefaultPages } from "@webstudio-is/project-build";
import { $awareness } from "~/shared/awareness";
import { $selectedPageId } from "~/shared/nano-states";
let id = 0;
const unique = () => `${++id}`;
@@ -95,7 +91,7 @@ const rootInstance = addLinkableSections(
["company", "employees"],
$pages.get()?.pages[0]
);
$awareness.set({ pageId: $pages.get()?.homePage.id ?? "" });
$selectedPageId.set($pages.get()?.homePage.id ?? "");
const instance: Instance = {
id: instanceId,
@@ -26,15 +26,15 @@ import {
import {
$propValuesByInstanceSelector,
$propsIndex,
$props,
$isDesignMode,
$isContentMode,
$memoryProps,
$selectedBreakpoint,
} from "~/shared/nano-states";
import { $props } from "~/shared/sync/data-stores";
import { CollapsibleSectionWithAddButton } from "~/builder/shared/collapsible-section";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { $selectedInstance, $selectedInstanceKey } from "~/shared/awareness";
import { $selectedInstance, $selectedInstanceKey } from "~/shared/nano-states";
import { renderControl } from "../controls/combined";
import { usePropsLogic, type PropAndMeta } from "./use-props-logic";
import { AnimationSection } from "./animation/animation-section";
@@ -10,13 +10,13 @@ import {
textContentAttribute,
} from "@webstudio-is/react-sdk";
import {
$instances,
$isContentMode,
$props,
$registeredComponentMetas,
} from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $props } from "~/shared/sync/data-stores";
import { isRichText } from "~/shared/content-model";
import { $selectedInstance, $selectedInstancePath } from "~/shared/awareness";
import { $selectedInstance, $selectedInstancePath } from "~/shared/nano-states";
import {
$selectedInstanceInitialPropNames,
$selectedInstancePropsMetas,
@@ -47,11 +47,9 @@ import {
} from "@webstudio-is/design-system";
import { TrashIcon, InfoCircleIcon, PlusIcon } from "@webstudio-is/icons";
import { humanizeString } from "~/shared/string-utils";
import {
$dataSources,
$resources,
$variableValuesByInstanceSelector,
} from "~/shared/nano-states";
import { $variableValuesByInstanceSelector } from "~/shared/nano-states";
import { $dataSources } from "~/shared/sync/data-stores";
import { $resources } from "~/shared/sync/data-stores";
import {
BindingControl,
BindingPopover,
@@ -69,7 +67,7 @@ import {
$selectedPage,
getInstanceKey,
type InstancePath,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import { updateWebstudioData } from "~/shared/instance-utils";
import { rebindTreeVariablesMutable } from "~/shared/data-variables";
import { parseCurl, type CurlRequest } from "./curl";
@@ -5,7 +5,7 @@ import { InputField } from "@webstudio-is/design-system";
import { $instances } from "~/shared/sync/data-stores";
import { HorizontalLayout, Label, Row, useLocalValue } from "./shared";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance } from "~/shared/nano-states";
import { getInstanceLabel } from "~/builder/shared/instance-label";
const saveLabel = (label: string, selectedInstance: Instance) => {
@@ -42,16 +42,16 @@ import {
} from "@webstudio-is/design-system";
import {
$dataSourceVariables,
$dataSources,
$registeredComponentMetas,
$variableValuesByInstanceSelector,
} from "~/shared/nano-states";
import { $dataSources } from "~/shared/sync/data-stores";
import type { BindingVariant } from "~/builder/shared/binding-popover";
import { humanizeString } from "~/shared/string-utils";
import {
$selectedInstance,
$selectedInstanceKeyWithRoot,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import { $instanceTags } from "../style-panel/shared/model";
export const showAttributeMeta: PropMeta = {
@@ -54,17 +54,15 @@ import {
formatValue,
} from "~/builder/shared/expression-editor";
import {
$dataSources,
$resources,
$permissions,
$instances,
$props,
$variableValuesByInstanceSelector,
} from "~/shared/nano-states";
import { $dataSources } from "~/shared/sync/data-stores";
import { $resources, $instances, $props } from "~/shared/sync/data-stores";
import {
$selectedInstance,
$selectedInstanceKeyWithRoot,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import {
EditorContent,
EditorDialog,
@@ -4,7 +4,7 @@ import { createDefaultPages } from "@webstudio-is/project-build";
import { $pages, $instances } from "~/shared/sync/data-stores";
import { $planFeatures } from "~/shared/nano-states";
import { registerContainers } from "~/shared/sync/sync-stores";
import { $awareness } from "~/shared/awareness";
import { $selectedPageId } from "~/shared/nano-states";
import { VariablesSection as VariablesSectionComponent } from "./variables-section";
$planFeatures.set({
@@ -24,7 +24,8 @@ $instances.set(
])
);
$pages.set(createDefaultPages({ rootInstanceId: "box" }));
$awareness.set({ pageId: "home", instanceSelector: ["box"] });
$selectedPageId.set("home");
// awareness not needed for stories - instanceSelector set via selectInstance if required
export const VariablesSection: StoryObj = {
render: () => (
@@ -22,14 +22,14 @@ import {
import { EllipsesIcon, PlusIcon } from "@webstudio-is/icons";
import type { DataSource } from "@webstudio-is/sdk";
import { findPageByIdOrPath } from "@webstudio-is/sdk";
import { $variableValuesByInstanceSelector } from "~/shared/nano-states";
import { $dataSources } from "~/shared/sync/data-stores";
import {
$dataSources,
$instances,
$pages,
$props,
$resources,
$variableValuesByInstanceSelector,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import {
CollapsibleSectionRoot,
useOpenState,
@@ -40,7 +40,7 @@ import {
$selectedInstance,
$selectedInstanceKeyWithRoot,
$selectedPage,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import { updateWebstudioData } from "~/shared/instance-utils";
import {
deleteVariableMutable,
@@ -17,12 +17,9 @@ import {
IconLink,
} from "@webstudio-is/design-system";
import { humanizeString } from "~/shared/string-utils";
import {
$breakpoints,
$instances,
$registeredComponentMetas,
$styleSources,
} from "~/shared/nano-states";
import { $registeredComponentMetas } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import { $instances, $styleSources } from "~/shared/sync/data-stores";
import type {
ComputedStyleDecl,
StyleValueSourceColor,
@@ -31,7 +28,7 @@ import { useComputedStyles } from "./shared/model";
import { useReadonly } from "./shared/readonly";
import { StyleSourceBadge } from "./style-source";
import { createBatchUpdate } from "./shared/use-style-data";
import { $virtualInstances } from "~/shared/awareness";
import { $virtualInstances } from "~/shared/nano-states";
import { getInstanceLabel } from "~/builder/shared/instance-label";
const $isAltPressed = atom(false);
@@ -21,7 +21,7 @@ import { useComputedStyles } from "../../shared/model";
import { getDots } from "../../shared/style-section";
import { CssEditor } from "../../../../shared/css-editor";
import { $advancedStyleDeclarations } from "./stores";
import { $selectedInstanceKey } from "~/shared/awareness";
import { $selectedInstanceKey } from "~/shared/nano-states";
import { useReadonly } from "../../shared/readonly";
// Only here to keep the same section module interface
@@ -2,7 +2,7 @@ import { computed } from "nanostores";
import type { CssProperty } from "@webstudio-is/css-engine";
import { ROOT_INSTANCE_ID } from "@webstudio-is/sdk";
import { $settings } from "~/builder/shared/client-settings";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance } from "~/shared/nano-states";
import type { ComputedStyleDecl } from "~/shared/style-object-model";
import { $computedStyleDeclarations } from "../../shared/model";
import { sections } from "../sections";
@@ -7,18 +7,21 @@ import {
theme,
} from "@webstudio-is/design-system";
import { getStyleDeclKey, type StyleDecl } from "@webstudio-is/sdk";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$instances,
$pages,
$selectedBreakpointId,
$styles,
$styleSources,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { createDefaultPages } from "@webstudio-is/project-build";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { Section } from "./backdrop-filter";
registerContainers();
@@ -39,10 +42,8 @@ $pages.set(
rootInstanceId: "box",
})
);
$awareness.set({
pageId: "homePageId",
instanceSelector: ["box"],
});
$selectedPageId.set("homePageId");
$selectedInstanceSelector.set(["box"]);
const blurBackdropFilter: StyleDecl = {
breakpointId: "base",
@@ -8,17 +8,20 @@ import {
theme,
} from "@webstudio-is/design-system";
import { createDefaultPages } from "@webstudio-is/project-build";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$instances,
$pages,
$selectedBreakpointId,
$styles,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { BackgroundContent as BackgroundContentPanel } from "./background-content";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { useComputedStyleDecl } from "../../shared/model";
import { setRepeatedStyleItem } from "../../shared/repeated-style";
import { type StyleValue } from "@webstudio-is/css-engine";
@@ -55,10 +58,8 @@ $styles.set(
new Map([[getStyleDeclKey(defaultBackgroundImage), defaultBackgroundImage]])
);
$awareness.set({
pageId: "homePageId",
instanceSelector: ["box"],
});
$selectedPageId.set("homePageId");
$selectedInstanceSelector.set(["box"]);
const BackgroundStory = ({ styleValue }: { styleValue: StyleValue }) => {
const backgroundImage = useComputedStyleDecl("background-image");
@@ -8,15 +8,18 @@ import {
theme,
} from "@webstudio-is/design-system";
import { registerContainers } from "~/shared/sync/sync-stores";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$instances,
$selectedBreakpointId,
$styles,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { Section as SectionComponent } from "./backgrounds";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
const backgroundImage: StyleDecl = {
breakpointId: "base",
@@ -53,10 +56,8 @@ $instances.set(
["box", { type: "instance", id: "box", component: "Box", children: [] }],
])
);
$awareness.set({
pageId: "",
instanceSelector: ["box"],
});
$selectedPageId.set("");
$selectedInstanceSelector.set(["box"]);
const SingleLayerVariant = () => {
useEffect(() => {
@@ -7,18 +7,21 @@ import {
theme,
} from "@webstudio-is/design-system";
import { getStyleDeclKey, type StyleDecl } from "@webstudio-is/sdk";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$instances,
$pages,
$selectedBreakpointId,
$styles,
$styleSources,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { createDefaultPages } from "@webstudio-is/project-build";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { Section } from "./borders";
registerContainers();
@@ -39,10 +42,8 @@ $pages.set(
rootInstanceId: "box",
})
);
$awareness.set({
pageId: "homePageId",
instanceSelector: ["box"],
});
$selectedPageId.set("homePageId");
$selectedInstanceSelector.set(["box"]);
const solidBorderTop: StyleDecl = {
breakpointId: "base",
@@ -7,18 +7,21 @@ import {
theme,
} from "@webstudio-is/design-system";
import { getStyleDeclKey, type StyleDecl } from "@webstudio-is/sdk";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$instances,
$pages,
$selectedBreakpointId,
$styles,
$styleSources,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { createDefaultPages } from "@webstudio-is/project-build";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { Section } from "./box-shadows";
registerContainers();
@@ -39,10 +42,8 @@ $pages.set(
rootInstanceId: "box",
})
);
$awareness.set({
pageId: "homePageId",
instanceSelector: ["box"],
});
$selectedPageId.set("homePageId");
$selectedInstanceSelector.set(["box"]);
const outerShadow: StyleDecl = {
breakpointId: "base",
@@ -7,18 +7,21 @@ import {
theme,
} from "@webstudio-is/design-system";
import { getStyleDeclKey, type StyleDecl } from "@webstudio-is/sdk";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$instances,
$pages,
$selectedBreakpointId,
$styles,
$styleSources,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { createDefaultPages } from "@webstudio-is/project-build";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { Section } from "./filter";
registerContainers();
@@ -39,10 +42,8 @@ $pages.set(
rootInstanceId: "box",
})
);
$awareness.set({
pageId: "homePageId",
instanceSelector: ["box"],
});
$selectedPageId.set("homePageId");
$selectedInstanceSelector.set(["box"]);
const blurFilterStyle: StyleDecl = {
breakpointId: "base",
@@ -32,7 +32,8 @@ import { createBatchUpdate } from "../../shared/use-style-data";
import { useReadonly } from "../../shared/readonly";
import { AlignSelfControl } from "../shared/align-self";
import { OrderControl } from "../shared/order";
import { $selectedInstancePath, selectInstance } from "~/shared/awareness";
import { $selectedInstancePath } from "~/shared/nano-states";
import { selectInstance } from "~/shared/nano-states";
export const properties = [
"flex-shrink",
@@ -42,7 +42,8 @@ import { useLocalValue } from "../../../settings-panel/shared";
import { AlignSelfControl, JustifySelfControl } from "../shared/align-self";
import { OrderControl } from "../shared/order";
import { useStore } from "@nanostores/react";
import { $selectedInstancePath, selectInstance } from "~/shared/awareness";
import { $selectedInstancePath } from "~/shared/nano-states";
import { selectInstance } from "~/shared/nano-states";
import { $isStylePanelGridVisible } from "~/builder/shared/nano-states";
import { ExternalLinkIcon } from "@webstudio-is/icons";
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, type JSX, type ReactNode } from "react";
import { useStore } from "@nanostores/react";
import { $selectedInstanceKey } from "~/shared/awareness";
import { $selectedInstanceKey } from "~/shared/nano-states";
import {
theme,
Box,
@@ -26,12 +26,10 @@ import {
import { useComputedStyleDecl } from "../../../shared/model";
import { createBatchUpdate } from "../../../shared/use-style-data";
import { useReadonly } from "../../../shared/readonly";
import {
$breakpoints,
$gridCellData,
$instances,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { $gridCellData } from "~/shared/nano-states";
import { $selectedInstanceSelector } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import { $instances } from "~/shared/sync/data-stores";
import { updateWebstudioData } from "~/shared/instance-utils";
import { DEFAULT_GRID_TRACK_COUNT } from "./constants";
@@ -26,7 +26,7 @@ import {
} from "@webstudio-is/css-data";
import { PlusIcon, MinusIcon } from "@webstudio-is/icons";
import { useStore } from "@nanostores/react";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance } from "~/shared/nano-states";
import { useReadonly } from "../../../shared/readonly";
import { useComputedStyleDecl } from "../../../shared/model";
import { CssValueInputContainer } from "../../../shared/css-value-input";
@@ -7,18 +7,21 @@ import {
theme,
} from "@webstudio-is/design-system";
import { getStyleDeclKey, type StyleDecl } from "@webstudio-is/sdk";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$instances,
$pages,
$selectedBreakpointId,
$styles,
$styleSources,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { createDefaultPages } from "@webstudio-is/project-build";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { Section } from "./outline";
registerContainers();
@@ -39,10 +42,8 @@ $pages.set(
rootInstanceId: "box",
})
);
$awareness.set({
pageId: "homePageId",
instanceSelector: ["box"],
});
$selectedPageId.set("homePageId");
$selectedInstanceSelector.set(["box"]);
const solidOutline: StyleDecl = {
breakpointId: "base",
@@ -4,15 +4,18 @@ import { getStyleDeclKey, StyleDecl } from "@webstudio-is/sdk";
import { createDefaultPages } from "@webstudio-is/project-build";
import { InsetControl } from "./inset-control";
import { registerContainers } from "~/shared/sync/sync-stores";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$pages,
$selectedBreakpointId,
$styles,
$styleSources,
$styleSourceSelections,
} from "~/shared/sync/data-stores";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { $awareness } from "~/shared/awareness";
const top: StyleDecl = {
breakpointId: "base",
@@ -88,10 +91,8 @@ $pages.set(
rootInstanceId: "box",
})
);
$awareness.set({
pageId: "homePageId",
instanceSelector: ["box"],
});
$selectedPageId.set("homePageId");
$selectedInstanceSelector.set(["box"]);
export const Inset = () => {
return (
@@ -1,16 +1,19 @@
import { Box, StorySection, theme } from "@webstudio-is/design-system";
import { getStyleDeclKey, StyleDecl } from "@webstudio-is/sdk";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$pages,
$selectedBreakpointId,
$styles,
$styleSources,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { Section } from "./transitions";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { createDefaultPages } from "@webstudio-is/project-build";
const transitionProperty: StyleDecl = {
@@ -53,10 +56,8 @@ $pages.set(
rootInstanceId: "box",
})
);
$awareness.set({
pageId: "homePageId",
instanceSelector: ["box"],
});
$selectedPageId.set("homePageId");
$selectedInstanceSelector.set(["box"]);
export const Transitions = () => (
<StorySection title="Transitions">
@@ -1,7 +1,7 @@
import { useStore } from "@nanostores/react";
import { map } from "nanostores";
import { useCallback } from "react";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance } from "~/shared/nano-states";
const instancesKv = map<Record<string, unknown>>({});
@@ -22,15 +22,17 @@ import {
} from "@webstudio-is/sdk";
import { rootComponent } from "@webstudio-is/sdk";
import {
$breakpoints,
$props,
$registeredComponentMetas,
$selectedBreakpoint,
$selectedInstanceStates,
$selectedOrLastStyleSourceSelector,
} from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$props,
$styles,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import {
getComputedStyleDecl,
getPresetStyleDeclKey,
@@ -40,7 +42,7 @@ import {
import {
$selectedInstancePathWithRoot,
type InstancePath,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import type { InstanceSelector } from "~/shared/tree-utils";
const $presetStyles = computed($registeredComponentMetas, (metas) => {
@@ -18,18 +18,21 @@ import {
} from "./repeated-style";
import { createComputedStyleDeclStore } from "./model";
import { parseCssFragment } from "./css-fragment";
import { $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$breakpoints,
$instances,
$selectedBreakpointId,
$styles,
$styleSources,
$styleSourceSelections,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { registerContainers } from "~/shared/sync/sync-stores";
import { setProperty } from "./use-style-data";
import type { ComputedStyleDecl } from "~/shared/style-object-model";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
setEnv("*");
registerContainers();
@@ -37,10 +40,8 @@ registerContainers();
beforeEach(() => {
$breakpoints.set(new Map([["base", { id: "base", label: "" }]]));
$selectedBreakpointId.set("base");
$awareness.set({
pageId: "",
instanceSelector: ["box"],
});
$selectedPageId.set("");
$selectedInstanceSelector.set(["box"]);
$instances.set(
new Map([
["box", { type: "instance", id: "box", component: "Box", children: [] }],
@@ -2,15 +2,20 @@ import { enableMapSet } from "immer";
import { describe, expect, test } from "vitest";
import { registerContainers } from "~/shared/sync/sync-stores";
import {
$breakpoints,
$instances,
$selectedBreakpointId,
$selectedStyleSources,
} from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import {
$instances,
$styleSourceSelections,
$styleSources,
$styles,
} from "~/shared/sync/data-stores";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { $awareness } from "~/shared/awareness";
import { deleteProperty, setProperty } from "./use-style-data";
import { getStyleDeclKey, type StyleDecl } from "@webstudio-is/sdk";
@@ -31,10 +36,8 @@ const setupSelection = (styleSource: {
],
])
);
$awareness.set({
pageId: "",
instanceSelector: ["body"],
});
$selectedPageId.set("");
$selectedInstanceSelector.set(["body"]);
$breakpoints.set(new Map([["base", { id: "base", label: "Base" }]]));
$selectedBreakpointId.set("base");
$styleSources.set(new Map([[styleSource.id, styleSource]]));
@@ -5,13 +5,15 @@ import {
$selectedBreakpoint,
$selectedOrLastStyleSourceSelector,
$selectedStyleSource,
} from "~/shared/nano-states";
import {
$styleSourceSelections,
$styleSources,
$styles,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { $ephemeralStyles } from "~/canvas/stores";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance } from "~/shared/nano-states";
import { isStyleSourceLocked } from "~/shared/style-source-utils";
type StyleUpdate =
@@ -29,7 +29,7 @@ import {
} from "~/shared/nano-states";
import { isStyleSourceLocked } from "~/shared/style-source-utils";
import { ReadonlyProvider } from "./shared/readonly";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance } from "~/shared/nano-states";
import { CollapsibleProvider } from "~/builder/shared/collapsible-section";
import {
$settings,
@@ -1,15 +1,18 @@
import { enableMapSet } from "immer";
import { expect, test, describe } from "vitest";
import { registerContainers } from "~/shared/sync/sync-stores";
import { $selectedStyleSources } from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import {
$instances,
$selectedStyleSources,
$styleSourceSelections,
$styleSources,
$styles,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { addStyleSourceToInstance, __testing__ } from "./style-source-section";
import { $awareness } from "~/shared/awareness";
import {
$selectedPageId,
$selectedInstanceSelector,
} from "~/shared/nano-states";
const { duplicateStyleSource, getComponentStates } = __testing__;
@@ -154,10 +157,8 @@ test("add style source to instance", () => {
],
])
);
$awareness.set({
pageId: "",
instanceSelector: ["body"],
});
$selectedPageId.set("");
$selectedInstanceSelector.set(["body"]);
$styleSources.set(new Map([["local1", { id: "local1", type: "local" }]]));
$styleSourceSelections.set(new Map());
$selectedStyleSources.set(new Map());
@@ -193,10 +194,8 @@ test("duplicate locked style source creates an unlocked copy", () => {
],
])
);
$awareness.set({
pageId: "",
instanceSelector: ["body"],
});
$selectedPageId.set("");
$selectedInstanceSelector.set(["body"]);
$styleSources.set(
new Map([
[
@@ -14,9 +14,9 @@ import {
getStyleDeclKey,
} from "@webstudio-is/sdk";
import { type ItemSource, StyleSourceInput } from "./style-source";
import { type RenameStyleSourceError } from "~/shared/style-source-utils";
import {
renameStyleSource,
type RenameStyleSourceError,
deleteStyleSource,
DeleteStyleSourceDialog,
setStyleSourceLocked,
@@ -28,15 +28,17 @@ import {
$selectedOrLastStyleSourceSelector,
$selectedStyleSources,
$selectedStyleState,
} from "~/shared/nano-states";
import {
$styleSourceSelections,
$styleSources,
$styles,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { removeByMutable } from "~/shared/array-utils";
import { cloneStyles } from "~/shared/tree-utils";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { subscribe } from "~/shared/pubsub";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance } from "~/shared/nano-states";
import { $instanceTags } from "./shared/model";
// Declare command for this module
@@ -9,7 +9,7 @@ import {
} from "@webstudio-is/design-system";
import { OfflineIcon } from "@webstudio-is/icons";
import { useEffect } from "react";
import { $queueStatus } from "~/shared/sync/project-queue";
import { $syncStatus } from "@webstudio-is/sync-client";
const $isOnline = atom(false);
@@ -24,13 +24,13 @@ const subscribeIsOnline = () => {
};
export const SyncStatus = () => {
const statusObject = useStore($queueStatus);
const statusObject = useStore($syncStatus);
const isOnline = useStore($isOnline);
useEffect(subscribeIsOnline, []);
if (
statusObject.status === "idle" ||
statusObject.status === "running" ||
statusObject.status === "syncing" ||
statusObject.status === "recovering"
) {
return null;
@@ -2,13 +2,13 @@ import { useStore } from "@nanostores/react";
import { styled } from "@webstudio-is/design-system";
import {
$instances,
$modifierKeys,
$textEditingInstanceSelector,
$textEditorContextMenu,
$textEditorContextMenuCommand,
findTemplates,
} from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { applyScale } from "./outline";
import { $scale } from "~/builder/shared/nano-states";
import { TemplatesMenu } from "./outline/block-instance-outline";
@@ -1,7 +1,7 @@
import { useStore } from "@nanostores/react";
import { $instanceContextMenu } from "~/shared/nano-states";
import { useEffect, useRef } from "react";
import { selectInstance } from "~/shared/awareness";
import { selectInstance } from "~/shared/nano-states";
import {
ContextMenu,
ContextMenuTrigger,
@@ -2,11 +2,11 @@ import { useStore } from "@nanostores/react";
import { css } from "@webstudio-is/design-system";
import { PlacementIndicator } from "@webstudio-is/design-system";
import {
$instances,
$isPreviewMode,
$dragAndDropState,
$canvasToolsVisible,
} from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import {
CollaborativeInstanceOutline,
HoveredInstanceOutline,
@@ -26,6 +26,7 @@ import {
import { BlockChildHoveredInstanceOutline } from "./outline/block-instance-outline";
import { TextEditorContextMenu } from "./block-editor-context-menu";
import { CanvasInstanceContextMenu } from "./canvas-instance-context-menu";
import { CollaborativeCursors } from "./collaborative-cursors";
const containerStyle = css({
position: "absolute",
@@ -86,6 +87,7 @@ export const CanvasTools = () => {
<>
{isPreviewMode === false && (
<>
<CollaborativeCursors />
{isStylePanelGridVisible && <GridGuides />}
<SelectedInstanceOutline />
<HoveredInstanceOutline />
@@ -0,0 +1,160 @@
import { describe, expect, test } from "vitest";
import { __testing__ } from "./collaborative-cursors";
const {
shouldSkipCollaborator,
hasCursorPosition,
computeCursorCoordinates,
isWithinLayer,
toRenderableCollaboratorCursor,
} = __testing__;
describe("collaborative-cursors helpers", () => {
test("shouldSkipCollaborator returns true only for different known pages", () => {
expect(
shouldSkipCollaborator({
currentPageId: "page-a",
collaboratorPageId: "page-b",
})
).toBe(true);
expect(
shouldSkipCollaborator({
currentPageId: "page-a",
collaboratorPageId: "page-a",
})
).toBe(false);
expect(
shouldSkipCollaborator({
currentPageId: "page-a",
collaboratorPageId: undefined,
})
).toBe(false);
});
test("hasCursorPosition returns true when pointer position is set", () => {
expect(hasCursorPosition(undefined)).toBe(false);
expect(hasCursorPosition({ x: 10, y: 20, xRatio: 0.1, yRatio: 0.2 })).toBe(
true
);
});
test("computeCursorCoordinates maps normalized coordinates using canvas rect", () => {
const layerRect = { left: 0, top: 0, width: 1200, height: 800 };
const canvasRect = { left: 100, top: 200, width: 800, height: 600 };
const result = computeCursorCoordinates({
pointerPosition: { x: 0, y: 0, xRatio: 0.5, yRatio: 0.25 },
layerRect,
canvasRect,
});
expect(result).toEqual({ x: 500, y: 350 });
});
test("computeCursorCoordinates falls back to layer rect when canvas rect is missing", () => {
const layerRect = { left: 0, top: 0, width: 1000, height: 800 };
const result = computeCursorCoordinates({
pointerPosition: { x: 0, y: 0, xRatio: 0.5, yRatio: 0.25 },
layerRect,
canvasRect: undefined,
});
expect(result).toEqual({ x: 500, y: 200 });
});
test("computeCursorCoordinates uses ratios against layer rect when canvas rect is missing", () => {
const layerRect = { left: 50, top: 100, width: 1000, height: 800 };
const result = computeCursorCoordinates({
pointerPosition: { x: 350, y: 500, xRatio: 0.3, yRatio: 0.5 },
layerRect,
canvasRect: undefined,
});
expect(result).toEqual({ x: 300, y: 400 });
});
test("isWithinLayer bounds-checks computed coordinates", () => {
const layerRect = { left: 0, top: 0, width: 500, height: 400 };
expect(isWithinLayer({ x: 10, y: 10, layerRect })).toBe(true);
expect(isWithinLayer({ x: -1, y: 10, layerRect })).toBe(false);
expect(isWithinLayer({ x: 10, y: 401, layerRect })).toBe(false);
expect(isWithinLayer({ x: 999, y: 999, layerRect: undefined })).toBe(true);
});
test("toRenderableCollaboratorCursor returns coordinates only for visible collaborator cursor", () => {
const layerRect = { left: 0, top: 0, width: 1200, height: 800 };
const canvasRect = { left: 100, top: 200, width: 800, height: 600 };
expect(
toRenderableCollaboratorCursor({
currentPageId: "page-a",
collaboratorPageId: "page-b",
pointerPosition: { x: 10, y: 20, xRatio: 0, yRatio: 0 },
layerRect,
canvasRect,
})
).toBeUndefined();
expect(
toRenderableCollaboratorCursor({
currentPageId: "page-a",
collaboratorPageId: "page-a",
pointerPosition: undefined,
layerRect,
canvasRect,
})
).toBeUndefined();
expect(
toRenderableCollaboratorCursor({
currentPageId: "page-a",
collaboratorPageId: "page-a",
pointerPosition: { x: 0, y: 0, xRatio: 0.5, yRatio: 0.25 },
layerRect,
canvasRect,
})
).toEqual({ x: 500, y: 350 });
});
test("toRenderableCollaboratorCursor recalculates position when receiver canvas changes", () => {
const layerRect = { left: 0, top: 0, width: 1400, height: 1000 };
const normalized = { xRatio: 0.25, yRatio: 0.75 };
const firstReceiverCanvasRect = {
left: 100,
top: 100,
width: 800,
height: 600,
};
const secondReceiverCanvasRect = {
left: 200,
top: 150,
width: 1200,
height: 900,
};
const firstRender = toRenderableCollaboratorCursor({
currentPageId: "page-a",
collaboratorPageId: "page-a",
pointerPosition: { x: 0, y: 0, ...normalized },
layerRect,
canvasRect: firstReceiverCanvasRect,
});
const secondRender = toRenderableCollaboratorCursor({
currentPageId: "page-a",
collaboratorPageId: "page-a",
pointerPosition: { x: 0, y: 0, ...normalized },
layerRect,
canvasRect: secondReceiverCanvasRect,
});
expect(firstRender).toEqual({ x: 300, y: 550 });
expect(secondRender).toEqual({ x: 500, y: 825 });
});
});
@@ -0,0 +1,245 @@
import { useStore } from "@nanostores/react";
import { useEffect, useMemo, useState } from "react";
import { css, type Rect } from "@webstudio-is/design-system";
import {
$collaborators,
assignCollaboratorColors,
type CollaboratorInfo,
} from "@webstudio-is/sync-client";
import {
$collaborativeInstanceSelector,
$selectedPageId,
} from "~/shared/nano-states";
import { $canvasRect } from "~/builder/shared/nano-states";
const layerStyle = css({
position: "fixed",
inset: 0,
pointerEvents: "none",
});
const cursorStyle = css({
position: "absolute",
top: 0,
left: 0,
transformOrigin: "top left",
transition: "transform 300ms ease-out",
willChange: "transform",
});
const shouldSkipCollaborator = ({
currentPageId,
collaboratorPageId,
}: {
currentPageId: string | undefined;
collaboratorPageId: string | undefined;
}) => {
return (
currentPageId !== undefined &&
collaboratorPageId !== undefined &&
collaboratorPageId !== currentPageId
);
};
const hasCursorPosition = (
pointerPosition: CollaboratorInfo["pointerPosition"]
) => {
return pointerPosition !== undefined;
};
const computeCursorCoordinates = ({
pointerPosition,
layerRect,
canvasRect,
}: {
pointerPosition: CollaboratorInfo["pointerPosition"];
layerRect: Rect | undefined;
canvasRect: Rect | undefined;
}) => {
if (pointerPosition === undefined || layerRect === undefined) {
return { x: 0, y: 0 };
}
if (canvasRect !== undefined) {
return {
x:
canvasRect.left -
layerRect.left +
pointerPosition.xRatio * canvasRect.width,
y:
canvasRect.top -
layerRect.top +
pointerPosition.yRatio * canvasRect.height,
};
}
return {
x: pointerPosition.xRatio * layerRect.width,
y: pointerPosition.yRatio * layerRect.height,
};
};
const isWithinLayer = ({
x,
y,
layerRect,
}: {
x: number;
y: number;
layerRect: Rect | undefined;
}) => {
if (layerRect === undefined) {
return true;
}
return x >= 0 && x < layerRect.width && y >= 0 && y < layerRect.height;
};
const toRenderableCollaboratorCursor = ({
currentPageId,
collaboratorPageId,
pointerPosition,
layerRect,
canvasRect,
}: {
currentPageId: string | undefined;
collaboratorPageId: string | undefined;
pointerPosition: CollaboratorInfo["pointerPosition"];
layerRect: Rect | undefined;
canvasRect: Rect | undefined;
}) => {
if (shouldSkipCollaborator({ currentPageId, collaboratorPageId })) {
return;
}
if (!hasCursorPosition(pointerPosition)) {
return;
}
const coordinates = computeCursorCoordinates({
pointerPosition,
layerRect,
canvasRect,
});
if (!isWithinLayer({ ...coordinates, layerRect })) {
return;
}
return coordinates;
};
const areInstanceSelectorsEqual = (
left: readonly string[] | undefined,
right: readonly string[] | undefined
) => {
if (left === right) {
return true;
}
if (
left === undefined ||
right === undefined ||
left.length !== right.length
) {
return false;
}
return left.every((id, index) => id === right[index]);
};
export const CollaborativeCursors = () => {
const collaborators = useStore($collaborators);
const canvasRect = useStore($canvasRect);
const currentPageId = useStore($selectedPageId);
const [layerElement, setLayerElement] = useState<
HTMLDivElement | null | undefined
>(undefined);
const layerRect = layerElement?.getBoundingClientRect();
const collaboratorColors = useMemo(
() => assignCollaboratorColors(collaborators.keys()),
[collaborators]
);
useEffect(() => {
const collaborativeSelection = [...collaborators.values()].find(
(collaborator) => {
if (
shouldSkipCollaborator({
currentPageId,
collaboratorPageId: collaborator.pageId,
})
) {
return false;
}
return (
collaborator.selectedInstanceIds !== undefined &&
collaborator.selectedInstanceIds.length > 0
);
}
)?.selectedInstanceIds;
const currentCollaborativeSelection = $collaborativeInstanceSelector.get();
if (
areInstanceSelectorsEqual(
currentCollaborativeSelection,
collaborativeSelection
)
) {
return;
}
$collaborativeInstanceSelector.set(collaborativeSelection);
}, [collaborators, currentPageId]);
return (
<div className={layerStyle()} ref={setLayerElement}>
{[...collaborators.entries()].map(([clientId, collaborator]) => {
const coordinates = toRenderableCollaboratorCursor({
currentPageId,
collaboratorPageId: collaborator.pageId,
pointerPosition: collaborator.pointerPosition,
layerRect,
canvasRect,
});
if (coordinates === undefined) {
return;
}
const { x, y } = coordinates;
const color = collaboratorColors.get(clientId);
return (
<div
key={clientId}
className={cursorStyle()}
style={{
color,
transform: `translate3d(${x}px, ${y}px, 0)`,
}}
>
<svg
width="14"
height="20"
viewBox="0 0 14 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 1.5L1 17.7L4.8 13.4L8 19L11 17.4L7.8 11.8L13 11.8L1 1.5Z"
fill="currentColor"
stroke="white"
strokeWidth="1"
/>
</svg>
</div>
);
})}
</div>
);
};
export const __testing__ = {
shouldSkipCollaborator,
hasCursorPosition,
computeCursorCoordinates,
isWithinLayer,
toRenderableCollaboratorCursor,
};
@@ -23,13 +23,13 @@ import {
$blockChildOutline,
$hoveredInstanceOutline,
$hoveredInstanceSelector,
$instances,
$isContentMode,
$modifierKeys,
findBlockSelector,
findTemplates,
type BlockChildOutline,
} from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $clampingRect, $scale } from "~/builder/shared/nano-states";
import type { InstanceSelector } from "~/shared/tree-utils";
import {
@@ -38,7 +38,7 @@ import {
} from "~/shared/instance-utils";
import { skipInertHandlersAttribute } from "~/builder/shared/inert-handlers";
import { useEffectEvent } from "~/shared/hook-utils/effect-event";
import { getInstancePath } from "~/shared/awareness";
import { getInstancePath } from "~/shared/nano-states";
import { insertTemplateAt } from "./block-utils";
import { Outline } from "./outline";
import { applyScale } from "../apply-scale";
@@ -1,7 +1,7 @@
import type { Instance, Instances } from "@webstudio-is/sdk";
import { blockTemplateComponent } from "@webstudio-is/sdk";
import { shallowEqual } from "shallow-equal";
import { selectInstance } from "~/shared/awareness";
import { selectInstance } from "~/shared/nano-states";
import { builderApi } from "~/shared/builder-api";
import { findAvailableVariables } from "~/shared/data-variables";
import {
@@ -14,13 +14,13 @@ import {
detectFragmentTokenConflicts,
} from "~/shared/instance-utils";
import {
$instances,
$project,
$registeredComponentMetas,
$textEditingInstanceSelector,
findBlockChildSelector,
findBlockSelector,
} from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $project } from "~/shared/sync/data-stores";
import type { DroppableTarget, InstanceSelector } from "~/shared/tree-utils";
const getInsertionIndex = (
@@ -1,16 +1,51 @@
import { useStore } from "@nanostores/react";
import { $collaborativeInstanceRect } from "~/shared/nano-states";
import { useMemo } from "react";
import {
$collaborators,
assignCollaboratorColors,
} from "@webstudio-is/sync-client";
import {
$collaborativeInstanceRect,
$collaborativeInstanceSelector,
$selectedPageId,
} from "~/shared/nano-states";
import { Outline } from "./outline";
import { applyScale } from "../apply-scale";
import { $scale, $clampingRect } from "~/builder/shared/nano-states";
import { $ephemeralStyles } from "~/canvas/stores";
const areSelectorsEqual = (
left: readonly string[] | undefined,
right: readonly string[] | undefined
) => {
if (left === right) {
return true;
}
if (
left === undefined ||
right === undefined ||
left.length !== right.length
) {
return false;
}
return left.every((id, index) => id === right[index]);
};
// Outline of an instance that is being edited by AI or a human collaborator.
export const CollaborativeInstanceOutline = () => {
const scale = useStore($scale);
const instanceRect = useStore($collaborativeInstanceRect);
const collaborativeInstanceSelector = useStore(
$collaborativeInstanceSelector
);
const collaborators = useStore($collaborators);
const currentPageId = useStore($selectedPageId);
const ephemeralStyles = useStore($ephemeralStyles);
const clampingRect = useStore($clampingRect);
const collaboratorColors = useMemo(
() => assignCollaboratorColors(collaborators.keys()),
[collaborators]
);
if (
instanceRect === undefined ||
@@ -22,11 +57,33 @@ export const CollaborativeInstanceOutline = () => {
const rect = applyScale(instanceRect, scale);
const selectedCollaborator = [...collaborators.entries()].find(
([, collaborator]) => {
if (
currentPageId !== undefined &&
collaborator.pageId !== undefined &&
collaborator.pageId !== currentPageId
) {
return false;
}
return areSelectorsEqual(
collaborator.selectedInstanceIds,
collaborativeInstanceSelector
);
}
);
const color =
selectedCollaborator === undefined
? undefined
: collaboratorColors.get(selectedCollaborator[0]);
return (
<Outline
variant="collaboration"
variant="default"
rect={rect}
clampingRect={clampingRect}
color={color}
></Outline>
);
};
@@ -4,10 +4,10 @@ import {
$blockChildOutline,
$hoveredInstanceOutlineAndInstance,
$hoveredInstanceSelector,
$instances,
$isContentMode,
$textEditingInstanceSelector,
} from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $clampingRect, $scale } from "~/builder/shared/nano-states";
import { findClosestSlot } from "~/shared/instance-utils";
import { isDescendantOrSelf } from "~/shared/tree-utils";
@@ -92,6 +92,7 @@ type OutlineProps = {
rect: Rect;
clampingRect: Rect;
variant?: "default" | "collaboration" | "slot";
color?: string;
};
export const Outline = ({
@@ -99,6 +100,7 @@ export const Outline = ({
rect,
clampingRect,
variant,
color,
}: OutlineProps) => {
const outlineRect = {
top: Math.max(rect.top, clampingRect.top),
@@ -112,6 +114,10 @@ export const Outline = ({
Math.max(rect.left, clampingRect.left),
};
const dynamicStyle = useDynamicStyle(outlineRect);
const style =
color === undefined
? dynamicStyle
: { ...dynamicStyle, borderColor: color };
if (outlineRect.width <= 0 || outlineRect.height <= 0) {
return;
}
@@ -130,7 +136,7 @@ export const Outline = ({
{propertyStyle}
<div
className={`${baseStyle()} ${baseOutlineStyle({ variant, isLeftClamped, isRightClamped, isBottomClamped, isTopClamped })}`}
style={dynamicStyle}
style={style}
>
{children}
</div>
@@ -1,10 +1,8 @@
import { useStore } from "@nanostores/react";
import {
$instances,
$selectedInstanceOutlineAndInstance,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { $selectedInstanceOutlineAndInstance } from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
import { $textEditingInstanceSelector } from "~/shared/nano-states";
import { $selectedInstanceSelector } from "~/shared/nano-states";
import { isDescendantOrSelf } from "~/shared/tree-utils";
import { Outline } from "./outline";
import { applyScale } from "../apply-scale";
@@ -9,11 +9,8 @@ import {
import { useEffect, useRef } from "react";
import { $canvasWidth } from "~/builder/shared/nano-states";
import { minCanvasWidth } from "~/shared/breakpoints";
import {
$breakpoints,
$isResizingCanvas,
$selectedBreakpointId,
} from "~/shared/nano-states";
import { $isResizingCanvas, $selectedBreakpointId } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
const handlesContainerStyle = css({
position: "absolute",
@@ -8,7 +8,7 @@ import {
} from "~/builder/shared/nano-states";
import { $textEditingInstanceSelector } from "~/shared/nano-states";
import { CanvasTools } from "./canvas-tools";
import { selectInstance } from "~/shared/awareness";
import { selectInstance } from "~/shared/nano-states";
import { ResizeHandles } from "./canvas-tools/resize-handles";
import { MediaBadge } from "./canvas-tools/media-badge";
import { useSetCanvasWidth } from "~/builder/shared/calc-canvas-width";
+1 -1
View File
@@ -31,7 +31,7 @@ import {
$selectedInstance,
$selectedInstanceKey,
$selectedPage,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import { InstanceIcon, getInstanceLabel } from "./shared/instance-label";
const InstanceInfo = ({ instance }: { instance: Instance }) => {
@@ -51,22 +51,18 @@ import {
} from "@webstudio-is/icons";
import { hyphenateProperty } from "@webstudio-is/css-engine";
import {
$assets,
$authPermit,
$editingPageId,
$instances,
$pages,
$permissions,
$props,
$styles,
$styleSourceSelections,
} from "~/shared/nano-states";
import { $assets } from "~/shared/sync/data-stores";
import { $styleSourceSelections } from "~/shared/sync/data-stores";
import { $openProjectSettings } from "~/shared/nano-states/project-settings";
import {
$awareness,
findAwarenessByInstanceId,
selectPage,
} from "~/shared/awareness";
import { $styles } from "~/shared/sync/data-stores";
import { $selectedInstanceSelector } from "~/shared/nano-states";
import { selectPage } from "~/shared/nano-states";
import { findPageAndSelectorByInstanceId } from "~/shared/instance-utils";
import { $selectedPageId } from "~/shared/nano-states";
import { updateWebstudioData } from "~/shared/instance-utils";
import { deleteAssets, replaceAsset } from "~/builder/shared/assets";
import { validateFiles } from "~/builder/shared/assets/asset-upload";
@@ -74,6 +70,7 @@ import {
$activeInspectorPanel,
setActiveSidebarPanel,
} from "~/builder/shared/nano-states";
import { $instances, $pages, $props } from "~/shared/sync/data-stores";
import {
formatAssetName,
parseAssetName,
@@ -266,12 +263,14 @@ const AssetUsagesList = ({ usages }: { usages: AssetUsage[] }) => {
if (!prop || !pages) {
return;
}
const awareness = findAwarenessByInstanceId(
pages,
instances,
prop.instanceId
);
$awareness.set(awareness);
const { pageId, instanceSelector } =
findPageAndSelectorByInstanceId(
pages,
instances,
prop.instanceId
);
$selectedPageId.set(pageId);
$selectedInstanceSelector.set(instanceSelector);
setActiveSidebarPanel("auto");
$activeInspectorPanel.set("settings");
}}
@@ -311,12 +310,14 @@ const AssetUsagesList = ({ usages }: { usages: AssetUsage[] }) => {
if (!styleInstanceId || !pages) {
return;
}
const awareness = findAwarenessByInstanceId(
pages,
instances,
styleInstanceId
);
$awareness.set(awareness);
const { pageId, instanceSelector } =
findPageAndSelectorByInstanceId(
pages,
instances,
styleInstanceId
);
$selectedPageId.set(pageId);
$selectedInstanceSelector.set(instanceSelector);
setActiveSidebarPanel("auto");
$activeInspectorPanel.set("style");
}}
@@ -7,7 +7,7 @@ import {
VIDEO_EXTENSIONS,
} from "@webstudio-is/sdk";
import type { Asset, AllowedFileExtension } from "@webstudio-is/sdk";
import { $assets } from "~/shared/nano-states";
import { $assets } from "~/shared/sync/data-stores";
import { useEffect } from "react";
// Create mock assets for every file type
@@ -15,7 +15,7 @@ import {
Box,
} from "@webstudio-is/design-system";
import type { Asset } from "@webstudio-is/sdk";
import { $assets, $pages, $props, $styles } from "~/shared/nano-states";
import { $assets, $pages, $props, $styles } from "~/shared/sync/data-stores";
import { deleteAssets } from "~/builder/shared/assets";
import { formatAssetName } from "~/builder/shared/assets/asset-utils";
import { calculateUsagesByAssetId } from "./asset-info";
@@ -1,5 +1,5 @@
import type { Asset } from "@webstudio-is/sdk";
import { $assets } from "~/shared/nano-states";
import { $assets } from "~/shared/sync/data-stores";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { onNextTransactionComplete } from "~/shared/sync/project-queue";
import { invalidateAssets } from "~/shared/resources";
@@ -1,6 +1,6 @@
import type { Asset } from "@webstudio-is/sdk";
import { toast } from "@webstudio-is/design-system";
import { $assets, $pages, $props, $styles } from "~/shared/nano-states";
import { $assets, $pages, $props, $styles } from "~/shared/sync/data-stores";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { onNextTransactionComplete } from "~/shared/sync/project-queue";
import { invalidateAssets } from "~/shared/resources";
@@ -9,12 +9,12 @@ import { restAssetsUploadPath, restAssetsPath } from "~/shared/router-utils";
import { fetch } from "~/shared/fetch.client";
import type { AssetActionResponse } from "~/builder/shared/assets";
import {
$assets,
$authToken,
$project,
$uploadingFilesDataStore,
type UploadingFileData,
} from "~/shared/nano-states";
import { $assets } from "~/shared/sync/data-stores";
import { $project } from "~/shared/sync/data-stores";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { onNextTransactionComplete } from "~/shared/sync/project-queue";
import { invalidateAssets } from "~/shared/resources";
@@ -2,7 +2,8 @@ import { useMemo } from "react";
import { computed } from "nanostores";
import { useStore } from "@nanostores/react";
import type { Asset } from "@webstudio-is/sdk";
import { $assets, $uploadingFilesDataStore } from "~/shared/nano-states";
import { $assets } from "~/shared/sync/data-stores";
import { $uploadingFilesDataStore } from "~/shared/nano-states";
import type {
AssetContainer,
UploadedAssetContainer,
@@ -7,11 +7,8 @@ import {
minCanvasWidth,
} from "~/shared/breakpoints-utils";
import { $workspaceRect, $canvasWidth } from "~/builder/shared/nano-states";
import {
$breakpoints,
$isPreviewMode,
$selectedBreakpoint,
} from "~/shared/nano-states";
import { $isPreviewMode, $selectedBreakpoint } from "~/shared/nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
const defaultWidth = 320;
+3 -2
View File
@@ -10,8 +10,8 @@ import {
$isDesignMode,
$isPreviewMode,
toggleBuilderMode,
$project,
} from "~/shared/nano-states";
import { $project } from "~/shared/sync/data-stores";
// Declare command for type safety
declare module "~/shared/pubsub" {
@@ -40,7 +40,8 @@ import {
setActiveSidebarPanel,
toggleActiveSidebarPanel,
} from "./nano-states";
import { $selectedInstancePath, selectInstance } from "~/shared/awareness";
import { $selectedInstancePath } from "~/shared/nano-states";
import { selectInstance } from "~/shared/nano-states";
import { openCommandPanel } from "../features/command-panel";
import { showWrapComponentsList } from "../features/command-panel/groups/wrap-group";
import { showConvertComponentsList } from "../features/command-panel/groups/convert-group";
@@ -16,13 +16,13 @@ import {
} from "@webstudio-is/design-system";
import type { DataSource, Instance } from "@webstudio-is/sdk";
import { ROOT_INSTANCE_ID } from "@webstudio-is/sdk";
import { $pages } from "~/shared/sync/data-stores";
import {
$pages,
$instances,
$props,
$dataSources,
$resources,
} from "~/shared/nano-states";
} from "~/shared/sync/data-stores";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { findVariableUsagesByInstance } from "~/shared/data-variables";
@@ -17,11 +17,11 @@ import {
$selectedInstancePath,
$selectedPage,
getInstanceKey,
} from "~/shared/awareness";
} from "~/shared/nano-states";
import { canUnwrapInstance } from "~/shared/instance-utils";
import { $propValuesByInstanceSelector } from "~/shared/nano-states";
import { ROOT_INSTANCE_ID } from "@webstudio-is/sdk";
import type { InstancePath } from "~/shared/awareness";
import type { InstancePath } from "~/shared/nano-states";
const getMenuPermissions = (instancePath: InstancePath | undefined) => {
const instanceId = instancePath?.[0]?.instance.id;
@@ -16,12 +16,11 @@ import {
} from "@webstudio-is/design-system";
import type { Instance, StyleSource } from "@webstudio-is/sdk";
import {
$styleSources,
$styleSourceSelections,
$styles,
$selectedStyleSources,
$selectedStyleState,
} from "~/shared/nano-states";
import { $styleSources } from "~/shared/sync/data-stores";
import { $styleSourceSelections, $styles } from "~/shared/sync/data-stores";
import {
deleteStyleSourceMutable,
findUnusedTokens,
@@ -32,10 +31,7 @@ import {
type RenameStyleSourceError,
} from "~/shared/style-source-utils";
import { serverSyncStore } from "~/shared/sync/sync-stores";
import { $selectedInstance } from "~/shared/awareness";
// Re-export the type for convenience
export type { RenameStyleSourceError };
import { $selectedInstance } from "~/shared/nano-states";
const $isDeleteUnusedTokensDialogOpen = atom(false);
@@ -15,7 +15,7 @@ import {
ShieldIcon,
WebstudioIcon,
} from "@webstudio-is/icons";
import { $queueStatus } from "~/shared/sync/project-queue";
import { $syncStatus } from "@webstudio-is/sync-client";
import { $authPermit } from "~/shared/nano-states";
import { SyncStatus } from "~/builder/features/sync-status";
import { ViewMode } from "~/builder/features/view-mode";
@@ -75,7 +75,7 @@ const SafeModeIndicator = () => (
);
export const TopbarLayouts = () => {
$queueStatus.set({ status: "failed" });
$syncStatus.set({ status: "failed" });
$authPermit.set("view");
return (
<StorySection title="Topbar Layouts">
+1 -1
View File
@@ -21,7 +21,7 @@ import { AddressBarPopover } from "~/builder/features/address-bar";
import { toggleActiveSidebarPanel } from "~/builder/shared/nano-states";
import type { ReactNode } from "react";
import { CloneButton } from "~/builder/features/clone";
import { $selectedPage } from "~/shared/awareness";
import { $selectedPage } from "~/shared/nano-states";
import { BuilderModeDropDown } from "~/builder/features/builder-mode";
import { SafeModeButton } from "~/builder/features/safe-mode";
import { NotificationPopover } from "~/shared/notifications/notification-popover";
@@ -6,7 +6,7 @@ import {
tokenizePathnamePattern,
validatePathnamePattern,
} from "./url-pattern";
import { VALID_URLPATTERN_PATHS } from "@webstudio-is/sdk/router-paths.test";
import { VALID_URLPATTERN_PATHS } from "@webstudio-is/sdk/router-path-test-data";
/**
* These tests use the shared test data from @webstudio-is/sdk to ensure
+6 -5
View File
@@ -37,9 +37,6 @@ import {
WebstudioComponentPreview,
} from "./features/webstudio-component";
import {
$assets,
$pages,
$instances,
registerComponentLibrary,
$registeredComponents,
subscribeComponentHooks,
@@ -48,8 +45,9 @@ import {
$isContentMode,
subscribeModifierKeys,
assetBaseUrl,
$breakpoints,
} from "~/shared/nano-states";
import { $assets } from "~/shared/sync/data-stores";
import { $pages, $instances, $breakpoints } from "~/shared/sync/data-stores";
import { useDragAndDrop } from "./shared/use-drag-drop";
import {
initCopyPaste,
@@ -69,13 +67,14 @@ import { subscribeFontLoadingDone } from "./shared/font-weight-support";
import { subscribeSelected } from "./instance-selected";
import { subscribeGridGuidesOnSelected } from "./grid-guide-utils";
import { subscribeScrollNewInstanceIntoView } from "./shared/scroll-new-instance-into-view";
import { $selectedPage } from "~/shared/awareness";
import { $selectedPage } from "~/shared/nano-states";
import { createInstanceElement } from "./elements";
import { subscribeScrollbarSize } from "./scrollbar-width";
import { compareMedia } from "@webstudio-is/css-engine";
import { builderApi } from "~/shared/builder-api";
import { useDebounceEffect } from "@webstudio-is/design-system";
import { subscribeInstanceContextMenu } from "./instance-context-menu";
import { startPointerTracking } from "~/shared/awareness";
registerContainers();
@@ -313,6 +312,8 @@ export const Canvas = () => {
useEffect(subscribeInflator, []);
useEffect(() => startPointerTracking(), []);
useHashLinkSync();
useEffect(subscribeInterceptedEvents, []);
@@ -5,7 +5,8 @@ import {
type WebstudioComponentSystemProps,
} from "@webstudio-is/react-sdk";
import * as React from "react";
import { $isDesignMode, $selectedInstanceSelector } from "~/shared/nano-states";
import { $isDesignMode } from "~/shared/nano-states";
import { $selectedInstanceSelector } from "~/shared/nano-states";
export const BlockTemplate = React.forwardRef<
HTMLDivElement,
@@ -8,12 +8,9 @@ import {
} from "@webstudio-is/react-sdk";
import * as React from "react";
import {
$instances,
$isDesignMode,
$isPreviewMode,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import { $isDesignMode, $isPreviewMode } from "~/shared/nano-states";
import { $selectedInstanceSelector } from "~/shared/nano-states";
import { $instances } from "~/shared/sync/data-stores";
export const Block = React.forwardRef<
HTMLDivElement,
Loaded 100 of 231 files, more files were not shown because too many files have changed in this diff. Show more