Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions @types/scroll-timeline.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ declare class ScrollTimeline extends AnimationTimeline {
interface ViewTimelineOptions {
subject?: Element | Document | null;
axis?: ScrollAxis;
inset?: string;
}

declare class ViewTimeline extends ScrollTimeline {
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