@webstudio-is/react-sdk 0.74.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/lib/cjs/components/component-meta.js +1 -0
  2. package/lib/cjs/context.js +5 -1
  3. package/lib/cjs/css/global-rules.js +1 -1
  4. package/lib/cjs/css/normalize.js +10 -13
  5. package/lib/cjs/embed-template.js +68 -3
  6. package/lib/cjs/expression.js +191 -0
  7. package/lib/cjs/index.js +7 -1
  8. package/lib/cjs/props.js +28 -10
  9. package/lib/cjs/tree/create-elements-tree.js +14 -1
  10. package/lib/cjs/tree/root.js +55 -0
  11. package/lib/cjs/tree/webstudio-component.js +9 -2
  12. package/lib/components/component-meta.js +1 -0
  13. package/lib/context.js +5 -1
  14. package/lib/css/global-rules.js +1 -1
  15. package/lib/css/normalize.js +10 -13
  16. package/lib/embed-template.js +68 -3
  17. package/lib/expression.js +161 -0
  18. package/lib/index.js +13 -1
  19. package/lib/props.js +28 -10
  20. package/lib/tree/create-elements-tree.js +14 -1
  21. package/lib/tree/root.js +63 -1
  22. package/lib/tree/webstudio-component.js +9 -2
  23. package/lib/types/components/component-meta.d.ts +115 -0
  24. package/lib/types/context.d.ts +3 -0
  25. package/lib/types/css/normalize.d.ts +1316 -0
  26. package/lib/types/embed-template.d.ts +512 -0
  27. package/lib/types/expression.d.ts +6 -0
  28. package/lib/types/expression.test.d.ts +1 -0
  29. package/lib/types/index.d.ts +2 -1
  30. package/lib/types/props.d.ts +8 -7
  31. package/lib/types/tree/create-elements-tree.d.ts +4 -2
  32. package/lib/types/tree/root.d.ts +3 -3
  33. package/lib/types/tree/webstudio-component.d.ts +1 -0
  34. package/package.json +15 -15
  35. package/src/components/component-meta.ts +1 -0
  36. package/src/context.tsx +11 -0
  37. package/src/css/global-rules.ts +2 -1
  38. package/src/css/normalize.ts +10 -13
  39. package/src/embed-template.test.ts +177 -1
  40. package/src/embed-template.ts +73 -2
  41. package/src/expression.test.ts +122 -0
  42. package/src/expression.ts +183 -0
  43. package/src/index.ts +7 -0
  44. package/src/props.ts +29 -10
  45. package/src/tree/create-elements-tree.tsx +20 -1
  46. package/src/tree/root.ts +81 -4
  47. package/src/tree/webstudio-component.tsx +7 -1
@@ -0,0 +1,122 @@
1
+ import { expect, test } from "@jest/globals";
2
+ import {
3
+ decodeDataSourceVariable,
4
+ encodeDataSourceVariable,
5
+ executeExpressions,
6
+ validateExpression,
7
+ } from "./expression";
8
+
9
+ test("allow literals and array expressions", () => {
10
+ expect(
11
+ validateExpression(`["", '', 0, true, false, null, undefined]`)
12
+ ).toEqual(`["", '', 0, true, false, null, undefined]`);
13
+ });
14
+
15
+ test("allow unary and binary expressions", () => {
16
+ expect(validateExpression(`[-1, 1 + 1]`)).toEqual(`[-1, 1 + 1]`);
17
+ });
18
+
19
+ test("forbid member expressions", () => {
20
+ expect(() => {
21
+ validateExpression("var1 + obj.param");
22
+ }).toThrowError(/Cannot access "param" of "obj"/);
23
+ });
24
+
25
+ test("forbid this expressions", () => {
26
+ expect(() => {
27
+ validateExpression("var1 + this");
28
+ }).toThrowError(/"this" is not supported/);
29
+ });
30
+
31
+ test("forbid call expressions", () => {
32
+ expect(() => {
33
+ validateExpression("var1 + fn1()");
34
+ }).toThrowError(/Cannot call "fn1"/);
35
+ expect(() => {
36
+ validateExpression("var1 + this.fn1()");
37
+ }).toThrowError(/Cannot call "this.fn1"/);
38
+ });
39
+
40
+ test("forbid ternary", () => {
41
+ expect(() => {
42
+ validateExpression("var1 ? var2 : var3");
43
+ }).toThrowError(/Ternary operator is not supported/);
44
+ });
45
+
46
+ test("forbid multiple expressions", () => {
47
+ expect(() => {
48
+ validateExpression("a b");
49
+ }).toThrowError(/Cannot use multiple expressions/);
50
+ expect(() => {
51
+ validateExpression("a, b");
52
+ }).toThrowError(/Cannot use multiple expressions/);
53
+ expect(() => {
54
+ validateExpression("a; b");
55
+ }).toThrowError(/Cannot use multiple expressions/);
56
+ });
57
+
58
+ test("transform identifiers", () => {
59
+ expect(validateExpression(`a + b`, (id) => `$ws$${id}`)).toEqual(
60
+ `$ws$a + $ws$b`
61
+ );
62
+ });
63
+
64
+ test("execute expression", () => {
65
+ const variables = new Map();
66
+ const expressions = new Map([["exp1", "1 + 1"]]);
67
+ expect(executeExpressions(variables, expressions)).toEqual(
68
+ new Map([["exp1", 2]])
69
+ );
70
+ });
71
+
72
+ test("execute expression dependent on variables", () => {
73
+ const variables = new Map([["var1", 5]]);
74
+ const expressions = new Map([["exp1", "var1 + 1"]]);
75
+ expect(executeExpressions(variables, expressions)).toEqual(
76
+ new Map([["exp1", 6]])
77
+ );
78
+ });
79
+
80
+ test("execute expression dependent on other expressions", () => {
81
+ const variables = new Map([["var1", 3]]);
82
+ const expressions = new Map([
83
+ ["exp1", "exp0 + 1"],
84
+ ["exp0", "var1 + 2"],
85
+ ]);
86
+ expect(executeExpressions(variables, expressions)).toEqual(
87
+ new Map([
88
+ ["exp1", 6],
89
+ ["exp0", 5],
90
+ ])
91
+ );
92
+ });
93
+
94
+ test("forbid circular expressions", () => {
95
+ const variables = new Map([["var1", 3]]);
96
+ const expressions = new Map([
97
+ ["exp0", "exp2 + 1"],
98
+ ["exp1", "exp0 + 2"],
99
+ ["exp2", "exp1 + 3"],
100
+ ]);
101
+ expect(() => {
102
+ executeExpressions(variables, expressions);
103
+ }).toThrowError(/exp2 is not defined/);
104
+ });
105
+
106
+ test("make sure dependency exists", () => {
107
+ const variables = new Map();
108
+ const expressions = new Map([["exp1", "var1 + 1"]]);
109
+ expect(() => {
110
+ executeExpressions(variables, expressions);
111
+ }).toThrowError(/Unknown dependency "var1"/);
112
+ });
113
+
114
+ test("encode/decode variable names", () => {
115
+ expect(encodeDataSourceVariable("my--id")).toEqual(
116
+ "$ws$dataSource$my__DASH____DASH__id"
117
+ );
118
+ expect(decodeDataSourceVariable(encodeDataSourceVariable("my--id"))).toEqual(
119
+ "my--id"
120
+ );
121
+ expect(decodeDataSourceVariable("myVarName")).toEqual(undefined);
122
+ });
@@ -0,0 +1,183 @@
1
+ import jsep from "jsep";
2
+
3
+ type TransformIdentifier = (id: string) => string;
4
+
5
+ type Node = jsep.CoreExpression;
6
+
7
+ const generateCode = (
8
+ node: Node,
9
+ failOnForbidden: boolean,
10
+ transformIdentifier: TransformIdentifier
11
+ ): string => {
12
+ if (node.type === "Identifier") {
13
+ return transformIdentifier(node.name);
14
+ }
15
+ if (node.type === "MemberExpression") {
16
+ if (failOnForbidden) {
17
+ const object = generateCode(
18
+ node.object as Node,
19
+ false,
20
+ transformIdentifier
21
+ );
22
+ const property = generateCode(
23
+ node.property as Node,
24
+ false,
25
+ transformIdentifier
26
+ );
27
+ throw Error(`Cannot access "${property}" of "${object}"`);
28
+ }
29
+ const object = generateCode(
30
+ node.object as Node,
31
+ failOnForbidden,
32
+ transformIdentifier
33
+ );
34
+ const property = generateCode(
35
+ node.property as Node,
36
+ failOnForbidden,
37
+ transformIdentifier
38
+ );
39
+ return `${object}.${property}`;
40
+ }
41
+ if (node.type === "Literal") {
42
+ return node.raw;
43
+ }
44
+ if (node.type === "UnaryExpression") {
45
+ const arg = generateCode(
46
+ node.argument as Node,
47
+ failOnForbidden,
48
+ transformIdentifier
49
+ );
50
+ return `${node.operator}${arg}`;
51
+ }
52
+ if (node.type === "BinaryExpression") {
53
+ const left = generateCode(
54
+ node.left as Node,
55
+ failOnForbidden,
56
+ transformIdentifier
57
+ );
58
+ const right = generateCode(
59
+ node.right as Node,
60
+ failOnForbidden,
61
+ transformIdentifier
62
+ );
63
+ return `${left} ${node.operator} ${right}`;
64
+ }
65
+ if (node.type === "ArrayExpression") {
66
+ const elements = node.elements.map((element) =>
67
+ generateCode(element as Node, failOnForbidden, transformIdentifier)
68
+ );
69
+ return `[${elements.join(", ")}]`;
70
+ }
71
+ if (node.type === "CallExpression") {
72
+ if (failOnForbidden) {
73
+ const callee = generateCode(
74
+ node.callee as Node,
75
+ false,
76
+ transformIdentifier
77
+ );
78
+ throw Error(`Cannot call "${callee}"`);
79
+ }
80
+ const callee = generateCode(
81
+ node.callee as Node,
82
+ failOnForbidden,
83
+ transformIdentifier
84
+ );
85
+ const args = node.arguments.map((arg) =>
86
+ generateCode(arg as Node, failOnForbidden, transformIdentifier)
87
+ );
88
+ return `${callee}(${args.join(", ")})`;
89
+ }
90
+ if (node.type === "ThisExpression") {
91
+ if (failOnForbidden) {
92
+ throw Error(`"this" is not supported`);
93
+ }
94
+ return "this";
95
+ }
96
+ if (node.type === "ConditionalExpression") {
97
+ throw Error("Ternary operator is not supported");
98
+ }
99
+ if (node.type === "Compound") {
100
+ throw Error("Cannot use multiple expressions");
101
+ }
102
+ node satisfies never;
103
+ return "";
104
+ };
105
+
106
+ export const validateExpression = (
107
+ code: string,
108
+ transformIdentifier: TransformIdentifier = (id) => id
109
+ ) => {
110
+ const expression = jsep(code) as Node;
111
+ return generateCode(expression, true, transformIdentifier);
112
+ };
113
+
114
+ export const executeExpressions = (
115
+ variables: Map<string, unknown>,
116
+ expressions: Map<string, string>
117
+ ) => {
118
+ const depsById = new Map<string, Set<string>>();
119
+ for (const [id, code] of expressions) {
120
+ const deps = new Set<string>();
121
+ validateExpression(code, (identifier) => {
122
+ if (variables.has(identifier) || expressions.has(identifier)) {
123
+ deps.add(identifier);
124
+ return identifier;
125
+ }
126
+ throw Error(`Unknown dependency "${identifier}"`);
127
+ });
128
+ depsById.set(id, deps);
129
+ }
130
+
131
+ // sort topologically
132
+ const sortedExpressions = Array.from(expressions.keys()).sort(
133
+ (left, right) => {
134
+ if (depsById.get(left)?.has(right)) {
135
+ return 1;
136
+ }
137
+ if (depsById.get(right)?.has(left)) {
138
+ return -1;
139
+ }
140
+ return 0;
141
+ }
142
+ );
143
+
144
+ // execute chain of expressions
145
+ let header = "";
146
+ for (const [id, value] of variables) {
147
+ header += `const ${id} = ${JSON.stringify(value)};\n`;
148
+ }
149
+
150
+ const values = new Map<string, unknown>();
151
+
152
+ for (const id of sortedExpressions) {
153
+ const code = expressions.get(id);
154
+ if (code === undefined) {
155
+ continue;
156
+ }
157
+ const executeFn = new Function(`${header}\nreturn (${code});`);
158
+ const value = executeFn();
159
+ header += `const ${id} = ${JSON.stringify(value)};\n`;
160
+ values.set(id, value);
161
+ }
162
+
163
+ return values;
164
+ };
165
+
166
+ const dataSourceVariablePrefix = "$ws$dataSource$";
167
+
168
+ // data source id is generated with nanoid which has "-" in alphabeta
169
+ // here "-" is encoded with "__DASH__' in variable name
170
+ // https://github.com/ai/nanoid/blob/047686abad8f15aff05f3a2eeedb7c98b6847392/url-alphabet/index.js
171
+
172
+ export const encodeDataSourceVariable = (id: string) => {
173
+ const encoded = id.replaceAll("-", "__DASH__");
174
+ return `${dataSourceVariablePrefix}${encoded}`;
175
+ };
176
+
177
+ export const decodeDataSourceVariable = (name: string) => {
178
+ if (name.startsWith(dataSourceVariablePrefix)) {
179
+ const encoded = name.slice(dataSourceVariablePrefix.length);
180
+ return encoded.replaceAll("__DASH__", "-");
181
+ }
182
+ return;
183
+ };
package/src/index.ts CHANGED
@@ -14,8 +14,15 @@ export {
14
14
  } from "./components/component-meta";
15
15
  export * from "./embed-template";
16
16
  export {
17
+ useInstanceProps,
17
18
  usePropUrl,
18
19
  usePropAsset,
19
20
  getInstanceIdFromComponentProps,
20
21
  } from "./props";
21
22
  export { type Params, ReactSdkContext } from "./context";
23
+ export {
24
+ validateExpression,
25
+ executeExpressions,
26
+ encodeDataSourceVariable,
27
+ decodeDataSourceVariable,
28
+ } from "./expression";
package/src/props.ts CHANGED
@@ -26,17 +26,36 @@ export const getPropsByInstanceId = (props: Props) => {
26
26
  // this utility is be used only for preview with static props
27
27
  // so there is no need to use computed to optimize rerenders
28
28
  export const useInstanceProps = (instanceId: Instance["id"]) => {
29
- const { propsByInstanceIdStore } = useContext(ReactSdkContext);
30
- const propsByInstanceId = useStore(propsByInstanceIdStore);
31
- const instanceProps = propsByInstanceId.get(instanceId);
32
- const instancePropsObject: Record<Prop["name"], Prop["value"]> = {};
33
- if (instanceProps) {
34
- for (const prop of instanceProps) {
35
- if (prop.type !== "asset" && prop.type !== "page") {
36
- instancePropsObject[prop.name] = prop.value;
29
+ const { propsByInstanceIdStore, dataSourceValuesStore } =
30
+ useContext(ReactSdkContext);
31
+ const instancePropsObjectStore = useMemo(() => {
32
+ return computed(
33
+ [propsByInstanceIdStore, dataSourceValuesStore],
34
+ (propsByInstanceId, dataSourceValues) => {
35
+ const instancePropsObject: Record<Prop["name"], unknown> = {};
36
+ const instanceProps = propsByInstanceId.get(instanceId);
37
+ if (instanceProps === undefined) {
38
+ return instancePropsObject;
39
+ }
40
+ for (const prop of instanceProps) {
41
+ if (prop.type === "asset" || prop.type === "page") {
42
+ continue;
43
+ }
44
+ if (prop.type === "dataSource") {
45
+ const dataSourceId = prop.value;
46
+ const value = dataSourceValues.get(dataSourceId);
47
+ if (value !== undefined) {
48
+ instancePropsObject[prop.name] = value;
49
+ }
50
+ continue;
51
+ }
52
+ instancePropsObject[prop.name] = prop.value;
53
+ }
54
+ return instancePropsObject;
37
55
  }
38
- }
39
- }
56
+ );
57
+ }, [propsByInstanceIdStore, dataSourceValuesStore, instanceId]);
58
+ const instancePropsObject = useStore(instancePropsObjectStore);
40
59
  return instancePropsObject;
41
60
  };
42
61
 
@@ -2,7 +2,11 @@ import { type ComponentProps, Fragment } from "react";
2
2
  import type { ReadableAtom } from "nanostores";
3
3
  import { Scripts, ScrollRestoration } from "@remix-run/react";
4
4
  import type { Assets } from "@webstudio-is/asset-uploader";
5
- import type { Instance, Instances } from "@webstudio-is/project-build";
5
+ import type {
6
+ DataSource,
7
+ Instance,
8
+ Instances,
9
+ } from "@webstudio-is/project-build";
6
10
  import type { Components } from "../components/components-utils";
7
11
  import { ReactSdkContext, type Params } from "../context";
8
12
  import type { Pages, PropsByInstanceId } from "../props";
@@ -19,6 +23,8 @@ export const createElementsTree = ({
19
23
  propsByInstanceIdStore,
20
24
  assetsStore,
21
25
  pagesStore,
26
+ dataSourceValuesStore,
27
+ onDataSourceUpdate,
22
28
  Component,
23
29
  components,
24
30
  }: Params & {
@@ -27,6 +33,8 @@ export const createElementsTree = ({
27
33
  propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
28
34
  assetsStore: ReadableAtom<Assets>;
29
35
  pagesStore: ReadableAtom<Pages>;
36
+ dataSourceValuesStore: ReadableAtom<Map<DataSource["id"], unknown>>;
37
+ onDataSourceUpdate: (dataSourceId: DataSource["id"], value: unknown) => void;
30
38
  Component: (props: ComponentProps<typeof WebstudioComponent>) => JSX.Element;
31
39
  components: Components;
32
40
  }) => {
@@ -62,9 +70,20 @@ export const createElementsTree = ({
62
70
  propsByInstanceIdStore,
63
71
  assetsStore,
64
72
  pagesStore,
73
+ dataSourceValuesStore,
65
74
  renderer,
66
75
  imageBaseUrl,
67
76
  assetBaseUrl,
77
+ setDataSourceValue: (instanceId, propName, value) => {
78
+ const propsByInstanceId = propsByInstanceIdStore.get();
79
+ const props = propsByInstanceId.get(instanceId);
80
+ const prop = props?.find((prop) => prop.name === propName);
81
+ if (prop?.type !== "dataSource") {
82
+ throw Error(`${propName} is not data source`);
83
+ }
84
+ const dataSourceId = prop.value;
85
+ onDataSourceUpdate(dataSourceId, value);
86
+ },
68
87
  }}
69
88
  >
70
89
  {root}
package/src/tree/root.ts CHANGED
@@ -1,12 +1,56 @@
1
- import type { ComponentProps } from "react";
2
- import { atom } from "nanostores";
3
- import type { Build, Page } from "@webstudio-is/project-build";
1
+ import { useRef, type ComponentProps } from "react";
2
+ import {
3
+ atom,
4
+ computed,
5
+ type ReadableAtom,
6
+ type WritableAtom,
7
+ } from "nanostores";
8
+ import { type Build, type Page, DataSource } from "@webstudio-is/project-build";
4
9
  import type { Asset } from "@webstudio-is/asset-uploader";
5
10
  import { createElementsTree } from "./create-elements-tree";
6
11
  import { WebstudioComponent } from "./webstudio-component";
7
12
  import { getPropsByInstanceId } from "../props";
8
13
  import type { Components } from "../components/components-utils";
9
14
  import type { Params } from "../context";
15
+ import {
16
+ executeExpressions,
17
+ encodeDataSourceVariable,
18
+ decodeDataSourceVariable,
19
+ } from "../expression";
20
+
21
+ const computeExpressions = (
22
+ dataSources: [DataSource["id"], DataSource][],
23
+ dataSourceValues: Map<DataSource["id"], unknown>
24
+ ) => {
25
+ const outputValues = new Map<DataSource["id"], unknown>();
26
+ const variables = new Map<string, unknown>();
27
+ const expressions = new Map<string, string>();
28
+ for (const [dataSourceId, dataSource] of dataSources) {
29
+ const name = encodeDataSourceVariable(dataSourceId);
30
+ if (dataSource.type === "variable") {
31
+ const value =
32
+ dataSourceValues.get(dataSourceId) ?? dataSource.value.value;
33
+ variables.set(name, value);
34
+ outputValues.set(dataSourceId, value);
35
+ }
36
+ if (dataSource.type === "expression") {
37
+ expressions.set(name, dataSource.code);
38
+ }
39
+ }
40
+ try {
41
+ const outputVariables = executeExpressions(variables, expressions);
42
+ for (const [name, value] of outputVariables) {
43
+ const id = decodeDataSourceVariable(name);
44
+ if (id !== undefined) {
45
+ outputValues.set(id, value);
46
+ }
47
+ }
48
+ } catch (error) {
49
+ // eslint-disable-next-line no-console
50
+ console.error(error);
51
+ }
52
+ return outputValues;
53
+ };
10
54
 
11
55
  export type Data = {
12
56
  page: Page;
@@ -17,7 +61,7 @@ export type Data = {
17
61
  };
18
62
 
19
63
  export type RootPropsData = Omit<Data, "build"> & {
20
- build: Pick<Data["build"], "instances" | "props">;
64
+ build: Pick<Data["build"], "instances" | "props" | "dataSources">;
21
65
  };
22
66
 
23
67
  type RootProps = {
@@ -31,6 +75,33 @@ export const InstanceRoot = ({
31
75
  Component,
32
76
  components,
33
77
  }: RootProps): JSX.Element | null => {
78
+ const dataSourceVariablesStoreRef = useRef<
79
+ undefined | WritableAtom<Map<DataSource["id"], unknown>>
80
+ >(undefined);
81
+ // initialize store with default data source values
82
+ if (dataSourceVariablesStoreRef.current === undefined) {
83
+ const dataSourceVariables = new Map<DataSource["id"], unknown>();
84
+ for (const [dataSourceId, dataSource] of data.build.dataSources) {
85
+ dataSourceVariables.set(dataSourceId, dataSource);
86
+ }
87
+ dataSourceVariablesStoreRef.current = atom(dataSourceVariables);
88
+ }
89
+ const dataSourceVariablesStore = dataSourceVariablesStoreRef.current;
90
+
91
+ const dataSourceValuesStoreRef = useRef<
92
+ undefined | ReadableAtom<Map<DataSource["id"], unknown>>
93
+ >(undefined);
94
+ // initialize store with default data source values
95
+ if (dataSourceValuesStoreRef.current === undefined) {
96
+ dataSourceValuesStoreRef.current = computed(
97
+ dataSourceVariablesStore,
98
+ (dataSourceVariables) => {
99
+ return computeExpressions(data.build.dataSources, dataSourceVariables);
100
+ }
101
+ );
102
+ }
103
+ const dataSourceValuesStore = dataSourceValuesStoreRef.current;
104
+
34
105
  return createElementsTree({
35
106
  imageBaseUrl: data.params?.imageBaseUrl ?? "/",
36
107
  assetBaseUrl: data.params?.assetBaseUrl ?? "/",
@@ -41,6 +112,12 @@ export const InstanceRoot = ({
41
112
  ),
42
113
  assetsStore: atom(new Map(data.assets.map((asset) => [asset.id, asset]))),
43
114
  pagesStore: atom(new Map(data.pages.map((page) => [page.id, page]))),
115
+ dataSourceValuesStore,
116
+ onDataSourceUpdate: (dataSourceId, value) => {
117
+ const dataSourceVariables = new Map(dataSourceVariablesStore.get());
118
+ dataSourceVariables.set(dataSourceId, value);
119
+ dataSourceVariablesStore.set(dataSourceVariables);
120
+ },
44
121
  Component: Component ?? WebstudioComponent,
45
122
  components,
46
123
  });
@@ -40,13 +40,18 @@ export const WebstudioComponent = ({
40
40
  components,
41
41
  ...rest
42
42
  }: WebstudioComponentProps) => {
43
- const instanceProps = useInstanceProps(instance.id);
43
+ const { [showAttribute]: show = true, ...instanceProps } = useInstanceProps(
44
+ instance.id
45
+ );
44
46
  const props = {
45
47
  ...instanceProps,
46
48
  ...rest,
47
49
  [idAttribute]: instance.id,
48
50
  [componentAttribute]: instance.component,
49
51
  };
52
+ if (show === false) {
53
+ return <></>;
54
+ }
50
55
  const Component = components.get(instance.component);
51
56
  if (Component === undefined) {
52
57
  return <></>;
@@ -60,4 +65,5 @@ export const WebstudioComponent = ({
60
65
 
61
66
  export const idAttribute = "data-ws-id";
62
67
  export const componentAttribute = "data-ws-component";
68
+ export const showAttribute = "data-ws-show";
63
69
  export const collapsedAttribute = "data-ws-collapsed";