Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions .devcontainer/postinstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,14 @@ sudo rm -rf /tmp/corepack-cache
sudo rm -rf /usr/local/lib/node_modules/corepack # Manually remove global corepack

# Reinstall corepack globally via npm
npm install -g corepack@latest # Install latest corepack version
npm install -g corepack@latest --force # Install latest corepack version
sudo corepack enable # Re-enable corepack

# Check corepack version after reinstall
echo "--- Corepack version after reinstall ---"
corepack --version
echo "--- End corepack version check ---"


# Prepare pnpm (again, after corepack reinstall)
sudo corepack prepare [email protected] --activate
corepack prepare [email protected] --activate

# Go to workspace directory
cd /workspaces/webstudio
Expand All @@ -42,7 +39,7 @@ find . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +

# Install dependencies, build, and migrate
pnpm install
pnpm run build
pnpm build
pnpm migrations migrate

# Add git aliases
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ jobs:
const results = [
await assertSize('./fixtures/ssg/dist/client', 352),
await assertSize('./fixtures/react-router-netlify/build/client', 360),
await assertSize('./fixtures/webstudio-features/build/client', 926),
await assertSize('./fixtures/webstudio-features/build/client', 932),
]
for (const result of results) {
if (result.passed) {
Expand Down
8 changes: 7 additions & 1 deletion apps/builder/app/builder/features/components/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,15 @@ const $metas = computed(
const availableComponents = new Set<string>();
const metas: Meta[] = [];
for (const [name, componentMeta] of componentMetas) {
if (
isFeatureEnabled("animation") === false &&
name.endsWith(":AnimateChildren")
) {
continue;
}

// only set available components from component meta
availableComponents.add(name);

if (
isFeatureEnabled("headSlotComponent") === false &&
name === "HeadSlot"
Expand Down
11 changes: 10 additions & 1 deletion apps/builder/app/builder/features/pages/page-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1631,7 +1631,16 @@ export const PageSettings = ({
onDuplicate={() => {
const newPageId = duplicatePage(pageId);
if (newPageId !== undefined) {
onDuplicate(newPageId);
// In `canvas.tsx`, within `subscribeStyles`, we use `requestAnimationFrame` (RAF) for style recalculation.
// After `duplicatePage`, styles are not yet recalculated.
// To ensure they are properly updated, we use double RAF.
requestAnimationFrame(() => {
// At this tick styles are updating
requestAnimationFrame(() => {
// At this tick styles are updated
onDuplicate(newPageId);
});
});
}
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ export const renderControl = ({
);
}

if (prop.type === "animationAction") {
throw new Error(
`Cannot render a fallback control for prop "${rest.propName}" with type animationAction`
);
}

prop satisfies never;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { parseCss } from "@webstudio-is/css-data";
import { StyleValue, toValue } from "@webstudio-is/css-engine";
import {
Text,
Grid,
IconButton,
Label,
Separator,
Tooltip,
} from "@webstudio-is/design-system";
import { MinusIcon, PlusIcon } from "@webstudio-is/icons";
import type { AnimationKeyframe } from "@webstudio-is/sdk";
import { Fragment, useMemo, useState } from "react";
import {
CssValueInput,
type IntermediateStyleValue,
} from "~/builder/features/style-panel/shared/css-value-input";
import { toKebabCase } from "~/builder/features/style-panel/shared/keyword-utils";
import { CodeEditor } from "~/builder/shared/code-editor";
import { useIds } from "~/shared/form-utils";

const unitOptions = [
{
id: "%" as const,
label: "%",
type: "unit" as const,
},
];

const OffsetInput = ({
id,
value,
onChange,
}: {
id: string;
value: number | undefined;
onChange: (value: number | undefined) => void;
}) => {
const [intermediateValue, setIntermediateValue] = useState<
StyleValue | IntermediateStyleValue
>();

return (
<CssValueInput
id={id}
placeholder="auto"
getOptions={() => []}
unitOptions={unitOptions}
intermediateValue={intermediateValue}
styleSource="default"
/* same as offset has 0 - 100% */
property={"fontStretch"}
value={
value !== undefined
? {
type: "unit",
value: Math.round(value * 1000) / 10,
unit: "%",
}
: undefined
}
onChange={(styleValue) => {
if (styleValue === undefined) {
setIntermediateValue(styleValue);
return;
}

const clampedStyleValue = { ...styleValue };
if (
clampedStyleValue.type === "unit" &&
clampedStyleValue.unit === "%"
) {
clampedStyleValue.value = Math.min(
100,
Math.max(0, clampedStyleValue.value)
);
}

setIntermediateValue(clampedStyleValue);
}}
onHighlight={(_styleValue) => {
/* @todo: think about preview */
}}
onChangeComplete={(event) => {
setIntermediateValue(undefined);

if (event.value.type === "unit" && event.value.unit === "%") {
onChange(Math.min(100, Math.max(0, event.value.value)) / 100);
return;
}

setIntermediateValue({
type: "invalid",
value: toValue(event.value),
});
}}
onAbort={() => {
/* @todo: allow to change some ephemeral property to see the result in action */
}}
onReset={() => {
setIntermediateValue(undefined);
onChange(undefined);
}}
/>
);
};

const Keyframe = ({
value,
onChange,
}: {
value: AnimationKeyframe;
onChange: (value: AnimationKeyframe | undefined) => void;
}) => {
const ids = useIds(["offset"]);

const cssProperties = useMemo(() => {
let result = ``;
for (const [property, style] of Object.entries(value.styles)) {
result = `${result}${toKebabCase(property)}: ${toValue(style)};\n`;
}
return result;
}, [value.styles]);

return (
<>
<Grid
gap={1}
align={"center"}
css={{ gridTemplateColumns: "1fr 1fr auto" }}
>
<Label htmlFor={ids.offset}>Offset</Label>
<OffsetInput
id={ids.offset}
value={value.offset}
onChange={(offset) => {
onChange({ ...value, offset });
}}
/>
<Tooltip content="Remove keyframe">
<IconButton onClick={() => onChange(undefined)}>
<MinusIcon />
</IconButton>
</Tooltip>
</Grid>
<Grid>
<CodeEditor
lang="css-properties"
size="keyframe"
value={cssProperties}
onChange={() => {
/* do nothing */
}}
onChangeComplete={(cssText) => {
const parsedStyles = parseCss(`selector{${cssText}}`);
onChange({
...value,
styles: parsedStyles.reduce(
(r, { property, value }) => ({ ...r, [property]: value }),
{}
),
});
}}
/>
</Grid>
</>
);
};

export const Keyframes = ({
value: keyframes,
onChange,
}: {
value: AnimationKeyframe[];
onChange: (value: AnimationKeyframe[]) => void;
}) => {
const ids = useIds(["addKeyframe"]);

return (
<Grid gap={2}>
<Grid gap={1} align={"center"} css={{ gridTemplateColumns: "1fr auto" }}>
<Label htmlFor={ids.addKeyframe}>
<Text variant={"titles"}>Keyframes</Text>
</Label>
<IconButton
id={ids.addKeyframe}
onClick={() =>
onChange([...keyframes, { offset: undefined, styles: {} }])
}
>
<PlusIcon />
</IconButton>
</Grid>

{keyframes.map((value, index) => (
<Fragment key={index}>
<Separator />
<Keyframe
key={index}
value={value}
onChange={(newValue) => {
if (newValue === undefined) {
const newValues = [...keyframes];
newValues.splice(index, 1);
onChange(newValues);
return;
}

const newValues = [...keyframes];
newValues[index] = newValue;
onChange(newValues);
}}
/>
</Fragment>
))}
</Grid>
);
};
Loading
Loading