Skip to content

Commit 075ab9d

Browse files
authored
Merge branch 'main' into replace-overflow-hidden
2 parents 294bf3d + f134553 commit 075ab9d

449 files changed

Lines changed: 13966 additions & 35296 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ jobs:
133133
}
134134
}
135135
const results = [
136-
await assertSize('./fixtures/ssg/dist/client', 352),
136+
await assertSize('./fixtures/ssg/dist/client', 356),
137137
await assertSize('./fixtures/react-router-netlify/build/client', 368),
138-
await assertSize('./fixtures/webstudio-features/build/client', 1028),
138+
await assertSize('./fixtures/webstudio-features/build/client', 1052),
139139
]
140140
for (const result of results) {
141141
if (result.passed) {

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
- [Twitter](https://twitter.com/getwebstudio)
2323
- [Youtube](https://www.youtube.com/@getwebstudio)
24-
- [Discord](https://discord.gg/UNdyrDkq5r)
24+
- [Discord](https://wstd.us/community)
2525

2626
## Thanks
2727

apps/builder/app/builder/features/ai/ai-fetch-result.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
operations,
55
handleAiRequest,
66
commandDetect,
7+
WsEmbedTemplate,
78
} from "@webstudio-is/ai";
89
import { createRegularStyleSheet } from "@webstudio-is/css-engine";
910
import {
@@ -13,7 +14,6 @@ import {
1314
componentAttribute,
1415
} from "@webstudio-is/react-sdk";
1516
import {
16-
type WsEmbedTemplate,
1717
Instance,
1818
createScope,
1919
findTreeInstanceIds,

apps/builder/app/builder/features/ai/apply-operations.ts

Lines changed: 54 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import { nanoid } from "nanoid";
2-
import { getStyleDeclKey, Instance, type StyleSource } from "@webstudio-is/sdk";
3-
import { generateDataFromEmbedTemplate } from "@webstudio-is/react-sdk";
2+
import {
3+
getStyleDeclKey,
4+
Instance,
5+
isComponentDetachable,
6+
type StyleSource,
7+
} from "@webstudio-is/sdk";
48
import type { copywriter, operations } from "@webstudio-is/ai";
59
import { serverSyncStore } from "~/shared/sync";
610
import { isBaseBreakpoint } from "~/shared/breakpoints";
711
import {
812
deleteInstanceMutable,
13+
findClosestInsertable,
914
insertWebstudioFragmentAt,
1015
updateWebstudioData,
1116
type Insertable,
1217
} from "~/shared/instance-utils";
1318
import {
1419
$breakpoints,
1520
$instances,
21+
$props,
1622
$registeredComponentMetas,
1723
$selectedInstanceSelector,
1824
$styleSourceSelections,
@@ -21,7 +27,8 @@ import {
2127
} from "~/shared/nano-states";
2228
import type { InstanceSelector } from "~/shared/tree-utils";
2329
import { $selectedInstance, getInstancePath } from "~/shared/awareness";
24-
import { isInstanceDetachable } from "~/shared/matcher";
30+
import { isRichTextTree } from "~/shared/content-model";
31+
import { generateDataFromEmbedTemplate } from "./embed-template";
2532

2633
export const applyOperations = (operations: operations.WsOperations) => {
2734
for (const operation of operations) {
@@ -47,7 +54,7 @@ const insertTemplateByOp = (
4754
operation: operations.generateInsertTemplateWsOperation
4855
) => {
4956
const metas = $registeredComponentMetas.get();
50-
const templateData = generateDataFromEmbedTemplate(operation.template, metas);
57+
const fragment = generateDataFromEmbedTemplate(operation.template, metas);
5158

5259
// @todo Find a way to avoid the workaround below, peharps improving the prompt.
5360
// Occasionally the LLM picks a component name or the entire data-ws-id attribute as the insertion point.
@@ -63,26 +70,18 @@ const insertTemplateByOp = (
6370
}
6471
}
6572

66-
const rootInstanceIds = templateData.children
73+
const rootInstanceIds = fragment.children
6774
.filter((child) => child.type === "id")
6875
.map((child) => child.value);
6976

7077
const instanceSelector = computeSelectorForInstanceId(operation.addTo);
7178
if (instanceSelector) {
72-
const currentInstance = $instances.get().get(instanceSelector[0]);
73-
// Only container components are allowed to have child elements.
74-
if (
75-
currentInstance &&
76-
metas.get(currentInstance.component)?.type !== "container"
77-
) {
78-
return;
79-
}
80-
81-
const dropTarget: Insertable = {
79+
let insertable: Insertable = {
8280
parentSelector: instanceSelector,
8381
position: operation.addAtIndex + 1,
8482
};
85-
insertWebstudioFragmentAt(templateData, dropTarget);
83+
insertable = findClosestInsertable(fragment, insertable) ?? insertable;
84+
insertWebstudioFragmentAt(fragment, insertable);
8685
return rootInstanceIds;
8786
}
8887
};
@@ -96,15 +95,10 @@ const deleteInstanceByOp = (
9695
if (instanceSelector.length === 1) {
9796
return;
9897
}
99-
const metas = $registeredComponentMetas.get();
10098
updateWebstudioData((data) => {
101-
if (
102-
isInstanceDetachable({
103-
metas,
104-
instances: data.instances,
105-
instanceSelector,
106-
}) === false
107-
) {
99+
const [instanceId] = instanceSelector;
100+
const instance = data.instances.get(instanceId);
101+
if (instance && !isComponentDetachable(instance.component)) {
108102
return;
109103
}
110104
deleteInstanceMutable(
@@ -214,39 +208,47 @@ const computeSelectorForInstanceId = (instanceId: Instance["id"]) => {
214208
};
215209

216210
export const patchTextInstance = (textInstance: copywriter.TextInstance) => {
217-
serverSyncStore.createTransaction([$instances], (instances) => {
218-
const currentInstance = instances.get(textInstance.instanceId);
219-
220-
if (currentInstance === undefined) {
221-
return;
222-
}
223-
224-
const meta = $registeredComponentMetas.get().get(currentInstance.component);
211+
serverSyncStore.createTransaction(
212+
[$instances, $props],
213+
(instances, props) => {
214+
const currentInstance = instances.get(textInstance.instanceId);
225215

226-
// Only container components are allowed to have child elements.
227-
if (meta?.type !== "container") {
228-
return;
229-
}
216+
if (currentInstance === undefined) {
217+
return;
218+
}
230219

231-
if (currentInstance.children.length === 0) {
232-
currentInstance.children = [{ type: "text", value: textInstance.text }];
233-
return;
234-
}
220+
const canBeEdited = isRichTextTree({
221+
instanceId: textInstance.instanceId,
222+
instances,
223+
props,
224+
metas: $registeredComponentMetas.get(),
225+
});
226+
if (!canBeEdited) {
227+
return;
228+
}
235229

236-
// Instances can have a number of text child nodes without interleaving components.
237-
// When this is the case we treat the child nodes as a single text node,
238-
// otherwise the AI would generate children.length chunks of separate text.
239-
// We can identify this case of "joint" text instances when the index is -1.
240-
const replaceAll = textInstance.index === -1;
241-
if (replaceAll) {
242-
if (currentInstance.children.every((child) => child.type === "text")) {
230+
if (currentInstance.children.length === 0) {
243231
currentInstance.children = [{ type: "text", value: textInstance.text }];
232+
return;
244233
}
245-
return;
246-
}
247234

248-
if (currentInstance.children[textInstance.index].type === "text") {
249-
currentInstance.children[textInstance.index].value = textInstance.text;
235+
// Instances can have a number of text child nodes without interleaving components.
236+
// When this is the case we treat the child nodes as a single text node,
237+
// otherwise the AI would generate children.length chunks of separate text.
238+
// We can identify this case of "joint" text instances when the index is -1.
239+
const replaceAll = textInstance.index === -1;
240+
if (replaceAll) {
241+
if (currentInstance.children.every((child) => child.type === "text")) {
242+
currentInstance.children = [
243+
{ type: "text", value: textInstance.text },
244+
];
245+
}
246+
return;
247+
}
248+
249+
if (currentInstance.children[textInstance.index].type === "text") {
250+
currentInstance.children[textInstance.index].value = textInstance.text;
251+
}
250252
}
251-
});
253+
);
252254
};

packages/react-sdk/src/embed-template.test.tsx renamed to apps/builder/app/builder/features/ai/embed-template.test.tsx

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from "vitest";
2-
import { generateDataFromEmbedTemplate, namespaceMeta } from "./embed-template";
3-
import { showAttribute } from "./props";
2+
import { showAttribute } from "@webstudio-is/react-sdk";
3+
import { generateDataFromEmbedTemplate } from "./embed-template";
44

55
const expectString = expect.any(String);
66

@@ -664,31 +664,3 @@ test("generate data for embedding from instance child bound to variables", () =>
664664
resources: [],
665665
});
666666
});
667-
668-
test("add namespace to constants and indexWithinAncestor", () => {
669-
expect(
670-
namespaceMeta(
671-
{
672-
type: "container",
673-
label: "",
674-
icon: "",
675-
constraints: {
676-
relation: "ancestor",
677-
component: { $nin: ["Button", "Box"] },
678-
},
679-
indexWithinAncestor: "Tooltip",
680-
},
681-
"my-namespace",
682-
new Set(["Tooltip", "Button"])
683-
)
684-
).toEqual({
685-
type: "container",
686-
label: "",
687-
icon: "",
688-
constraints: {
689-
relation: "ancestor",
690-
component: { $nin: ["my-namespace:Button", "my-namespace:Box"] },
691-
},
692-
indexWithinAncestor: "my-namespace:Tooltip",
693-
});
694-
});

packages/react-sdk/src/embed-template.ts renamed to apps/builder/app/builder/features/ai/embed-template.ts

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@ import type {
88
Breakpoint,
99
DataSource,
1010
WebstudioFragment,
11-
Matcher,
12-
EmbedTemplateVariable,
13-
WsEmbedTemplate,
14-
EmbedTemplateInstance,
1511
WsComponentMeta,
1612
} from "@webstudio-is/sdk";
1713
import {
1814
encodeDataSourceVariable,
1915
transpileExpression,
2016
} from "@webstudio-is/sdk";
17+
import type { EmbedTemplateVariable, WsEmbedTemplate } from "@webstudio-is/ai";
2118

2219
const getVariablValue = (
2320
value: EmbedTemplateVariable["initialValue"]
@@ -275,47 +272,3 @@ export const generateDataFromEmbedTemplate = (
275272
resources: [],
276273
};
277274
};
278-
279-
const namespaceMatcher = (namespace: string, matcher: Matcher) => {
280-
const newMatcher = structuredClone(matcher);
281-
if (newMatcher.component?.$eq) {
282-
newMatcher.component.$eq = `${namespace}:${newMatcher.component.$eq}`;
283-
}
284-
if (newMatcher.component?.$neq) {
285-
newMatcher.component.$neq = `${namespace}:${newMatcher.component.$neq}`;
286-
}
287-
if (newMatcher.component?.$in) {
288-
newMatcher.component.$in = newMatcher.component.$in.map(
289-
(component) => `${namespace}:${component}`
290-
);
291-
}
292-
if (newMatcher.component?.$nin) {
293-
newMatcher.component.$nin = newMatcher.component.$nin.map(
294-
(component) => `${namespace}:${component}`
295-
);
296-
}
297-
return newMatcher;
298-
};
299-
300-
export const namespaceMeta = (
301-
meta: WsComponentMeta,
302-
namespace: string,
303-
components: Set<EmbedTemplateInstance["component"]>
304-
) => {
305-
const newMeta = { ...meta };
306-
if (newMeta.constraints) {
307-
if (Array.isArray(newMeta.constraints)) {
308-
newMeta.constraints = newMeta.constraints.map((matcher) =>
309-
namespaceMatcher(namespace, matcher)
310-
);
311-
} else {
312-
newMeta.constraints = namespaceMatcher(namespace, newMeta.constraints);
313-
}
314-
}
315-
if (newMeta.indexWithinAncestor) {
316-
newMeta.indexWithinAncestor = components.has(newMeta.indexWithinAncestor)
317-
? `${namespace}:${newMeta.indexWithinAncestor}`
318-
: newMeta.indexWithinAncestor;
319-
}
320-
return newMeta;
321-
};

apps/builder/app/builder/features/blocking-alerts/alert.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { atom } from "nanostores";
2+
import { useStore } from "@nanostores/react";
13
import type { ReactNode } from "react";
24
import {
5+
Button,
36
css,
47
Flex,
58
Popover,
@@ -23,7 +26,19 @@ const contentStyle = css({
2326
color: theme.colors.foregroundDestructive,
2427
});
2528

26-
export const Alert = ({ message }: { message: string | ReactNode }) => {
29+
const $isAlertDismissed = atom(false);
30+
31+
export const Alert = ({
32+
message,
33+
isDismissable,
34+
}: {
35+
message: string | ReactNode;
36+
isDismissable?: boolean;
37+
}) => {
38+
const isAlertDismissed = useStore($isAlertDismissed);
39+
if (isAlertDismissed) {
40+
return;
41+
}
2742
return (
2843
<Popover open>
2944
<PopoverContent css={{ zIndex: theme.zIndices.max }}>
@@ -38,6 +53,14 @@ export const Alert = ({ message }: { message: string | ReactNode }) => {
3853
<Text color="contrast" align="center">
3954
{message}
4055
</Text>
56+
{isDismissable && (
57+
<Button
58+
color="destructive"
59+
onClick={() => $isAlertDismissed.set(true)}
60+
>
61+
Dismiss
62+
</Button>
63+
)}
4164
</Flex>
4265
</Flex>
4366
</PopoverContent>

apps/builder/app/builder/features/blocking-alerts/blocking-alerts.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,9 @@ export const BlockingAlerts = () => {
9393
const isPreviewMode = useStore($isPreviewMode);
9494
const loadingState = useStore($loadingState);
9595

96+
const unsupportedBrowsersMessage = useUnsupportedBrowser();
9697
// Takes the latest message, order matters
97-
const message = [useTooSmallMessage(), useUnsupportedBrowser()]
98+
const message = [useTooSmallMessage(), unsupportedBrowsersMessage]
9899
.filter(Boolean)
99100
.pop();
100101

@@ -107,5 +108,10 @@ export const BlockingAlerts = () => {
107108
return;
108109
}
109110

110-
return <Alert message={message} />;
111+
return (
112+
<Alert
113+
message={message}
114+
isDismissable={unsupportedBrowsersMessage !== undefined}
115+
/>
116+
);
111117
};

apps/builder/app/builder/features/command-panel/command-panel.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export const CommandPanel: StoryFn = () => {
8181
<button onClick={openCommandPanel}>Open command panel</button>
8282
<br />
8383
<input
84-
defaultValue="Press cmd+k to open command panel"
84+
defaultValue="Press meta+k to open command panel"
8585
style={{ width: 300 }}
8686
/>
8787
<CommandPanelComponent />

0 commit comments

Comments
 (0)