@webstudio-is/react-sdk 0.83.0 → 0.84.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 (54) hide show
  1. package/lib/cjs/component-renderer.js +125 -0
  2. package/lib/cjs/components/component-meta.js +5 -0
  3. package/lib/cjs/context.js +2 -1
  4. package/lib/cjs/css/style-rules.js +1 -1
  5. package/lib/cjs/embed-template.js +101 -55
  6. package/lib/cjs/hook.js +34 -0
  7. package/lib/cjs/index.js +6 -0
  8. package/lib/cjs/instance-utils.js +65 -0
  9. package/lib/cjs/props.js +12 -2
  10. package/lib/cjs/tree/create-elements-tree.js +5 -4
  11. package/lib/cjs/tree/root.js +6 -2
  12. package/lib/cjs/tree/webstudio-component.js +2 -0
  13. package/lib/component-renderer.js +111 -0
  14. package/lib/components/component-meta.js +5 -0
  15. package/lib/context.js +2 -1
  16. package/lib/css/style-rules.js +1 -1
  17. package/lib/embed-template.js +101 -55
  18. package/lib/hook.js +14 -0
  19. package/lib/index.js +8 -1
  20. package/lib/instance-utils.js +45 -0
  21. package/lib/props.js +13 -3
  22. package/lib/tree/create-elements-tree.js +5 -4
  23. package/lib/tree/root.js +6 -2
  24. package/lib/tree/webstudio-component.js +2 -0
  25. package/lib/types/component-renderer.d.ts +8 -0
  26. package/lib/types/components/component-meta.d.ts +9 -6
  27. package/lib/types/context.d.ts +2 -0
  28. package/lib/types/css/css.d.ts +19 -19
  29. package/lib/types/css/global-rules.d.ts +19 -19
  30. package/lib/types/css/normalize.d.ts +47 -47
  31. package/lib/types/embed-template.d.ts +291 -181
  32. package/lib/types/hook.d.ts +31 -0
  33. package/lib/types/index.d.ts +4 -1
  34. package/lib/types/instance-utils.d.ts +16 -0
  35. package/lib/types/instance-utils.test.d.ts +1 -0
  36. package/lib/types/props.d.ts +47 -46
  37. package/lib/types/tree/create-elements-tree.d.ts +5 -2
  38. package/lib/types/tree/root.d.ts +5 -2
  39. package/lib/types/tree/webstudio-component.d.ts +1 -0
  40. package/package.json +11 -11
  41. package/src/component-renderer.tsx +117 -0
  42. package/src/components/component-meta.ts +5 -0
  43. package/src/context.tsx +3 -0
  44. package/src/css/style-rules.ts +1 -1
  45. package/src/embed-template.test.ts +81 -70
  46. package/src/embed-template.ts +116 -56
  47. package/src/hook.ts +42 -0
  48. package/src/index.ts +4 -0
  49. package/src/instance-utils.test.ts +89 -0
  50. package/src/instance-utils.ts +65 -0
  51. package/src/props.ts +13 -1
  52. package/src/tree/create-elements-tree.tsx +8 -3
  53. package/src/tree/root.ts +8 -0
  54. package/src/tree/webstudio-component.tsx +1 -0
@@ -1,8 +1,5 @@
1
1
  import { expect, test } from "@jest/globals";
2
- import {
3
- generateDataFromEmbedTemplate,
4
- namespaceEmbedTemplateComponents,
5
- } from "./embed-template";
2
+ import { generateDataFromEmbedTemplate, namespaceMeta } from "./embed-template";
6
3
  import { showAttribute } from "./tree";
7
4
 
8
5
  const expectString = expect.any(String);
@@ -223,15 +220,14 @@ test("generate data for embedding from props bound to data source variables", ()
223
220
  {
224
221
  type: "instance",
225
222
  component: "Box1",
223
+ dataSources: {
224
+ showOtherBoxDataSource: { type: "variable", initialValue: false },
225
+ },
226
226
  props: [
227
227
  {
228
- type: "boolean",
228
+ type: "dataSource",
229
229
  name: "showOtherBox",
230
- value: false,
231
- dataSourceRef: {
232
- type: "variable",
233
- name: "showOtherBoxDataSource",
234
- },
230
+ dataSourceName: "showOtherBoxDataSource",
235
231
  },
236
232
  ],
237
233
  children: [],
@@ -241,13 +237,9 @@ test("generate data for embedding from props bound to data source variables", ()
241
237
  component: "Box2",
242
238
  props: [
243
239
  {
244
- type: "boolean",
240
+ type: "dataSource",
245
241
  name: showAttribute,
246
- value: false,
247
- dataSourceRef: {
248
- type: "variable",
249
- name: "showOtherBoxDataSource",
250
- },
242
+ dataSourceName: "showOtherBoxDataSource",
251
243
  },
252
244
  ],
253
245
  children: [],
@@ -305,12 +297,18 @@ test("generate data for embedding from props bound to data source expressions",
305
297
  {
306
298
  type: "instance",
307
299
  component: "Box1",
300
+ dataSources: {
301
+ boxState: { type: "variable", initialValue: "initial" },
302
+ boxStateSuccess: {
303
+ type: "expression",
304
+ code: `boxState === 'success'`,
305
+ },
306
+ },
308
307
  props: [
309
308
  {
310
- type: "string",
309
+ type: "dataSource",
311
310
  name: "state",
312
- value: "initial",
313
- dataSourceRef: { type: "variable", name: "boxState" },
311
+ dataSourceName: "boxState",
314
312
  },
315
313
  ],
316
314
  children: [],
@@ -320,14 +318,9 @@ test("generate data for embedding from props bound to data source expressions",
320
318
  component: "Box2",
321
319
  props: [
322
320
  {
323
- type: "boolean",
321
+ type: "dataSource",
324
322
  name: showAttribute,
325
- value: false,
326
- dataSourceRef: {
327
- type: "expression",
328
- name: "boxStateSuccess",
329
- code: `boxState === 'success'`,
330
- },
323
+ dataSourceName: "boxStateSuccess",
331
324
  },
332
325
  ],
333
326
  children: [],
@@ -392,12 +385,14 @@ test("generate data for embedding from action props", () => {
392
385
  {
393
386
  type: "instance",
394
387
  component: "Box1",
388
+ dataSources: {
389
+ boxState: { type: "variable", initialValue: "initial" },
390
+ },
395
391
  props: [
396
392
  {
397
- type: "string",
393
+ type: "dataSource",
398
394
  name: "state",
399
- value: "initial",
400
- dataSourceRef: { type: "variable", name: "boxState" },
395
+ dataSourceName: "boxState",
401
396
  },
402
397
  ],
403
398
  children: [
@@ -495,48 +490,64 @@ test("generate data for embedding from action props", () => {
495
490
 
496
491
  test("add namespace to selected components in embed template", () => {
497
492
  expect(
498
- namespaceEmbedTemplateComponents(
499
- [
500
- {
501
- type: "instance",
502
- component: "Tooltip",
503
- children: [
504
- { type: "text", value: "Some text" },
505
- {
506
- type: "instance",
507
- component: "Box",
508
- children: [
509
- {
510
- type: "instance",
511
- component: "Button",
512
- children: [],
513
- },
514
- ],
515
- },
516
- ],
517
- },
518
- ],
493
+ namespaceMeta(
494
+ {
495
+ type: "container",
496
+ label: "",
497
+ icon: "",
498
+ requiredAncestors: ["Button", "Box"],
499
+ invalidAncestors: ["Tooltip"],
500
+ indexWithinAncestor: "Tooltip",
501
+ template: [
502
+ {
503
+ type: "instance",
504
+ component: "Tooltip",
505
+ children: [
506
+ { type: "text", value: "Some text" },
507
+ {
508
+ type: "instance",
509
+ component: "Box",
510
+ children: [
511
+ {
512
+ type: "instance",
513
+ component: "Button",
514
+ children: [],
515
+ },
516
+ ],
517
+ },
518
+ ],
519
+ },
520
+ ],
521
+ },
519
522
  "my-namespace",
520
523
  new Set(["Tooltip", "Button"])
521
524
  )
522
- ).toEqual([
523
- {
524
- type: "instance",
525
- component: "my-namespace:Tooltip",
526
- children: [
527
- { type: "text", value: "Some text" },
528
- {
529
- type: "instance",
530
- component: "Box",
531
- children: [
532
- {
533
- type: "instance",
534
- component: "my-namespace:Button",
535
- children: [],
536
- },
537
- ],
538
- },
539
- ],
540
- },
541
- ]);
525
+ ).toEqual({
526
+ type: "container",
527
+ label: "",
528
+ icon: "",
529
+ requiredAncestors: ["my-namespace:Button", "Box"],
530
+ invalidAncestors: ["my-namespace:Tooltip"],
531
+ indexWithinAncestor: "my-namespace:Tooltip",
532
+ template: [
533
+ {
534
+ type: "instance",
535
+ component: "my-namespace:Tooltip",
536
+ children: [
537
+ { type: "text", value: "Some text" },
538
+ {
539
+ type: "instance",
540
+ component: "Box",
541
+ children: [
542
+ {
543
+ type: "instance",
544
+ component: "my-namespace:Button",
545
+ children: [],
546
+ },
547
+ ],
548
+ },
549
+ ],
550
+ },
551
+ ],
552
+ });
542
553
  });
@@ -13,6 +13,7 @@ import {
13
13
  import { StyleValue, type StyleProperty } from "@webstudio-is/css-data";
14
14
  import type { Simplify } from "type-fest";
15
15
  import { encodeDataSourceVariable, validateExpression } from "./expression";
16
+ import type { WsComponentMeta } from "./components/component-meta";
16
17
 
17
18
  const EmbedTemplateText = z.object({
18
19
  type: z.literal("text"),
@@ -21,43 +22,48 @@ const EmbedTemplateText = z.object({
21
22
 
22
23
  type EmbedTemplateText = z.infer<typeof EmbedTemplateText>;
23
24
 
24
- const DataSourceVariableRef = z.object({
25
- type: z.literal("variable"),
26
- name: z.string(),
27
- });
28
-
29
- const DataSourceRef = z.union([
30
- DataSourceVariableRef,
25
+ const EmbedTemplateDataSource = z.union([
26
+ z.object({
27
+ type: z.literal("variable"),
28
+ initialValue: z.union([
29
+ z.string(),
30
+ z.number(),
31
+ z.boolean(),
32
+ z.array(z.string()),
33
+ ]),
34
+ }),
31
35
  z.object({
32
36
  type: z.literal("expression"),
33
- name: z.string(),
34
37
  code: z.string(),
35
38
  }),
36
39
  ]);
37
40
 
41
+ type EmbedTemplateDataSource = z.infer<typeof EmbedTemplateDataSource>;
42
+
38
43
  const EmbedTemplateProp = z.union([
44
+ z.object({
45
+ type: z.literal("dataSource"),
46
+ name: z.string(),
47
+ dataSourceName: z.string(),
48
+ }),
39
49
  z.object({
40
50
  type: z.literal("number"),
41
51
  name: z.string(),
42
- dataSourceRef: z.optional(DataSourceRef),
43
52
  value: z.number(),
44
53
  }),
45
54
  z.object({
46
55
  type: z.literal("string"),
47
56
  name: z.string(),
48
- dataSourceRef: z.optional(DataSourceRef),
49
57
  value: z.string(),
50
58
  }),
51
59
  z.object({
52
60
  type: z.literal("boolean"),
53
61
  name: z.string(),
54
- dataSourceRef: z.optional(DataSourceRef),
55
62
  value: z.boolean(),
56
63
  }),
57
64
  z.object({
58
65
  type: z.literal("string[]"),
59
66
  name: z.string(),
60
- dataSourceRef: z.optional(DataSourceRef),
61
67
  value: z.array(z.string()),
62
68
  }),
63
69
  z.object({
@@ -95,6 +101,7 @@ export type EmbedTemplateInstance = {
95
101
  type: "instance";
96
102
  component: string;
97
103
  label?: string;
104
+ dataSources?: Record<string, EmbedTemplateDataSource>;
98
105
  props?: EmbedTemplateProp[];
99
106
  styles?: EmbedTemplateStyleDecl[];
100
107
  children: Array<EmbedTemplateInstance | EmbedTemplateText>;
@@ -106,6 +113,7 @@ export const EmbedTemplateInstance: z.ZodType<EmbedTemplateInstance> = z.lazy(
106
113
  type: z.literal("instance"),
107
114
  component: z.string(),
108
115
  label: z.optional(z.string()),
116
+ dataSources: z.optional(z.record(z.string(), EmbedTemplateDataSource)),
109
117
  props: z.optional(z.array(EmbedTemplateProp)),
110
118
  styles: z.optional(z.array(EmbedTemplateStyleDecl)),
111
119
  children: WsEmbedTemplate,
@@ -118,6 +126,25 @@ export const WsEmbedTemplate = z.lazy(() =>
118
126
 
119
127
  export type WsEmbedTemplate = z.infer<typeof WsEmbedTemplate>;
120
128
 
129
+ const getDataSourceValue = (
130
+ value: Extract<EmbedTemplateDataSource, { type: "variable" }>["initialValue"]
131
+ ): Extract<DataSource, { type: "variable" }>["value"] => {
132
+ if (typeof value === "string") {
133
+ return { type: "string", value };
134
+ }
135
+ if (typeof value === "number") {
136
+ return { type: "number", value };
137
+ }
138
+ if (typeof value === "boolean") {
139
+ return { type: "boolean", value };
140
+ }
141
+ if (Array.isArray(value)) {
142
+ return { type: "string[]", value };
143
+ }
144
+ value satisfies never;
145
+ throw Error("Impossible case");
146
+ };
147
+
121
148
  const createInstancesFromTemplate = (
122
149
  treeTemplate: WsEmbedTemplate,
123
150
  instances: InstancesList,
@@ -133,6 +160,38 @@ const createInstancesFromTemplate = (
133
160
  if (item.type === "instance") {
134
161
  const instanceId = nanoid();
135
162
 
163
+ if (item.dataSources) {
164
+ for (const [name, dataSource] of Object.entries(item.dataSources)) {
165
+ if (dataSourceByRef.has(name)) {
166
+ throw Error(`${name} data source already defined`);
167
+ }
168
+ if (dataSource.type === "variable") {
169
+ dataSourceByRef.set(name, {
170
+ type: "variable",
171
+ id: nanoid(),
172
+ scopeInstanceId: instanceId,
173
+ name,
174
+ value: getDataSourceValue(dataSource.initialValue),
175
+ });
176
+ }
177
+ if (dataSource.type === "expression") {
178
+ dataSourceByRef.set(name, {
179
+ type: "expression",
180
+ id: nanoid(),
181
+ scopeInstanceId: instanceId,
182
+ name,
183
+ // replace all references with variable names
184
+ code: validateExpression(dataSource.code, {
185
+ transformIdentifier: (ref) => {
186
+ const id = dataSourceByRef.get(ref)?.id ?? ref;
187
+ return encodeDataSourceVariable(id);
188
+ },
189
+ }),
190
+ });
191
+ }
192
+ }
193
+ }
194
+
136
195
  // populate props
137
196
  if (item.props) {
138
197
  for (const prop of item.props) {
@@ -166,51 +225,21 @@ const createInstancesFromTemplate = (
166
225
  });
167
226
  continue;
168
227
  }
169
- if (prop.dataSourceRef === undefined) {
170
- props.push({ id: propId, instanceId, ...prop });
171
- continue;
172
- }
173
- let dataSource = dataSourceByRef.get(prop.dataSourceRef.name);
174
- if (dataSource === undefined) {
175
- const id = nanoid();
176
- const { name: propName, dataSourceRef, ...rest } = prop;
177
- if (dataSourceRef.type === "variable") {
178
- dataSource = {
179
- type: "variable",
180
- id,
181
- // the first instance where data source is appeared in becomes its scope
182
- scopeInstanceId: instanceId,
183
- name: dataSourceRef.name,
184
- value: rest,
185
- };
186
- dataSourceByRef.set(dataSourceRef.name, dataSource);
187
- } else if (dataSourceRef.type === "expression") {
188
- dataSource = {
189
- type: "expression",
190
- id,
191
- scopeInstanceId: instanceId,
192
- name: dataSourceRef.name,
193
- // replace all references with variable names
194
- code: validateExpression(dataSourceRef.code, {
195
- transformIdentifier: (ref) => {
196
- const id = dataSourceByRef.get(ref)?.id ?? ref;
197
- return encodeDataSourceVariable(id);
198
- },
199
- }),
200
- };
201
- dataSourceByRef.set(dataSourceRef.name, dataSource);
202
- } else {
203
- dataSourceRef satisfies never;
204
- continue;
228
+ if (prop.type === "dataSource") {
229
+ const dataSource = dataSourceByRef.get(prop.dataSourceName);
230
+ if (dataSource === undefined) {
231
+ throw Error(`${prop.dataSourceName} data source is not defined`);
205
232
  }
233
+ props.push({
234
+ id: propId,
235
+ instanceId,
236
+ type: "dataSource",
237
+ name: prop.name,
238
+ value: dataSource.id,
239
+ });
240
+ continue;
206
241
  }
207
- props.push({
208
- id: propId,
209
- instanceId,
210
- type: "dataSource",
211
- name: prop.name,
212
- value: dataSource.id,
213
- });
242
+ props.push({ id: propId, instanceId, ...prop });
214
243
  }
215
244
  }
216
245
 
@@ -309,7 +338,7 @@ export type EmbedTemplateData = ReturnType<
309
338
  typeof generateDataFromEmbedTemplate
310
339
  >;
311
340
 
312
- export const namespaceEmbedTemplateComponents = (
341
+ const namespaceEmbedTemplateComponents = (
313
342
  template: WsEmbedTemplate,
314
343
  namespace: string,
315
344
  components: Set<EmbedTemplateInstance["component"]>
@@ -334,3 +363,34 @@ export const namespaceEmbedTemplateComponents = (
334
363
  throw Error("Impossible case");
335
364
  });
336
365
  };
366
+
367
+ export const namespaceMeta = (
368
+ meta: WsComponentMeta,
369
+ namespace: string,
370
+ components: Set<EmbedTemplateInstance["component"]>
371
+ ) => {
372
+ const newMeta = { ...meta };
373
+ if (newMeta.requiredAncestors) {
374
+ newMeta.requiredAncestors = newMeta.requiredAncestors.map((component) =>
375
+ components.has(component) ? `${namespace}:${component}` : component
376
+ );
377
+ }
378
+ if (newMeta.invalidAncestors) {
379
+ newMeta.invalidAncestors = newMeta.invalidAncestors.map((component) =>
380
+ components.has(component) ? `${namespace}:${component}` : component
381
+ );
382
+ }
383
+ if (newMeta.indexWithinAncestor) {
384
+ newMeta.indexWithinAncestor = components.has(newMeta.indexWithinAncestor)
385
+ ? `${namespace}:${newMeta.indexWithinAncestor}`
386
+ : newMeta.indexWithinAncestor;
387
+ }
388
+ if (newMeta.template) {
389
+ newMeta.template = namespaceEmbedTemplateComponents(
390
+ newMeta.template,
391
+ namespace,
392
+ components
393
+ );
394
+ }
395
+ return newMeta;
396
+ };
package/src/hook.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { Instance, Prop } from "@webstudio-is/project-build";
2
+
3
+ /**
4
+ * Hooks are subscriptions to builder events
5
+ * with limited way to interact with it.
6
+ * Called independently from components.
7
+ */
8
+
9
+ export type HookContext = {
10
+ setPropVariable: (
11
+ instanceId: Instance["id"],
12
+ propName: Prop["name"],
13
+ value: unknown
14
+ ) => void;
15
+ };
16
+
17
+ export type InstanceSelection = Instance[];
18
+
19
+ type NavigatorEvent = {
20
+ instanceSelection: InstanceSelection;
21
+ };
22
+
23
+ export type Hook = {
24
+ onNavigatorSelect?: (context: HookContext, event: NavigatorEvent) => void;
25
+ onNavigatorUnselect?: (context: HookContext, event: NavigatorEvent) => void;
26
+ };
27
+
28
+ export const getClosestInstance = (
29
+ instanceSelection: InstanceSelection,
30
+ currentInstance: Instance,
31
+ closestComponent: Instance["component"]
32
+ ) => {
33
+ let matched = false;
34
+ for (const instance of instanceSelection) {
35
+ if (currentInstance === instance) {
36
+ matched = true;
37
+ }
38
+ if (matched && instance.component === closestComponent) {
39
+ return instance;
40
+ }
41
+ }
42
+ };
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  usePropUrl,
19
19
  usePropAsset,
20
20
  getInstanceIdFromComponentProps,
21
+ getIndexWithinAncestorFromComponentProps,
21
22
  } from "./props";
22
23
  export { type Params, ReactSdkContext } from "./context";
23
24
  export {
@@ -32,3 +33,6 @@ export {
32
33
  decodeDataSourceVariable,
33
34
  decodeVariablesMap,
34
35
  } from "./expression";
36
+ export { renderComponentTemplate } from "./component-renderer";
37
+ export { getIndexesWithinAncestors } from "./instance-utils";
38
+ export * from "./hook";
@@ -0,0 +1,89 @@
1
+ import { test, expect } from "@jest/globals";
2
+ import type { Instance, Instances } from "@webstudio-is/project-build";
3
+ import { getIndexesWithinAncestors } from "./instance-utils";
4
+ import type { WsComponentMeta } from ".";
5
+
6
+ const getIdValuePair = <T extends { id: string }>(item: T) =>
7
+ [item.id, item] as const;
8
+
9
+ const toMap = <T extends { id: string }>(list: T[]) =>
10
+ new Map(list.map(getIdValuePair));
11
+
12
+ const createInstance = (
13
+ id: Instance["id"],
14
+ component: string,
15
+ children: Instance["children"]
16
+ ): Instance => {
17
+ return { type: "instance", id, component, children };
18
+ };
19
+
20
+ const createMeta = (meta?: Partial<WsComponentMeta>) => {
21
+ return { type: "container", label: "", icon: "", ...meta } as const;
22
+ };
23
+
24
+ test("get indexes within ancestors", () => {
25
+ // body0
26
+ // tabs1
27
+ // tabs1list
28
+ // tabs1box
29
+ // tabs1trigger1
30
+ // tabs1trigger2
31
+ // tabs1content1
32
+ // tabs2
33
+ // tabs2list
34
+ // tabs2trigger1
35
+ // tabs2content1
36
+ // tabs1content2
37
+ const instances: Instances = toMap([
38
+ createInstance("body0", "Body", [{ type: "id", value: "tabs1" }]),
39
+ // tabs1
40
+ createInstance("tabs1", "Tabs", [
41
+ { type: "id", value: "tabs1list" },
42
+ { type: "id", value: "tabs1content1" },
43
+ { type: "id", value: "tabs1content2" },
44
+ ]),
45
+ createInstance("tabs1list", "TabsList", [
46
+ { type: "id", value: "tabs1box" },
47
+ ]),
48
+ createInstance("tabs1box", "Box", [
49
+ { type: "id", value: "tabs1trigger1" },
50
+ { type: "id", value: "tabs1trigger2" },
51
+ ]),
52
+ createInstance("tabs1trigger1", "TabsTrigger", []),
53
+ createInstance("tabs1trigger2", "TabsTrigger", []),
54
+ createInstance("tabs1content1", "TabsContent", [
55
+ { type: "id", value: "tabs2" },
56
+ ]),
57
+ createInstance("tabs1content2", "TabsContent", []),
58
+ // tabs2
59
+ createInstance("tabs2", "Tabs", [
60
+ { type: "id", value: "tabs2list" },
61
+ { type: "id", value: "tabs2content1" },
62
+ ]),
63
+ createInstance("tabs2list", "TabsList", [
64
+ { type: "id", value: "tabs2trigger1" },
65
+ ]),
66
+ createInstance("tabs2trigger1", "TabsTrigger", []),
67
+ createInstance("tabs2content1", "TabsContent", []),
68
+ ] satisfies Instance[]);
69
+ const metas = new Map<Instance["component"], WsComponentMeta>([
70
+ ["Body", createMeta()],
71
+ ["Box", createMeta()],
72
+ ["Tabs", createMeta()],
73
+ ["TabsList", createMeta({ indexWithinAncestor: "Tabs" })],
74
+ ["TabsTrigger", createMeta({ indexWithinAncestor: "TabsList" })],
75
+ ["TabsContent", createMeta({ indexWithinAncestor: "Tabs" })],
76
+ ]);
77
+ expect(getIndexesWithinAncestors(metas, instances, ["body0"])).toEqual(
78
+ new Map([
79
+ ["tabs1list", 0],
80
+ ["tabs1trigger1", 0],
81
+ ["tabs1trigger2", 1],
82
+ ["tabs1content1", 0],
83
+ ["tabs1content2", 1],
84
+ ["tabs2list", 0],
85
+ ["tabs2trigger1", 0],
86
+ ["tabs2content1", 0],
87
+ ])
88
+ );
89
+ });
@@ -0,0 +1,65 @@
1
+ import type { Instance, Instances } from "@webstudio-is/project-build";
2
+ import type { WsComponentMeta } from "./components/component-meta";
3
+
4
+ export type IndexesWithinAncestors = Map<Instance["id"], number>;
5
+
6
+ export const getIndexesWithinAncestors = (
7
+ metas: Map<Instance["component"], WsComponentMeta>,
8
+ instances: Instances,
9
+ rootIds: Instance["id"][]
10
+ ) => {
11
+ const ancestors = new Set<Instance["component"]>();
12
+ for (const meta of metas.values()) {
13
+ if (meta.indexWithinAncestor !== undefined) {
14
+ ancestors.add(meta.indexWithinAncestor);
15
+ }
16
+ }
17
+
18
+ const indexes: IndexesWithinAncestors = new Map();
19
+
20
+ const traverseInstances = (
21
+ instances: Instances,
22
+ instanceId: Instance["id"],
23
+ latestIndexes = new Map<
24
+ Instance["component"],
25
+ Map<Instance["component"], number>
26
+ >()
27
+ ) => {
28
+ const instance = instances.get(instanceId);
29
+ if (instance === undefined) {
30
+ return;
31
+ }
32
+ const meta = metas.get(instance.component);
33
+ if (meta === undefined) {
34
+ return;
35
+ }
36
+
37
+ if (ancestors.has(instance.component)) {
38
+ latestIndexes = new Map(latestIndexes);
39
+ latestIndexes.set(instance.component, new Map());
40
+ }
41
+
42
+ if (meta.indexWithinAncestor !== undefined) {
43
+ const ancestorIndexes = latestIndexes.get(meta.indexWithinAncestor);
44
+ if (ancestorIndexes !== undefined) {
45
+ let index = ancestorIndexes.get(instance.component) ?? -1;
46
+ index += 1;
47
+ ancestorIndexes.set(instance.component, index);
48
+ indexes.set(instance.id, index);
49
+ }
50
+ }
51
+
52
+ for (const child of instance.children) {
53
+ if (child.type === "id") {
54
+ traverseInstances(instances, child.value, latestIndexes);
55
+ }
56
+ }
57
+ };
58
+
59
+ const latestIndexes = new Map();
60
+ for (const instanceId of rootIds) {
61
+ traverseInstances(instances, instanceId, latestIndexes);
62
+ }
63
+
64
+ return indexes;
65
+ };