1
0
mirror of https://github.com/webstudio-is/webstudio.git synced 2025-03-15 09:45:09 +00:00

experimental: Animation publishing ()

## Description

1. What is this PR about (link the issue and add a short description)

## Steps for reproduction

1. click button
2. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
This commit is contained in:
Ivan Starkov
2025-03-11 11:19:01 +05:00
committed by GitHub
parent 051c1ba226
commit ca4b0f89bc
27 changed files with 1973 additions and 77 deletions

@ -135,7 +135,7 @@ jobs:
const results = [
await assertSize('./fixtures/ssg/dist/client', 352),
await assertSize('./fixtures/react-router-netlify/build/client', 368),
await assertSize('./fixtures/webstudio-features/build/client', 936),
await assertSize('./fixtures/webstudio-features/build/client', 1024),
]
for (const result of results) {
if (result.passed) {

3
.gitignore vendored

@ -62,4 +62,5 @@ dist
# should be here otherwise if placed inside prisma-client pnpm deploy doesn't copy it
packages/prisma-client/src/__generated__
.temp
.temp
*.timestamp-*.mjs

@ -15,6 +15,8 @@ export default tseslint.config({
"packages/prisma-client/prisma/migrations/**",
"packages/cli/templates/**",
"fixtures/**",
"packages/sdk-components-animation/private-src/polyfill/**",
"packages/sdk-components-animation/private-src/perf/**",
],
extends: [
eslint.configs.recommended,

@ -53,6 +53,7 @@
"@types/react-dom": "^18.2.25",
"typescript": "5.7.3",
"vite": "^5.4.11",
"wrangler": "^3.63.2"
"wrangler": "^3.63.2",
"fast-glob": "^3.3.2"
}
}

@ -4,7 +4,34 @@ import {
} from "@remix-run/dev";
import { defineConfig } from "vite";
import { existsSync } from "node:fs";
// @ts-ignore
import path from "node:path";
// @ts-ignore
import fg from "fast-glob";
const rootDir = ["..", "../..", "../../.."]
.map((dir) => path.join(__dirname, dir))
.find((dir) => existsSync(path.join(dir, ".git")));
const hasPrivateFolders =
fg.sync([path.join(rootDir ?? "", "packages/*/private-src/*")], {
ignore: ["**/node_modules/**"],
}).length > 0;
const conditions = hasPrivateFolders
? ["webstudio-private", "webstudio"]
: ["webstudio"];
export default defineConfig(({ mode }) => ({
resolve: {
conditions,
},
ssr: {
resolve: {
conditions,
},
},
plugins: [
// without this, remixCloudflareDevProxy trying to load workerd even for production (it's not needed for production)
mode === "production" ? undefined : remixCloudflareDevProxy(),

File diff suppressed because it is too large Load Diff

@ -1,26 +1,26 @@
export const sitemap = [
{
path: "/",
lastModified: "2025-03-04",
lastModified: "2025-03-10",
},
{
path: "/_route_with_symbols_",
lastModified: "2025-03-04",
lastModified: "2025-03-10",
},
{
path: "/form",
lastModified: "2025-03-04",
lastModified: "2025-03-10",
},
{
path: "/heading-with-id",
lastModified: "2025-03-04",
lastModified: "2025-03-10",
},
{
path: "/resources",
lastModified: "2025-03-04",
lastModified: "2025-03-10",
},
{
path: "/nested/nested-page",
lastModified: "2025-03-04",
lastModified: "2025-03-10",
},
];

@ -0,0 +1,39 @@
/* eslint-disable */
/* This is a auto generated file for building the project */
import type { PageMeta } from "@webstudio-is/sdk";
import type { System, ResourceRequest } from "@webstudio-is/sdk";
export const getResources = (_props: { system: System }) => {
const _data = new Map<string, ResourceRequest>([]);
const _action = new Map<string, ResourceRequest>([]);
return { data: _data, action: _action };
};
export const getPageMeta = ({
system,
resources,
}: {
system: System;
resources: Record<string, any>;
}): PageMeta => {
return {
title: "Untitled",
description: "",
excludePageFromSearch: true,
language: "",
socialImageAssetName: undefined,
socialImageUrl: "",
status: 200,
redirect: "",
custom: [],
};
};
type Params = Record<string, string | undefined>;
export const getRemixParams = ({ ...params }: Params): Params => {
return params;
};
export const projectId = "cddc1d44-af37-4cb6-a430-d300cf6f932d";
export const contactEmail = "hello@webstudio.is";

@ -0,0 +1,294 @@
/* eslint-disable */
/* This is a auto generated file for building the project */
import { Fragment, useState } from "react";
import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime";
import { Body as Body } from "@webstudio-is/sdk-components-react-router";
import {
Box as Box,
Heading as Heading,
} from "@webstudio-is/sdk-components-react";
import {
AnimateChildren as AnimateChildren,
AnimateText as AnimateText,
} from "@webstudio-is/sdk-components-animation";
export const siteName = "KittyGuardedZone";
export const favIconAsset: string | undefined =
"DALL_E_2023-10-30_12.39.46_-_Photo_logo_with_a_bold_cat_silhouette_centered_on_a_contrasting_background_designed_for_clarity_at_small_32x32_favicon_resolution_00h6cEA8u2pJRvVJv7hRe.png";
// Font assets on current page (can be preloaded)
export const pageFontAssets: string[] = [];
export const pageBackgroundImageAssets: string[] = [];
const Page = (_props: { system: any }) => {
return (
<Body className={`w-body c1jgcte3 cncutr5 c1ywt30e cqvurle`}>
<Box className={`w-box c1m04i8w c13v7j50 cpjvam0 cqkqnmd`}>
<Heading className={`w-heading`}>{"ANIMATIONS"}</Heading>
</Box>
<Box className={`w-box c1y8ynw5 c2gdfrb c1yk3skc c16asro7 c1pt69cw`}>
<Box className={`w-box`}>
<AnimateChildren
action={{
type: "view",
animations: [
{
name: "Fade In",
description: "Fade in the element as it scrolls into view.",
keyframes: [
{
offset: 0,
styles: {
opacity: { type: "unit", unit: "number", value: 0 },
},
},
],
timing: {
easing: "linear",
fill: "backwards",
rangeStart: [
"entry",
{ type: "unit", value: 0, unit: "%" },
],
rangeEnd: [
"entry",
{ type: "unit", value: 100, unit: "%" },
],
},
},
{
name: "Fade Out",
description:
"Fade out the element as it scrolls out of view.",
keyframes: [
{
offset: 1,
styles: {
opacity: { type: "unit", unit: "number", value: 0 },
},
},
],
timing: {
easing: "linear",
fill: "forwards",
rangeStart: ["exit", { type: "unit", value: 0, unit: "%" }],
rangeEnd: ["exit", { type: "unit", value: 100, unit: "%" }],
},
},
{
name: "Parallax In",
description:
"Parallax the element as it scrolls out of view.",
keyframes: [
{
offset: 0,
styles: {
translate: {
type: "tuple",
value: [
{ type: "unit", unit: "number", value: 0 },
{ type: "unit", unit: "px", value: 100 },
],
},
},
},
],
timing: {
easing: "linear",
fill: "backwards",
rangeStart: [
"cover",
{ type: "unit", value: 0, unit: "%" },
],
rangeEnd: ["cover", { type: "unit", value: 50, unit: "%" }],
},
},
{
name: "Parallax Out",
description:
"Parallax the element as it scrolls out of view.",
keyframes: [
{
offset: 1,
styles: {
translate: {
type: "tuple",
value: [
{ type: "unit", unit: "number", value: 0 },
{ type: "unit", unit: "px", value: -100 },
],
},
},
},
],
timing: {
easing: "linear",
fill: "forwards",
rangeStart: [
"cover",
{ type: "unit", value: 50, unit: "%" },
],
rangeEnd: [
"cover",
{ type: "unit", value: 100, unit: "%" },
],
},
},
],
isPinned: false,
}}
>
<Heading className={`w-heading c1pdroxx cudat22`}>
{"ANIMATED CHILD 0"}
</Heading>
<Heading className={`w-heading`}>{"ANIMATED CHILD 1"}</Heading>
</AnimateChildren>
</Box>
<Box className={`w-box`}>
{
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. "
}
</Box>
</Box>
<Box className={`w-box cq3mp4w`} />
<Box className={`w-box c1y8ynw5 c2gdfrb c1yk3skc`}>
<Box className={`w-box`}>
<AnimateChildren
action={{
type: "view",
animations: [
{
name: "Fade In",
description: "Fade in the element as it scrolls into view.",
keyframes: [
{
offset: 0,
styles: {
opacity: { type: "unit", unit: "number", value: 0 },
},
},
],
timing: {
easing: "linear",
fill: "backwards",
rangeStart: [
"entry",
{ type: "unit", value: 0, unit: "%" },
],
rangeEnd: [
"entry",
{ type: "unit", value: 100, unit: "%" },
],
},
},
{
name: "Fade Out",
description:
"Fade out the element as it scrolls out of view.",
keyframes: [
{
offset: 1,
styles: {
opacity: { type: "unit", unit: "number", value: 0 },
},
},
],
timing: {
easing: "linear",
fill: "forwards",
rangeStart: ["exit", { type: "unit", value: 0, unit: "%" }],
rangeEnd: ["exit", { type: "unit", value: 100, unit: "%" }],
},
},
{
name: "Parallax In",
description:
"Parallax the element as it scrolls out of view.",
keyframes: [
{
offset: 0,
styles: {
translate: {
type: "tuple",
value: [
{ type: "unit", unit: "number", value: 0 },
{ type: "unit", unit: "px", value: 100 },
],
},
},
},
],
timing: {
easing: "linear",
fill: "backwards",
rangeStart: [
"cover",
{ type: "unit", value: 0, unit: "%" },
],
rangeEnd: ["cover", { type: "unit", value: 50, unit: "%" }],
},
},
{
name: "Parallax Out",
description:
"Parallax the element as it scrolls out of view.",
keyframes: [
{
offset: 1,
styles: {
translate: {
type: "tuple",
value: [
{ type: "unit", unit: "number", value: 0 },
{ type: "unit", unit: "px", value: -100 },
],
},
},
},
],
timing: {
easing: "linear",
fill: "forwards",
rangeStart: [
"cover",
{ type: "unit", value: 50, unit: "%" },
],
rangeEnd: [
"cover",
{ type: "unit", value: 100, unit: "%" },
],
},
},
],
isPinned: false,
}}
>
<AnimateText charWindow={20}>
<Heading className={`w-heading c1pdroxx`}>
{"ANIMATED CHILD 1"}
</Heading>
<Box className={`w-box`}>
{
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. "
}
</Box>
</AnimateText>
</AnimateChildren>
</Box>
<Box className={`w-box`}>
{
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. "
}
</Box>
</Box>
<Box className={`w-box c1m04i8w c13v7j50 cpjvam0 cqkqnmd`}>
<Heading className={`w-heading`}>{"THE END"}</Heading>
</Box>
</Body>
);
};
export { Page };

@ -411,4 +411,45 @@
.c1a2hnxl {
background-color: rgba(2, 139, 250, 1);
}
.c1m04i8w {
height: 100dvh;
}
}
@media all and (min-width: 472px) {
.c1jgcte3 {
max-width: 900px;
}
.cncutr5 {
width: 100%;
}
.c1ywt30e {
min-width: 0px;
}
.cqvurle {
justify-self: center;
}
.c1y8ynw5 {
display: grid;
}
.c2gdfrb {
grid-auto-flow: column;
}
.c1yk3skc {
grid-auto-columns: 1fr;
}
.c16asro7 {
align-items: start;
}
.c1pt69cw {
align-content: start;
}
.cq3mp4w {
height: 40dvh;
}
.c1pdroxx {
margin-top: 0em;
}
.cudat22 {
margin-bottom: 0em;
}
}

@ -0,0 +1,295 @@
import {
type MetaFunction,
type LinksFunction,
type LinkDescriptor,
type ActionFunctionArgs,
type LoaderFunctionArgs,
type HeadersFunction,
data,
redirect,
useLoaderData,
} from "react-router";
import {
isLocalResource,
loadResource,
loadResources,
formIdFieldName,
formBotFieldName,
} from "@webstudio-is/sdk/runtime";
import {
ReactSdkContext,
PageSettingsMeta,
PageSettingsTitle,
} from "@webstudio-is/react-sdk/runtime";
import {
Page,
siteName,
favIconAsset,
pageFontAssets,
pageBackgroundImageAssets,
} from "../__generated__/[animations]._index";
import {
getResources,
getPageMeta,
getRemixParams,
projectId,
contactEmail,
} from "../__generated__/[animations]._index.server";
import { assetBaseUrl, imageLoader } from "../constants.mjs";
import css from "../__generated__/index.css?url";
import { sitemap } from "../__generated__/$resources.sitemap.xml";
const customFetch: typeof fetch = (input, init) => {
if (typeof input !== "string") {
return fetch(input, init);
}
if (isLocalResource(input, "sitemap.xml")) {
// @todo: dynamic import sitemap ???
const response = new Response(JSON.stringify(sitemap));
response.headers.set("content-type", "application/json; charset=utf-8");
return Promise.resolve(response);
}
return fetch(input, init);
};
export const loader = async (arg: LoaderFunctionArgs) => {
const url = new URL(arg.request.url);
const host =
arg.request.headers.get("x-forwarded-host") ||
arg.request.headers.get("host") ||
"";
url.host = host;
url.protocol = "https";
const params = getRemixParams(arg.params);
const system = {
params,
search: Object.fromEntries(url.searchParams),
origin: url.origin,
};
const resources = await loadResources(
customFetch,
getResources({ system }).data
);
const pageMeta = getPageMeta({ system, resources });
if (pageMeta.redirect) {
const status =
pageMeta.status === 301 || pageMeta.status === 302
? pageMeta.status
: 302;
throw redirect(pageMeta.redirect, status);
}
// typecheck
arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;
if (arg.context.EXCLUDE_FROM_SEARCH) {
pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;
}
return data(
{
host,
url: url.href,
system,
resources,
pageMeta,
},
// No way for current information to change, so add cache for 10 minutes
// In case of CRM Data, this should be set to 0
{
status: pageMeta.status,
headers: {
"Cache-Control": "public, max-age=600",
},
}
);
};
export const headers: HeadersFunction = () => {
return {
"Cache-Control": "public, max-age=0, must-revalidate",
};
};
export const meta: MetaFunction<typeof loader> = ({ data }) => {
const metas: ReturnType<MetaFunction> = [];
if (data === undefined) {
return metas;
}
const origin = `https://${data.host}`;
if (siteName) {
metas.push({
"script:ld+json": {
"@context": "https://schema.org",
"@type": "WebSite",
name: siteName,
url: origin,
},
});
}
return metas;
};
export const links: LinksFunction = () => {
const result: LinkDescriptor[] = [];
result.push({
rel: "stylesheet",
href: css,
});
if (favIconAsset) {
result.push({
rel: "icon",
href: imageLoader({
src: `${assetBaseUrl}${favIconAsset}`,
// width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search
width: 144,
height: 144,
fit: "pad",
quality: 100,
format: "auto",
}),
type: undefined,
});
}
for (const asset of pageFontAssets) {
result.push({
rel: "preload",
href: `${assetBaseUrl}${asset}`,
as: "font",
crossOrigin: "anonymous",
});
}
for (const backgroundImageAsset of pageBackgroundImageAssets) {
result.push({
rel: "preload",
href: `${assetBaseUrl}${backgroundImageAsset}`,
as: "image",
});
}
return result;
};
const getRequestHost = (request: Request): string =>
request.headers.get("x-forwarded-host") || request.headers.get("host") || "";
export const action = async ({
request,
context,
}: ActionFunctionArgs): Promise<
{ success: true } | { success: false; errors: string[] }
> => {
try {
const url = new URL(request.url);
url.host = getRequestHost(request);
const formData = await request.formData();
const system = {
params: {},
search: {},
origin: url.origin,
};
const resourceName = formData.get(formIdFieldName);
let resource =
typeof resourceName === "string"
? getResources({ system }).action.get(resourceName)
: undefined;
const formBotValue = formData.get(formBotFieldName);
if (formBotValue == null || typeof formBotValue !== "string") {
throw new Error("Form bot field not found");
}
const submitTime = parseInt(formBotValue, 16);
// Assumes that the difference between the server time and the form submission time,
// including any client-server time drift, is within a 5-minute range.
// Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.
// Example: `formBotValue: jsdom`, or `formBotValue: headless-env`
if (
Number.isNaN(submitTime) ||
Math.abs(Date.now() - submitTime) > 1000 * 60 * 5
) {
throw new Error(`Form bot value invalid ${formBotValue}`);
}
formData.delete(formIdFieldName);
formData.delete(formBotFieldName);
if (resource) {
resource.headers.push({
name: "Content-Type",
value: "application/json",
});
resource.body = Object.fromEntries(formData);
} else {
if (contactEmail === undefined) {
throw new Error("Contact email not found");
}
resource = context.getDefaultActionResource?.({
url,
projectId,
contactEmail,
formData,
});
}
if (resource === undefined) {
throw Error("Resource not found");
}
const { ok, statusText } = await loadResource(fetch, resource);
if (ok) {
return { success: true };
}
return { success: false, errors: [statusText] };
} catch (error) {
console.error(error);
return {
success: false,
errors: [error instanceof Error ? error.message : "Unknown error"],
};
}
};
const Outlet = () => {
const { system, resources, url, pageMeta, host } =
useLoaderData<typeof loader>();
return (
<ReactSdkContext.Provider
value={{
imageLoader,
assetBaseUrl,
resources,
}}
>
{/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
<Page key={url} system={system} />
<PageSettingsMeta
url={url}
pageMeta={pageMeta}
host={host}
siteName={siteName}
imageLoader={imageLoader}
/>
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
</ReactSdkContext.Provider>
);
};
export default Outlet;

@ -6,7 +6,7 @@
"dev": "react-router dev",
"cli": "NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio",
"fixtures:link": "pnpm cli link --link https://p-cddc1d44-af37-4cb6-a430-d300cf6f932d-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=1cdc6026-dd5b-4624-b89b-9bd45e9bcc3d'",
"fixtures:sync": "pnpm cli sync --buildId 6f14cdae-073c-4f2c-b535-da3f404f3e36 && pnpm prettier --write ./.webstudio/",
"fixtures:sync": "pnpm cli sync --buildId 342a7c73-ed72-441d-8a69-8b2659639e93 && pnpm prettier --write ./.webstudio/",
"fixtures:build": "pnpm cli build --template react-router --template ./.template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json"
},
"private": true,

@ -89,8 +89,7 @@
"css-tree@2.3.1": "patches/css-tree@2.3.1.patch",
"@types/css-tree@2.3.1": "patches/@types__css-tree@2.3.1.patch",
"@radix-ui/react-scroll-area@1.0.5": "patches/@radix-ui__react-scroll-area@1.0.5.patch",
"@remix-run/dev": "patches/@remix-run__dev.patch",
"scroll-timeline-polyfill@1.1.0": "patches/scroll-timeline-polyfill@1.1.0.patch"
"@remix-run/dev": "patches/@remix-run__dev.patch"
}
}
}

@ -3,6 +3,7 @@ import { readFile, rm } from "node:fs/promises";
import type { WsComponentMeta } from "@webstudio-is/sdk";
import { generateRemixRoute, namespaceMeta } from "@webstudio-is/react-sdk";
import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas";
import * as animationComponentMetas from "@webstudio-is/sdk-components-animation/metas";
import * as reactRouterComponentMetas from "@webstudio-is/sdk-components-react-router/metas";
import * as radixComponentMetas from "@webstudio-is/sdk-components-react-radix/metas";
import type { Framework } from "./framework";
@ -40,12 +41,26 @@ export const createFramework = async (): Promise<Framework> => {
);
}
const animationComponentNamespacedMetas: Record<string, WsComponentMeta> = {};
for (const [name, meta] of Object.entries(animationComponentMetas)) {
const namespace = "@webstudio-is/sdk-components-animation";
animationComponentNamespacedMetas[`${namespace}:${name}`] = namespaceMeta(
meta as WsComponentMeta,
namespace,
new Set(Object.keys(animationComponentMetas))
);
}
return {
components: [
{
source: "@webstudio-is/sdk-components-react",
metas: baseComponentMetas,
},
{
source: "@webstudio-is/sdk-components-animation",
metas: animationComponentNamespacedMetas,
},
{
source: "@webstudio-is/sdk-components-react-radix",
metas: radixComponentNamespacedMetas,

@ -2,5 +2,8 @@
"dependencies": {
"worktop": "0.8.0-next.18",
"zod": "^3.22.4"
},
"devDependencies": {
"fast-glob": "^3.3.2"
}
}

@ -0,0 +1,48 @@
import {
vitePlugin as remix,
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
import { existsSync } from "node:fs";
// @ts-ignore
import path from "node:path";
// @ts-ignore
import fg from "fast-glob";
const rootDir = ["..", "../..", "../../.."]
.map((dir) => path.join(__dirname, dir))
.find((dir) => existsSync(path.join(dir, ".git")));
const hasPrivateFolders =
fg.sync([path.join(rootDir ?? "", "packages/*/private-src/*")], {
ignore: ["**/node_modules/**"],
}).length > 0;
const conditions = hasPrivateFolders
? ["webstudio-private", "webstudio"]
: ["webstudio"];
export default defineConfig(({ mode }) => ({
resolve: {
conditions,
},
ssr: {
resolve: {
conditions,
},
},
plugins: [
// without this, remixCloudflareDevProxy trying to load workerd even for production (it's not needed for production)
mode === "production" ? undefined : remixCloudflareDevProxy(),
remix({
future: {
v3_lazyRouteDiscovery: false,
v3_relativeSplatPath: false,
v3_singleFetch: false,
v3_fetcherPersist: false,
v3_throwAbortReason: false,
},
}),
].filter(Boolean),
}));

@ -31,9 +31,6 @@ const isExternal = (id: string, importer: string | undefined) => {
export default defineConfig({
// resolve only webstudio condition in tests
resolve: {
conditions: ["webstudio"],
},
build: {
minify: false,
lib: {

@ -7,7 +7,7 @@
"type": "module",
"scripts": {
"typecheck": "tsc",
"build": "rm -rf lib && esbuild src/index.ts --outdir=lib --bundle --format=esm --packages=external",
"build": "rm -rf lib && esbuild src/index.ts --outdir=lib --bundle --format=esm --packages=external && esbuild src/runtime.ts --outdir=lib --bundle --format=esm --packages=external",
"dts": "tsc --project tsconfig.dts.json",
"test": "vitest run"
},
@ -25,9 +25,16 @@
"vitest": "^3.0.4"
},
"exports": {
"webstudio": "./src/index.ts",
"types": "./lib/types/index.d.ts",
"import": "./lib/index.js"
".": {
"webstudio": "./src/index.ts",
"types": "./lib/types/index.d.ts",
"import": "./lib/index.js"
},
"./runtime": {
"webstudio": "./src/runtime.ts",
"types": "./lib/types/runtime.d.ts",
"import": "./lib/runtime.js"
}
},
"files": [
"lib/*",

@ -0,0 +1 @@
export { toValue } from "./core/to-value";

@ -61,7 +61,6 @@
"change-case": "^5.4.4",
"nanostores": "^0.11.3",
"react-error-boundary": "^5.0.0",
"scroll-timeline-polyfill": "^1.1.0",
"shallow-equal": "^3.1.0"
},
"devDependencies": {

@ -1,11 +1,17 @@
import type { PropMeta } from "@webstudio-is/sdk";
export const props: Record<string, PropMeta> = {
charWindow: { required: true, control: "number", type: "number" },
charWindow: {
required: false,
control: "number",
type: "number",
defaultValue: 5,
},
easing: {
required: true,
required: false,
control: "select",
type: "string",
defaultValue: "linear",
options: [
"linear",
"easeIn",

@ -14,13 +14,13 @@ const easings = {
};
type AnimateChildrenProps = {
charWindow: number;
easing: keyof typeof easings;
charWindow?: number;
easing?: keyof typeof easings;
children: React.ReactNode;
};
export const AnimateText = forwardRef<ElementRef<"div">, AnimateChildrenProps>(
({ charWindow: _, easing: __, ...props }, ref) => {
({ charWindow = 5, easing = "linear", ...props }, ref) => {
return <div ref={ref} {...props} />;
}
);

@ -6,6 +6,7 @@
"private-src",
"../sdk/src/schema/animation-schema.ts"
],
"exclude": ["private-src/perf/**/*"],
"compilerOptions": {
"types": ["react/experimental", "react-dom/experimental", "@types/node"]
}

@ -1,25 +0,0 @@
diff --git a/src/scroll-timeline-base.js b/src/scroll-timeline-base.js
index 2c854928701c5dae23c985eb04f73a6ca2b661c3..abf1a7629568f411dd83dd509640850706d59528 100644
--- a/src/scroll-timeline-base.js
+++ b/src/scroll-timeline-base.js
@@ -162,13 +162,19 @@ function isValidAxis(axis) {
*/
export function measureSource (source) {
const style = getComputedStyle(source);
+ const clientHeight = source.clientHeight;
return {
scrollLeft: source.scrollLeft,
scrollTop: source.scrollTop,
scrollWidth: source.scrollWidth,
scrollHeight: source.scrollHeight,
clientWidth: source.clientWidth,
- clientHeight: source.clientHeight,
+ get clientHeight() {
+ if (document.scrollingElement === source) {
+ return window.innerHeight;
+ }
+ return clientHeight;
+ },
writingMode: style.writingMode,
direction: style.direction,
scrollPaddingTop: style.scrollPaddingTop,

14
pnpm-lock.yaml generated

@ -26,9 +26,6 @@ patchedDependencies:
css-tree@2.3.1:
hash: epgcmebti7rfrc2ej4odb3t4jy
path: patches/css-tree@2.3.1.patch
scroll-timeline-polyfill@1.1.0:
hash: i4g3vdpump4efgy2hri5l5rsfm
path: patches/scroll-timeline-polyfill@1.1.0.patch
importers:
@ -854,6 +851,9 @@ importers:
'@types/react-dom':
specifier: ^18.2.25
version: 18.2.25
fast-glob:
specifier: ^3.3.2
version: 3.3.2
typescript:
specifier: 5.7.3
version: 5.7.3
@ -1871,9 +1871,6 @@ importers:
react-error-boundary:
specifier: ^5.0.0
version: 5.0.0(react@18.3.0-canary-14898b6a9-20240318)
scroll-timeline-polyfill:
specifier: ^1.1.0
version: 1.1.0(patch_hash=i4g3vdpump4efgy2hri5l5rsfm)
shallow-equal:
specifier: ^3.1.0
version: 3.1.0
@ -8497,9 +8494,6 @@ packages:
scheduler@0.24.0-canary-14898b6a9-20240318:
resolution: {integrity: sha512-ifDO3bUdooS4OlxvGxMyoDEC/aq14MvJLDd0thjrUSZGeLJA7WBc+sr9NZxIxrXfVqMl1GTGGPwXqRJZDNW76w==}
scroll-timeline-polyfill@1.1.0:
resolution: {integrity: sha512-BpL3gk3Ynt/5VYaDFUNUP/FTkDldwKQnWcA07g/mDHkMVS9pQUyUXBpsy4RZYAgsfFeI1tWcnPNrEFtCpQoO9Q==}
selfsigned@2.1.1:
resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==}
engines: {node: '>=10'}
@ -16559,8 +16553,6 @@ snapshots:
dependencies:
loose-envify: 1.4.0
scroll-timeline-polyfill@1.1.0(patch_hash=i4g3vdpump4efgy2hri5l5rsfm): {}
selfsigned@2.1.1:
dependencies:
node-forge: 1.3.1

@ -1,17 +1,19 @@
import { defineConfig } from "vite";
import { existsSync } from "node:fs";
import path from "node:path";
const hasPrivateFolders = existsSync(
path.join(process.cwd(), "private-src", "README.md")
);
const isBareImport = (id: string) =>
id.startsWith("@") || id.includes(".") === false;
export default defineConfig({
// resolve only webstudio condition in tests
resolve: {
conditions: ["webstudio"],
},
build: {
lib: {
entry: [
"src/components.ts",
hasPrivateFolders ? "private-src/components.ts" : "src/components.ts",
"src/metas.ts",
"src/props.ts",
"src/hooks.ts",
@ -21,11 +23,20 @@ export default defineConfig({
},
rollupOptions: {
external: isBareImport,
output: {
preserveModules: true,
preserveModulesRoot: "src",
dir: "lib",
},
output: [
{
preserveModules: true,
preserveModulesRoot: "src",
dir: "lib",
},
hasPrivateFolders
? {
preserveModules: true,
preserveModulesRoot: "private-src",
dir: "lib",
}
: undefined,
].filter((output) => output !== undefined),
},
},
});