Merge branch 'main' into 494-primitive-tokens

This commit is contained in:
Oleg Isonen authored and GitHub committed 2022-11-27 18:41:41 +01:00
commit 4fcbf1d108
67 files changed
+1124 -283

No files matched your search

+7
View File
@@ -4,6 +4,9 @@ import type { Build, CanvasData } from "@webstudio-is/project";
import {
createElementsTree,
useAllUserProps,
registerComponents,
customComponents,
setParams,
type OnChangeChildren,
type Tree,
} from "@webstudio-is/react-sdk";
@@ -139,6 +142,10 @@ export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
const assets = useAssets(data.assets);
useAllUserProps(data.props);
usePopulateRootInstance(data.tree);
setParams(data.params ?? null);
registerComponents(customComponents);
// e.g. toggling preview is still needed in both modes
useShortcuts();
const isPreviewMode = useSubscribePreviewMode();
@@ -32,22 +32,20 @@ const $writeUpdates = (
const children = node.getChildren();
for (const child of children) {
if ($isParagraphNode(child)) {
if (updates.length !== 0) {
updates.push("\n");
}
$writeUpdates(child, updates, refs);
}
if ($isLineBreakNode(child)) {
if (updates.length !== 0) {
// @todo should we visually distinct line breaks and paragraphs?
updates.push("\n");
}
updates.push("\n");
}
if ($isLinkNode(child)) {
const id = refs.get(child.getKey());
const childrenUpdates: ChildrenUpdates = [];
$writeUpdates(child, childrenUpdates, refs);
updates.push({ id, component: "Link", children: childrenUpdates });
updates.push({
id,
component: "RichTextLink",
children: childrenUpdates,
});
}
if ($isTextNode(child)) {
// support nesting bold into italic and vice versa
@@ -115,7 +113,7 @@ const $writeLexical = (
}
// convert instances
if (child.component === "Link" && $isElementNode(parent)) {
if (child.component === "RichTextLink" && $isElementNode(parent)) {
const linkNode = $createLinkNode("");
refs.set(linkNode.getKey(), child.id);
parent.append(linkNode);
@@ -1,4 +1,14 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useLayoutEffect } from "react";
import {
KEY_ENTER_COMMAND,
INSERT_LINE_BREAK_COMMAND,
COMMAND_PRIORITY_EDITOR,
RootNode,
ElementNode,
$createLineBreakNode,
$getSelection,
$isRangeSelection,
} from "lexical";
import { LinkNode } from "@lexical/link";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
@@ -39,6 +49,65 @@ const AutofocusPlugin = () => {
return null;
};
const RemoveParagaphsPlugin = () => {
const [editor] = useLexicalComposerContext();
// register own commands before RichTextPlugin
// to stop propagation
useLayoutEffect(() => {
const removeCommand = editor.registerCommand(
KEY_ENTER_COMMAND,
(event) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
event?.preventDefault();
// returns true which stops propagation
return editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false);
},
COMMAND_PRIORITY_EDITOR
);
// merge pasted paragraphs into single one
// and separate lines with line breaks
const removeNodeTransform = editor.registerNodeTransform(
RootNode,
(node) => {
// merge paragraphs into first with line breaks between
if (node.getChildrenSize() > 1) {
const children = node.getChildren();
let first;
for (let index = 0; index < children.length; index += 1) {
const paragraph = children[index];
// With default configuration root contains only paragraphs.
// Lexical converts headings to paragraphs on paste for example.
// So he we just check root children which are all paragraphs.
if (paragraph instanceof ElementNode) {
if (index === 0) {
first = paragraph;
} else if (first) {
first.append($createLineBreakNode());
for (const child of paragraph.getChildren()) {
first.append(child);
}
paragraph.remove();
}
}
}
}
}
);
return () => {
removeCommand();
removeNodeTransform();
};
}, [editor]);
return null;
};
const onError = (error: Error) => {
throw error;
};
@@ -54,12 +123,16 @@ export const TextEditor = ({
contentEditable,
onChange,
}: TextEditorProps) => {
const [paragraphClassName] = useState(() => nanoid());
const [italicClassName] = useState(() => nanoid());
// initially instance styles are applied to all nodes
// so not necessary to use layout effect
useEffect(() => {
useLayoutEffect(() => {
const engine = createCssEngine();
// reset paragraph styles and make it work inside <a>
engine.addPlaintextRule(`
.${paragraphClassName} { display: inline; margin: 0; }
`);
/// set italic style for bold italic combination on the same element
engine.addPlaintextRule(`
.${italicClassName} { font-style: italic; }
`);
@@ -67,7 +140,7 @@ export const TextEditor = ({
return () => {
engine.unmount();
};
}, [italicClassName]);
}, [paragraphClassName, italicClassName]);
// store references separately because lexical nodes
// cannot store custom data
@@ -76,6 +149,7 @@ export const TextEditor = ({
const initialConfig = {
namespace: "WsTextEditor",
theme: {
paragraph: paragraphClassName,
text: {
italic: italicClassName,
},
@@ -93,6 +167,7 @@ export const TextEditor = ({
return (
<LexicalComposer initialConfig={initialConfig}>
<AutofocusPlugin />
<RemoveParagaphsPlugin />
<ToolbarConnectorPlugin />
<BindInstanceToNodePlugin refs={refs} />
<RichTextPlugin
@@ -4,7 +4,7 @@ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext
import {
useUserProps,
renderWrapperComponentChildren,
components,
getComponent,
type Instance,
type OnChangeChildren,
idAttribute,
@@ -66,7 +66,7 @@ export const WrapperComponentDev = ({
const readonlyProps =
instance.component === "Input" ? { readOnly: true } : undefined;
const { Component } = components[instance.component];
const Component = getComponent(instance.component);
const props = {
...userProps,
@@ -80,7 +80,10 @@ export const WrapperComponentDev = ({
"data-ws-component": instance.component,
[idAttribute]: instance.id,
onClick: (event: MouseEvent) => {
if (instance.component === "Link") {
if (
instance.component === "Link" ||
instance.component === "RichTextLink"
) {
event.preventDefault();
}
},
+2 -2
View File
@@ -4,7 +4,7 @@ import {
type Instance,
type InstanceProps,
type Tree,
components,
getComponentMeta,
allUserPropsContainer,
getBrowserStyle,
useAllUserProps,
@@ -79,7 +79,7 @@ export const findInsertLocation = (
path.reverse();
const parentIndex = path.findIndex(
({ item }) => components[item.component].canAcceptChildren
({ item }) => getComponentMeta(item.component).canAcceptChildren
);
// Just in case selected Instance is not in the tree for some reason.
@@ -55,10 +55,6 @@ const helperStyles = [
`[${idAttribute}][contenteditable] {
box-shadow: 0 0 0 4px rgb(36 150 255 / 20%)
}`,
// Text Editor wraps each line into a p, so we need to make sure there is no jump between regular rendering and editing
`[${idAttribute}][contenteditable] p {
margin: 0
}`,
];
export const useManageDesignModeStyles = () => {
@@ -18,7 +18,7 @@ import {
} from "@webstudio-is/design-system";
import {
type Instance,
components,
getComponentMeta,
BaseInstance,
toBaseInstance,
} from "@webstudio-is/react-sdk";
@@ -116,7 +116,7 @@ export const useDragAndDrop = () => {
}
const data = path.find(
(instance) => components[instance.component].canAcceptChildren
(instance) => getComponentMeta(instance.component).canAcceptChildren
);
if (data === undefined) {
@@ -179,7 +179,7 @@ export const useDragAndDrop = () => {
}
// When trying to drag an inline instance, drag its parent instead
if (components[instance.component].isInlineOnly) {
if (getComponentMeta(instance.component).isInlineOnly) {
const nonInlineParent = utils.tree.findClosestNonInlineParent(
rootInstance,
instance.id
@@ -1,6 +1,6 @@
import { useHotkeys } from "react-hotkeys-hook";
import store from "immerhin";
import { type Instance, components } from "@webstudio-is/react-sdk";
import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk";
import { shortcuts, options } from "~/shared/shortcuts";
import { publish, useSubscribe } from "~/shared/pubsub";
import { useSelectedInstance } from "./nano-states";
@@ -120,7 +120,9 @@ export const useShortcuts = () => {
"enter",
(event) => {
if (selectedInstance === undefined) return;
const { isContentEditable } = components[selectedInstance.component];
const { isContentEditable } = getComponentMeta(
selectedInstance.component
);
if (isContentEditable === false) return;
// Prevents inserting a newline when entering text-editing mode
event.preventDefault();
@@ -1,5 +1,9 @@
import { useCallback, useEffect, useRef } from "react";
import { type Instance, components } from "@webstudio-is/react-sdk";
import {
type Instance,
getComponentMeta,
getComponentNames,
} from "@webstudio-is/react-sdk";
import { useSubscribe } from "~/shared/pubsub";
import { useSelectedElement, useSelectedInstance } from "./nano-states";
import {
@@ -87,14 +91,15 @@ export const useTrackSelectedElement = () => {
if (editingInstanceIdRef.current === id) {
return;
}
const components = getComponentNames();
// It's the second click in a double click.
if (event.detail === 2) {
const component = dataset.wsComponent as Instance["component"];
if (component === undefined || component in components === false) {
if (component === undefined || !components.includes(component)) {
return;
}
const { isInlineOnly, isContentEditable } = components[component];
const { isInlineOnly, isContentEditable } = getComponentMeta(component);
// When user double clicks on an inline instance, we need to select the parent instance and put it indo text editing mode.
// Inline instances are not editable directly, only through parent instance.
@@ -102,7 +107,7 @@ export const useTrackSelectedElement = () => {
const parent =
rootInstance &&
utils.tree.findClosestNonInlineParent(rootInstance, id);
if (parent && components[parent.component].isContentEditable) {
if (parent && getComponentMeta(parent.component).isContentEditable) {
selectInstance(parent.id);
setEditingInstanceId(parent.id);
}
@@ -9,10 +9,14 @@ import {
Text,
Flex,
} from "@webstudio-is/design-system";
import { User } from "@webstudio-is/prisma-client";
import { User as DbUser } from "@webstudio-is/prisma-client";
import { useNavigate } from "react-router-dom";
import { logoutPath } from "~/shared/router-utils";
type User = Omit<DbUser, "createdAt"> & {
createdAt: string;
};
export const DashboardHeader = ({ user }: { user: User }) => {
const navigate = useNavigate();
const userNameFallback = (user?.username || user?.email || "X")
+6 -1
View File
@@ -3,9 +3,10 @@ import { useActionData } from "@remix-run/react";
import { Flex } from "@webstudio-is/design-system";
import interStyles from "~/shared/font-faces/inter.css";
import dashboardStyles from "./dashboard.css";
import { User } from "@webstudio-is/prisma-client";
import { User as DbUser } from "@webstudio-is/prisma-client";
import { DashboardHeader } from "./components/header";
import { SelectProjectCard } from "./components/card";
export const links = () => {
return [
{
@@ -19,6 +20,10 @@ export const links = () => {
];
};
type User = Omit<DbUser, "createdAt"> & {
createdAt: string;
};
type DashboardProps = {
projects?: Array<{ id: string; title: string }>;
user: User;
@@ -1,5 +1,5 @@
import {
componentsMeta,
getComponentMetaProps,
type Instance,
type UserProp,
} from "@webstudio-is/react-sdk";
@@ -13,7 +13,6 @@ import {
Switch,
TextField,
} from "@webstudio-is/design-system";
import { isFeatureEnabled } from "~/shared/feature-flags";
import { ValuePickerPopover } from "../style-panel/shared/value-picker-popover";
import { ImageManager } from "~/designer/shared/image-manager";
@@ -183,11 +182,13 @@ export function Control({
userProp,
onChangePropValue,
}: ControlProps) {
const meta = componentsMeta[component];
const argType = meta[userProp.prop as keyof typeof meta];
const meta = getComponentMetaProps(component);
const defaultValue = argType.defaultValue;
const type = argType.type;
const argType = meta[userProp.prop];
// argType can be undefined in case of new property created
const defaultValue = argType?.defaultValue ?? "";
const type = argType?.type ?? "text";
if (type == null) {
warnOnce(
@@ -208,17 +209,15 @@ export function Control({
return <NotImplemented />;
}
if (isFeatureEnabled("assets")) {
if (component === "Image" && userProp.prop === "src") {
const asset = userProp.asset ?? null;
if (component === "Image" && userProp.prop === "src") {
const asset = userProp.asset ?? null;
return (
<ImageControl
asset={asset}
onChange={(asset) => onChangePropValue(asset.path, asset)}
/>
);
}
return (
<ImageControl
asset={asset}
onChange={(asset) => onChangePropValue(asset.path, asset)}
/>
);
}
if (includes(textControlTypes, type)) {
@@ -242,12 +241,12 @@ export function Control({
}
if (
argType.type === "radio" ||
argType.type === "inline-radio" ||
argType.type === "check" ||
argType.type === "inline-check" ||
argType.type === "multi-select" ||
argType.type === "select"
argType?.type === "radio" ||
argType?.type === "inline-radio" ||
argType?.type === "check" ||
argType?.type === "inline-check" ||
argType?.type === "multi-select" ||
argType?.type === "select"
) {
const options = argType.options;
@@ -1,5 +1,5 @@
import { ComponentStoryObj } from "@storybook/react";
import { componentsMeta, type UserProp } from "@webstudio-is/react-sdk";
import { getComponentMetaProps, type UserProp } from "@webstudio-is/react-sdk";
import { PropsPanel } from "./props-panel";
export default {
@@ -64,7 +64,7 @@ export const DefaultProps: ComponentStoryObj<typeof PropsPanel> = {
},
};
const meta = componentsMeta["Button"];
const meta = getComponentMetaProps("Button");
export const AllProps: ComponentStoryObj<typeof PropsPanel> = {
args: {
@@ -81,7 +81,7 @@ export const AllProps: ComponentStoryObj<typeof PropsPanel> = {
return {
id: prop,
prop,
value: value.defaultValue,
value: value?.defaultValue ?? "",
} as UserProp;
}),
},
@@ -1,5 +1,5 @@
import {
componentsMeta,
getComponentMetaProps,
type Instance,
type UserProp,
} from "@webstudio-is/react-sdk";
@@ -121,7 +121,7 @@ const Property = ({
onChangePropValue,
onDelete,
}: PropertyProps) => {
const meta = componentsMeta[component];
const meta = getComponentMetaProps(component);
const argType = meta[userProp.prop as keyof typeof meta];
const isInvalid =
@@ -142,7 +142,8 @@ const Property = ({
const argType = meta[name as keyof typeof meta];
const defaultValue =
argType?.defaultValue ?? argType?.type === "boolean" ? false : "";
argType?.defaultValue ??
(argType?.type === "boolean" ? false : "");
onChangePropName(name, defaultValue);
}
@@ -151,7 +152,7 @@ const Property = ({
const argType = meta[name as keyof typeof meta];
const defaultValue =
argType?.defaultValue ?? argType?.type === "boolean" ? false : "";
argType?.defaultValue ?? (argType?.type === "boolean" ? false : "");
onChangePropName(name, defaultValue);
}}
@@ -1,11 +1,11 @@
import { renderHook, act } from "@testing-library/react-hooks";
import { components, UserProp } from "@webstudio-is/react-sdk";
import { type ComponentName, UserProp } from "@webstudio-is/react-sdk";
import { nanoid } from "nanoid";
import type { SelectedInstanceData } from "@webstudio-is/project";
import { usePropsLogic } from "./use-props-logic";
const getSelectedInstanceData = (
componentName: keyof typeof components,
componentName: ComponentName,
props: UserProp[]
): SelectedInstanceData => {
return {
@@ -350,7 +350,7 @@ describe("usePropsLogic", () => {
name: "string",
description: "string",
location: "REMOTE",
createdAt: new Date("1995-12-17T03:24:00Z"),
createdAt: new Date("1995-12-17T03:24:00Z").toISOString(),
meta: { width: 101, height: 202 },
path: "string",
status: "uploaded",
@@ -361,7 +361,7 @@ describe("usePropsLogic", () => {
[
{
"asset": {
"createdAt": 1995-12-17T03:24:00.000Z,
"createdAt": "1995-12-17T03:24:00.000Z",
"description": "string",
"format": "string",
"id": "string",
@@ -4,7 +4,7 @@ import type {
UserPropsUpdates,
} from "@webstudio-is/react-sdk";
import { type Publish } from "~/shared/pubsub";
import { componentsMeta } from "@webstudio-is/react-sdk";
import { getComponentMetaProps } from "@webstudio-is/react-sdk";
import ObjectId from "bson-objectid";
import produce from "immer";
import uniqBy from "lodash/uniqBy";
@@ -37,9 +37,9 @@ const getRequiredProps = (
selectedInstanceData: SelectedInstanceData
): UserProp[] => {
const { component } = selectedInstanceData;
const meta = componentsMeta[component];
const meta = getComponentMetaProps(component);
return Object.entries(meta)
.filter(([_, value]) => value.required)
.filter(([_, value]) => value?.required)
.map(([prop, _]) => ({
id: ObjectId().toString(),
prop,
@@ -54,12 +54,11 @@ const getPropsWithDefaultValue = (
selectedInstanceData: SelectedInstanceData
): UserProp[] => {
const { component } = selectedInstanceData;
const meta = componentsMeta[component];
const meta = getComponentMetaProps(component);
return Object.entries(meta)
.filter(([_, value]) => value.defaultValue != null)
.filter(([_, value]) => value?.defaultValue != null)
.map(([prop, propObj]) => {
const { defaultValue } = propObj;
const value = defaultValue ?? "";
const value = propObj?.defaultValue ?? "";
return {
id: ObjectId().toString(),
prop,
@@ -1,4 +1,4 @@
import { type Instance, components } from "@webstudio-is/react-sdk";
import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk";
import { forwardRef, type ElementRef, type ComponentProps } from "react";
import { Flex, Text, styled } from "@webstudio-is/design-system";
@@ -30,7 +30,7 @@ export const ComponentThumb = forwardRef<
ElementRef<typeof Thumb>,
ComponentThumbProps
>(({ component, ...rest }, ref) => {
const { Icon, label } = components[component];
const { Icon, label } = getComponentMeta(component);
return (
<Thumb
direction="column"
@@ -1,6 +1,11 @@
import { type MouseEventHandler, useState } from "react";
import { createPortal } from "react-dom";
import { type Instance, components } from "@webstudio-is/react-sdk";
import {
type Instance,
type ComponentName,
getComponentMeta,
getComponentNames,
} from "@webstudio-is/react-sdk";
import { useSubscribe, type Publish } from "~/shared/pubsub";
import { Flex, useDrag, type Point } from "@webstudio-is/design-system";
import { PlusIcon } from "@webstudio-is/icons";
@@ -10,10 +15,6 @@ import { ComponentThumb } from "./component-thumb";
import { useCanvasRect, useZoom } from "~/designer/shared/nano-states";
import { Header, CloseButton } from "../../lib/header";
const componentNames = (
Object.keys(components) as Array<Instance["component"]>
).filter((component) => components[component].isListed);
type DraggableThumbProps = {
onClick: MouseEventHandler<HTMLDivElement>;
component: Instance["component"];
@@ -65,7 +66,10 @@ type TabContentProps = {
publish: Publish;
};
const elementToComponentName = (element: Element) => {
const elementToComponentName = (
element: Element,
listedComponentNames: ComponentName[]
) => {
// If drag doesn't start on the button element directly but on one of its children,
// we need to trace back to the button that has the data.
const parentWithData = element.closest("[data-drag-component]");
@@ -74,7 +78,7 @@ const elementToComponentName = (element: Element) => {
return;
}
const { dragComponent } = parentWithData.dataset;
return componentNames.find((component) => component === dragComponent);
return listedComponentNames.find((component) => component === dragComponent);
};
export const TabContent = ({ publish, onSetActiveTab }: TabContentProps) => {
@@ -92,9 +96,16 @@ export const TabContent = ({ publish, onSetActiveTab }: TabContentProps) => {
return { x: (x - canvasRect.x) / scale, y: (y - canvasRect.y) / scale };
};
const listedComponentNames = getComponentNames().filter(
(name) => getComponentMeta(name).isListed
);
const useDragHandlers = useDrag<Instance["component"]>({
elementToData(element) {
const componentName = elementToComponentName(element);
const componentName = elementToComponentName(
element,
listedComponentNames
);
if (componentName === undefined) {
return false;
}
@@ -142,7 +153,7 @@ export const TabContent = ({ publish, onSetActiveTab }: TabContentProps) => {
css={{ padding: "$spacing$3", overflow: "auto" }}
ref={useDragHandlers.rootRef}
>
{componentNames.map((component: Instance["component"]) => (
{listedComponentNames.map((component: Instance["component"]) => (
<DraggableThumb
key={component}
component={component}
@@ -37,8 +37,6 @@ export const SidebarLeft = ({ publish }: SidebarLeftProps) => {
switch (panel) {
case "designTokens":
return isFeatureEnabled("designTokens");
case "assets":
return isFeatureEnabled("assets");
case "navigator":
return clientSettings.navigatorLayout === "docked";
}
@@ -32,7 +32,6 @@ import {
RowGapIcon,
ColumnGapIcon,
} from "@webstudio-is/icons";
import { isFeatureEnabled } from "~/shared/feature-flags";
import { keywordValues, properties } from "@webstudio-is/css-data";
type BaseStyleConfig = {
@@ -68,7 +67,7 @@ const getControl = (property: StyleProperty): Control => {
return "FontFamilyControl";
}
case "backgroundImage": {
return isFeatureEnabled("assets") ? "ImageControl" : "TextControl";
return "ImageControl";
}
case "fontWeight": {
return "FontWeightControl";
@@ -1,6 +1,6 @@
import { useCallback, useState } from "react";
import { styled, type Rect } from "@webstudio-is/design-system";
import { type Instance, components } from "@webstudio-is/react-sdk";
import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk";
type LabelPosition = "top" | "inside" | "bottom";
type LabelRefCallback = (element: HTMLElement | null) => void;
@@ -74,7 +74,7 @@ type LabelProps = {
export const Label = ({ component, instanceRect }: LabelProps) => {
const [labelRef, position] = useLabelPosition(instanceRect);
const { Icon, label } = components[component];
const { Icon, label } = getComponentMeta(component);
return (
<LabelContainer position={position} ref={labelRef}>
<Icon width="1em" height="1em" />
@@ -1,4 +1,4 @@
import { components } from "@webstudio-is/react-sdk";
import { getComponentMeta } from "@webstudio-is/react-sdk";
import { Flex, Text } from "@webstudio-is/design-system";
import type { SelectedInstanceData } from "@webstudio-is/project";
@@ -15,7 +15,9 @@ export const ComponentInfo = ({
color: "$colors$slate11",
fontWeight: "500",
}}
>{`Selected: ${components[selectedInstanceData.component].label}`}</Text>
>{`Selected: ${
getComponentMeta(selectedInstanceData.component).label
}`}</Text>
</Flex>
);
};
@@ -7,7 +7,7 @@ import {
type TreeProps,
type TreeItemRenderProps,
} from "@webstudio-is/design-system";
import { components, Instance } from "@webstudio-is/react-sdk";
import { getComponentMeta, Instance } from "@webstudio-is/react-sdk";
import { utils } from "@webstudio-is/project";
const instanceRelatedProps = {
@@ -15,13 +15,13 @@ const instanceRelatedProps = {
getItemPath: utils.tree.getInstancePath,
getItemPathWithPositions: utils.tree.getInstancePathWithPositions,
canLeaveParent(item: Instance) {
return components[item.component].isInlineOnly !== true;
return getComponentMeta(item.component).isInlineOnly !== true;
},
canAcceptChild(item: Instance) {
return components[item.component].canAcceptChildren;
return getComponentMeta(item.component).canAcceptChildren;
},
getItemChildren(item: Instance) {
const component = components[item.component];
const component = getComponentMeta(item.component);
// We want to avoid calling .filter() unnecessarily, because this is a hot path for performance.
// We rely on the fact that only content editable or inline components may have `string` children.
@@ -37,7 +37,7 @@ const instanceRelatedProps = {
) as Instance[];
},
renderItem(props: TreeItemRenderProps<Instance>) {
const { Icon, label } = components[props.itemData.component];
const { Icon, label } = getComponentMeta(props.itemData.component);
return (
<TreeItemBody {...props} selectionEvent="focus">
<TreeItemLabel prefix={<Icon />}>{label}</TreeItemLabel>
+2
View File
@@ -9,6 +9,8 @@ const env = {
PUBLISHER_ENDPOINT: process.env.PUBLISHER_ENDPOINT || null,
PUBLISHER_HOST: process.env.PUBLISHER_HOST || null,
BUILD_REQUIRE_SUBDOMAIN: process.env.BUILD_REQUIRE_SUBDOMAIN === "true",
// Must be set for Vercel deployments
RESIZE_ORIGIN: process.env.RESIZE_ORIGIN,
} as const;
export default env;
-1
View File
@@ -7,7 +7,6 @@ import { OutletProps } from "react-router-dom";
import { PersistentFetcherProvider } from "./shared/fetcher";
const RootWithErrorBoundary = (props: OutletProps) => (
// @ts-expect-error 'ErrorBoundary' cannot be used as a JSX component.
<ErrorBoundary>
<TooltipProvider>
<PersistentFetcherProvider>
+42 -55
View File
@@ -1,8 +1,8 @@
import {
redirect,
json,
type LoaderFunction,
type MetaFunction,
import { redirect, json } from "@remix-run/node";
import type {
MetaFunction,
LoaderFunction,
ErrorBoundaryComponent,
} from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { InstanceRoot, Root } from "@webstudio-is/react-sdk";
@@ -20,9 +20,7 @@ import { db } from "@webstudio-is/project/server";
import type { DynamicLinksFunction } from "remix-utils";
import type { CanvasData } from "@webstudio-is/project";
type Data =
| (CanvasData & { env: Env; mode: BuildMode })
| { errors: string; env: Env };
type Data = CanvasData & { env: Env; mode: BuildMode };
export const dynamicLinks: DynamicLinksFunction<CanvasData> = ({
data,
@@ -42,64 +40,53 @@ export const dynamicLinks: DynamicLinksFunction<CanvasData> = ({
export const handle = { dynamicLinks };
export const meta: MetaFunction = ({ data }: { data: Data }) => {
if ("errors" in data) {
return { title: "Error" };
}
const { page } = data;
return { title: page.title, ...page.meta };
};
export const loader: LoaderFunction = async ({
request,
}): Promise<Data | Response> => {
try {
const buildParams = getBuildParams(request);
export const loader: LoaderFunction = async ({ request }): Promise<Data> => {
const buildParams = getBuildParams(request);
if (buildParams === undefined) {
return redirect(dashboardPath());
}
const { mode, pathname } = buildParams;
const project = await db.project.loadByParams(buildParams);
if (project === null) {
throw json("Project not found", { status: 404 });
}
const canvasData = await loadCanvasData(
project,
mode === "published" ? "prod" : "dev",
pathname
);
if (canvasData === undefined) {
throw json("Page not found", { status: 404 });
}
return { ...canvasData, env, mode };
} catch (error) {
// If a Response is thrown, we're rethrowing it for Remix to handle.
// https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders
if (error instanceof Response) {
throw error;
}
sentryException({ error });
return {
errors: error instanceof Error ? error.message : String(error),
env,
};
if (buildParams === undefined) {
throw redirect(dashboardPath());
}
const { mode, pathname } = buildParams;
const project = await db.project.loadByParams(buildParams);
if (project === null) {
throw json("Project not found", { status: 404 });
}
const canvasData = await loadCanvasData(
project,
mode === "published" ? "prod" : "dev",
pathname
);
if (canvasData === undefined) {
throw json("Page not found", { status: 404 });
}
const params: CanvasData["params"] = {};
if (env.RESIZE_ORIGIN != null) {
params.resizeOrigin = env.RESIZE_ORIGIN;
}
return { ...canvasData, env, mode, params };
};
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => {
sentryException({ error });
const message = error instanceof Error ? error.message : String(error);
return <ErrorMessage message={message} />;
};
const Content = () => {
const data = useLoaderData<Data>();
if ("errors" in data) {
return <ErrorMessage message={data.errors} />;
}
const Outlet =
data.mode === "edit"
? () => <Canvas data={data} />
+5 -1
View File
@@ -10,9 +10,13 @@ import { db } from "@webstudio-is/project/server";
import { ensureUserCookie } from "~/shared/session";
import { authenticator } from "~/services/auth.server";
import { zfd } from "zod-form-data";
import { type Project, User } from "@webstudio-is/prisma-client";
import { type Project, User as DbUser } from "@webstudio-is/prisma-client";
import { designerPath, loginPath } from "~/shared/router-utils";
type User = Omit<DbUser, "createdAt"> & {
createdAt: string;
};
export { links };
const schema = zfd.formData({
project: zfd.text(),
@@ -1,5 +1,5 @@
import { useLoaderData } from "@remix-run/react";
import { LoaderFunction } from "@remix-run/node";
import type { ErrorBoundaryComponent, LoaderFunction } from "@remix-run/node";
import { type DesignerProps, Designer, links } from "~/designer";
import { db } from "@webstudio-is/project/server";
import { ErrorMessage } from "~/shared/error";
@@ -11,45 +11,38 @@ export { links };
export const loader: LoaderFunction = async ({
params,
request,
}): Promise<DesignerProps | Error> => {
try {
if (params.projectId === undefined) {
throw new Error("Project id undefined");
}
const url = new URL(request.url);
const pageIdParam = url.searchParams.get("pageId");
const project = await db.project.loadById(params.projectId);
if (project === null) {
throw new Error(`Project "${params.projectId}" not found`);
}
const devBuild = await db.build.loadByProjectId(project.id, "dev");
return {
project,
pages: devBuild.pages,
pageId: pageIdParam || devBuild.pages.homePage.id,
buildOrigin: getBuildOrigin(request),
};
} catch (error) {
sentryException({ error });
return { errors: error instanceof Error ? error.message : String(error) };
}): Promise<DesignerProps> => {
if (params.projectId === undefined) {
throw new Error("Project id undefined");
}
const url = new URL(request.url);
const pageIdParam = url.searchParams.get("pageId");
const project = await db.project.loadById(params.projectId);
if (project === null) {
throw new Error(`Project "${params.projectId}" not found`);
}
const devBuild = await db.build.loadByProjectId(project.id, "dev");
return {
project,
pages: devBuild.pages,
pageId: pageIdParam || devBuild.pages.homePage.id,
buildOrigin: getBuildOrigin(request),
};
};
type Error = {
errors: string;
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => {
sentryException({ error });
const message = error instanceof Error ? error.message : String(error);
return <ErrorMessage message={message} />;
};
export const DesignerRoute = () => {
const data = useLoaderData<DesignerProps | Error>();
if ("errors" in data) {
return <ErrorMessage message={data.errors} />;
}
const data = useLoaderData<DesignerProps>();
return <Designer {...data} />;
};
+9 -1
View File
@@ -3,11 +3,16 @@
// see https://github.com/remix-run/remix/issues/2098#issuecomment-1049262218 .
// To solve this, we're re-exporting the $.tsx route API in index.tsx
import type { LoaderFunction, MetaFunction } from "@remix-run/node";
import type {
ErrorBoundaryComponent,
LoaderFunction,
MetaFunction,
} from "@remix-run/node";
import CatchAllContnet, {
loader as catchAllloader,
meta as catchAllmeta,
handle as catchAllHandle,
ErrorBoundary as CatchAllErrorBoundary,
} from "./$";
// We're wrapping functions in order for them to be distinct from the ones in $.tsx.
@@ -16,5 +21,8 @@ import CatchAllContnet, {
export const meta: MetaFunction = (args) => catchAllmeta(args);
export const loader: LoaderFunction = (args) => catchAllloader(args);
export const handle = catchAllHandle;
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => (
<CatchAllErrorBoundary error={error} />
);
const Content = () => <CatchAllContnet />;
export default Content;
@@ -1,3 +1,2 @@
export const assets = false;
export const designTokens = false;
export const example = false;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@webstudio-is/asset-uploader",
"version": "0.2.0",
"version": "0.3.0",
"description": "Webstudio Asset Uploader",
"author": "Webstudio <github@webstudio.is>",
"homepage": "https://webstudio.is",
+2 -1
View File
@@ -3,9 +3,10 @@ import type { FontMeta } from "@webstudio-is/fonts/server";
import { Asset as DbAsset } from "@webstudio-is/prisma-client";
import type { ImageMeta } from "./utils/format-asset";
type BaseAsset = Omit<DbAsset, "meta"> & {
type BaseAsset = Omit<DbAsset, "meta" | "createdAt"> & {
path: string;
status?: "uploaded";
createdAt: string;
};
export type FontAsset = Omit<BaseAsset, "format"> & {
@@ -19,6 +19,7 @@ export const formatAsset = (asset: DbAsset): Asset => {
if (isFont) {
return {
...base,
createdAt: base.createdAt.toISOString(),
format: asset.format as FontFormat,
meta: FontMeta.parse(JSON.parse(asset.meta)),
};
@@ -26,6 +27,7 @@ export const formatAsset = (asset: DbAsset): Asset => {
return {
...base,
createdAt: base.createdAt.toISOString(),
meta: ImageMeta.parse(JSON.parse(asset.meta)),
};
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@webstudio-is/css-data",
"version": "0.2.0",
"version": "0.3.0",
"description": "CSS Data",
"author": "Webstudio <github@webstudio.is>",
"homepage": "https://webstudio.is",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@webstudio-is/css-engine",
"version": "0.2.0",
"version": "0.3.0",
"description": "CSS Renderer for Webstudio",
"author": "Webstudio <github@webstudio.is>",
"homepage": "https://webstudio.is",
@@ -6,6 +6,7 @@ export const config: CustomProjectConfig = {
storybookUrl: "packages/design-system/storybook-static",
},
lostPixelProjectId: "cl8a8xmx714904901m9oce4vhqc",
lostPixelUrl: 'https://app.gitbased.lost-pixel.com/api/callback',
s3: {
endPoint: "ams3.digitaloceanspaces.com",
accessKey: process.env.S3_ACCESS_KEY,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@webstudio-is/icons",
"version": "0.2.0",
"version": "0.3.0",
"description": "Webstudio Icons",
"author": "Webstudio <github@webstudio.is>",
"homepage": "https://webstudio.is",
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@webstudio-is/image",
"version": "0.0.1",
"version": "0.1.0",
"description": "Image optimization",
"author": "Webstudio <github@webstudio.is>",
"homepage": "https://webstudio.is",
@@ -11,11 +11,13 @@
"checks": "yarn typecheck && yarn lint && yarn test",
"dev": "tsup --watch",
"build:args": "generate-arg-types './src/*.tsx !./src/**/*.stories.tsx !./src/**/*.ws.tsx' && prettier --write \"**/*.props.json\"",
"build": "rm -fr lib tsconfig.tsbuildinfo && tsc",
"build": "rm -fr lib tsconfig.tsbuildinfo && yarn build:cjs && tsc",
"build:cjs": "tsc --module commonjs --outDir lib/cjs && find lib/cjs -name '*.js' | while read NAME; do mv $NAME ${NAME%.js}.cjs; done",
"lint": "eslint ./src --ext .ts,.tsx --max-warnings 0",
"storybook:run": "start-storybook -p 6006",
"storybook:build": "build-storybook",
"generate-types": "generate-arg-types"
"generate-types": "generate-arg-types",
"publish-to-npm": "bash ../../bin/publish-to-npm.sh"
},
"dependencies": {},
"devDependencies": {
@@ -38,8 +40,8 @@
},
"module": "./lib/index.js",
"exports": {
".": "./lib/index.js",
"./server": "./server.js"
"import": "./lib/index.js",
"require": "./lib/cjs/index.cjs"
},
"types": "lib/index.d.ts",
"files": [
+3 -7
View File
@@ -2,11 +2,7 @@
import React, { type ComponentProps, type HTMLAttributes } from "react";
import type { ComponentMeta, ComponentStory } from "@storybook/react";
import {
Image as ImagePrimitive,
cloudflareImageLoader,
localImageLoader,
} from "./";
import { Image as ImagePrimitive, loaders } from "./";
// to not allow include local assets everywhere, just enable it for this file
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -33,8 +29,8 @@ const imageSrc = USE_CLOUDFLARE_IMAGE_TRANSFORM
: localLogoImage;
const imageLoader = USE_CLOUDFLARE_IMAGE_TRANSFORM
? cloudflareImageLoader({ resizeOrigin: "https://webstudio.is" })
: localImageLoader();
? loaders.cloudflareImageLoader({ resizeOrigin: "https://webstudio.is" })
: loaders.localImageLoader();
const ImageBase: ComponentStory<
React.ForwardRefExoticComponent<
+1 -1
View File
@@ -1,4 +1,4 @@
export { Image } from "./image";
export type { ImageLoader } from "./image-optimize";
export { cloudflareImageLoader, localImageLoader } from "./image-loaders";
export * as loaders from "./image-loaders";
export { default as imageProps } from "./__generated__/image.props.json";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@webstudio-is/project",
"version": "0.1.0",
"version": "0.2.0",
"description": "Webstudio Project",
"author": "Webstudio <github@webstudio.is>",
"homepage": "https://webstudio.is",
@@ -1,5 +1,5 @@
import ObjectId from "bson-objectid";
import { type Instance, components } from "@webstudio-is/react-sdk";
import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk";
import { CssRule } from "@webstudio-is/css-data";
export const createInstanceId = () => {
@@ -17,7 +17,7 @@ export const createInstance = ({
children?: Instance["children"];
cssRules?: Array<CssRule>;
}): Instance => {
const componentMeta = components[component];
const componentMeta = getComponentMeta(component);
return {
component,
id: id === undefined ? createInstanceId() : id,
@@ -1,4 +1,4 @@
import { Instance, components } from "@webstudio-is/react-sdk";
import { Instance, getComponentMeta } from "@webstudio-is/react-sdk";
import { getInstancePath } from "./get-instance-path";
export const findClosestNonInlineParent = (
@@ -7,5 +7,7 @@ export const findClosestNonInlineParent = (
): Instance | undefined => {
const path = getInstancePath(rootInstance, instanceId);
path.reverse();
return path.find((item) => components[item.component].isInlineOnly === false);
return path.find(
(item) => getComponentMeta(item.component).isInlineOnly === false
);
};
@@ -1,5 +1,5 @@
import { Breakpoint } from "@webstudio-is/css-data";
import { type Instance, components } from "@webstudio-is/react-sdk";
import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk";
/**
* Populate instance with:
@@ -11,7 +11,7 @@ export const populateInstance = (
breakpoint: Breakpoint["id"] = ""
): Instance => {
const populatedInstance = { ...instance };
const componentMeta = components[instance.component];
const componentMeta = getComponentMeta(instance.component);
if (componentMeta !== undefined && "defaultStyle" in componentMeta) {
const cssRule = {
breakpoint,
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@webstudio-is/react-sdk",
"version": "0.8.0",
"version": "0.9.0",
"description": "Webstudio JavaScript / TypeScript API",
"author": "Webstudio <github@webstudio.is>",
"homepage": "https://webstudio.is",
@@ -35,7 +35,6 @@
"@storybook/testing-library": "^0.0.11",
"@types/node": "^17.0.21",
"@webstudio-is/jest-config": "*",
"@webstudio-is/prisma-client": "*",
"@webstudio-is/generate-arg-types": "*",
"babel-loader": "^8.2.5",
"esbuild": "^0.14.25",
@@ -61,9 +60,9 @@
"detect-font": "^0.1.5",
"@webstudio-is/asset-uploader": "^*",
"@webstudio-is/css-data": "*",
"@webstudio-is/fonts": "*",
"@webstudio-is/icons": "*",
"@webstudio-is/jest-config": "*",
"@webstudio-is/image": "*",
"@webstudio-is/prisma-client": "*",
"immer": "^9.0.12",
"mitt": "^3.0.0",
"react-nano-state": "^0.4.0",
@@ -0,0 +1,40 @@
import {
forwardRef,
useMemo,
type ComponentProps,
type ElementRef,
} from "react";
import { Image as WebstudioImage, loaders } from "@webstudio-is/image";
import { Image as SdkImage } from "../../components/image";
import { useUserPropsAsset } from "../../user-props/use-user-props-asset";
import { idAttribute } from "../../tree/wrapper-component";
import { getParams } from "../params";
const defaultTag = "img";
type Props = ComponentProps<typeof WebstudioImage> & { [idAttribute]: string };
export const Component = forwardRef<ElementRef<typeof defaultTag>, Props>(
(props, ref) => {
const componentId = props[idAttribute] as string;
const asset = useUserPropsAsset(componentId, "src");
const params = getParams();
const loader = useMemo(() => {
if (asset == null) return null;
if (asset.location === "REMOTE")
return loaders.cloudflareImageLoader(params);
return loaders.localImageLoader();
}, [asset, params]);
if (asset === null || loader == null)
return <SdkImage {...props} ref={ref} />;
return (
<WebstudioImage {...props} loader={loader} optimize={true} ref={ref} />
);
}
);
Component.displayName = "Image";
@@ -0,0 +1 @@
export * as Image from "./image";
@@ -1,2 +1,3 @@
export * from "./root";
export * from "./handle-request.server";
export * from "./params";
+13
View File
@@ -0,0 +1,13 @@
export type Params = {
resizeOrigin?: string;
};
let params: Params | null = {};
const emptyParams: Params = {};
export const getParams = (): Params => params ?? emptyParams;
export const setParams = (newParams: Params | null) => {
params = newParams;
};
File renamed without changes.
@@ -0,0 +1,547 @@
{
"slot": {
"defaultValue": null,
"required": false,
"type": "text"
},
"style": {
"defaultValue": null,
"required": false,
"type": "text"
},
"title": {
"defaultValue": null,
"required": false,
"type": "text"
},
"download": {
"defaultValue": null,
"required": false,
"type": "text"
},
"href": {
"defaultValue": "",
"required": false,
"type": "text"
},
"hrefLang": {
"defaultValue": null,
"required": false,
"type": "text"
},
"media": {
"defaultValue": null,
"required": false,
"type": "text"
},
"ping": {
"defaultValue": null,
"required": false,
"type": "text"
},
"rel": {
"defaultValue": null,
"required": false,
"type": "text"
},
"target": {
"defaultValue": null,
"required": false,
"type": "text"
},
"type": {
"defaultValue": null,
"required": false,
"type": "text"
},
"referrerPolicy": {
"defaultValue": null,
"options": [
"\"\"",
"no-referrer",
"no-referrer-when-downgrade",
"origin",
"origin-when-cross-origin",
"same-origin",
"strict-origin",
"strict-origin-when-cross-origin",
"unsafe-url"
],
"required": false,
"type": "select"
},
"defaultChecked": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"defaultValue": {
"defaultValue": null,
"required": false,
"type": "text"
},
"suppressContentEditableWarning": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"suppressHydrationWarning": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"accessKey": {
"defaultValue": null,
"required": false,
"type": "text"
},
"className": {
"defaultValue": null,
"required": false,
"type": "text"
},
"contentEditable": {
"defaultValue": null,
"required": false,
"type": "text"
},
"contextMenu": {
"defaultValue": null,
"required": false,
"type": "text"
},
"dir": {
"defaultValue": null,
"required": false,
"type": "text"
},
"draggable": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"hidden": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"id": {
"defaultValue": null,
"required": false,
"type": "text"
},
"lang": {
"defaultValue": null,
"required": false,
"type": "text"
},
"placeholder": {
"defaultValue": null,
"required": false,
"type": "text"
},
"spellCheck": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"tabIndex": {
"defaultValue": null,
"required": false,
"type": "number"
},
"translate": {
"defaultValue": null,
"options": ["yes", "no"],
"required": false,
"type": "radio"
},
"radioGroup": {
"defaultValue": null,
"required": false,
"type": "text"
},
"role": {
"defaultValue": null,
"required": false,
"type": "text"
},
"about": {
"defaultValue": null,
"required": false,
"type": "text"
},
"datatype": {
"defaultValue": null,
"required": false,
"type": "text"
},
"inlist": {
"defaultValue": null,
"required": false,
"type": "text"
},
"prefix": {
"defaultValue": null,
"required": false,
"type": "text"
},
"property": {
"defaultValue": null,
"required": false,
"type": "text"
},
"resource": {
"defaultValue": null,
"required": false,
"type": "text"
},
"typeof": {
"defaultValue": null,
"required": false,
"type": "text"
},
"vocab": {
"defaultValue": null,
"required": false,
"type": "text"
},
"autoCapitalize": {
"defaultValue": null,
"required": false,
"type": "text"
},
"autoCorrect": {
"defaultValue": null,
"required": false,
"type": "text"
},
"autoSave": {
"defaultValue": null,
"required": false,
"type": "text"
},
"color": {
"defaultValue": null,
"required": false,
"type": "color"
},
"itemProp": {
"defaultValue": null,
"required": false,
"type": "text"
},
"itemScope": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"itemType": {
"defaultValue": null,
"required": false,
"type": "text"
},
"itemID": {
"defaultValue": null,
"required": false,
"type": "text"
},
"itemRef": {
"defaultValue": null,
"required": false,
"type": "text"
},
"results": {
"defaultValue": null,
"required": false,
"type": "number"
},
"security": {
"defaultValue": null,
"required": false,
"type": "text"
},
"unselectable": {
"defaultValue": null,
"options": ["on", "off"],
"required": false,
"type": "radio"
},
"inputMode": {
"defaultValue": null,
"options": [
"text",
"none",
"search",
"tel",
"url",
"email",
"numeric",
"decimal"
],
"required": false,
"type": "select"
},
"is": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-activedescendant": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-atomic": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-autocomplete": {
"defaultValue": null,
"options": ["list", "none", "inline", "both"],
"required": false,
"type": "radio"
},
"aria-busy": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-checked": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-colcount": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-colindex": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-colspan": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-controls": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-current": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-describedby": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-details": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-disabled": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-dropeffect": {
"defaultValue": null,
"options": ["link", "none", "copy", "execute", "move", "popup"],
"required": false,
"type": "select"
},
"aria-errormessage": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-expanded": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-flowto": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-grabbed": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-haspopup": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-hidden": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-invalid": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-keyshortcuts": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-label": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-labelledby": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-level": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-live": {
"defaultValue": null,
"options": ["off", "assertive", "polite"],
"required": false,
"type": "radio"
},
"aria-modal": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-multiline": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-multiselectable": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-orientation": {
"defaultValue": null,
"options": ["horizontal", "vertical"],
"required": false,
"type": "radio"
},
"aria-owns": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-placeholder": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-posinset": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-pressed": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-readonly": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-relevant": {
"defaultValue": null,
"options": [
"text",
"additions",
"additions removals",
"additions text",
"all",
"removals",
"removals additions",
"removals text",
"text additions",
"text removals"
],
"required": false,
"type": "select"
},
"aria-required": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-roledescription": {
"defaultValue": null,
"required": false,
"type": "text"
},
"aria-rowcount": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-rowindex": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-rowspan": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-selected": {
"defaultValue": null,
"required": false,
"type": "boolean"
},
"aria-setsize": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-sort": {
"defaultValue": null,
"options": ["none", "ascending", "descending", "other"],
"required": false,
"type": "radio"
},
"aria-valuemax": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-valuemin": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-valuenow": {
"defaultValue": null,
"required": false,
"type": "number"
},
"aria-valuetext": {
"defaultValue": null,
"required": false,
"type": "text"
}
}
@@ -1,12 +1,6 @@
import * as components from "./index";
import { getComponentNames, getComponentMeta } from "./index";
import { WsComponentMeta } from "./component-type";
test.each(Object.entries(components))(
"validating meta definition of %s",
(name, component) => {
if (name === "default") {
return;
}
WsComponentMeta.parse(component);
}
);
test.each(getComponentNames())("validating meta definition of %s", (name) => {
WsComponentMeta.parse(getComponentMeta(name));
});
+98 -15
View File
@@ -1,15 +1,98 @@
export { default as Body } from "./body.ws";
export { default as Box } from "./box.ws";
export { default as TextBlock } from "./text-block.ws";
export { default as Heading } from "./heading.ws";
export { default as Paragraph } from "./paragraph.ws";
export { default as Link } from "./link.ws";
export { default as Span } from "./span.ws";
export { default as Bold } from "./bold.ws";
export { default as Italic } from "./italic.ws";
export { default as Superscript } from "./superscript.ws";
export { default as Subscript } from "./subscript.ws";
export { default as Button } from "./button.ws";
export { default as Input } from "./input.ws";
export { default as Form } from "./form.ws";
export { default as Image } from "./image.ws";
import Body from "./body.ws";
import Box from "./box.ws";
import TextBlock from "./text-block.ws";
import Heading from "./heading.ws";
import Paragraph from "./paragraph.ws";
import Link from "./link.ws";
import RichTextLink from "./rich-text-link.ws";
import Span from "./span.ws";
import Bold from "./bold.ws";
import Italic from "./italic.ws";
import Superscript from "./superscript.ws";
import Subscript from "./subscript.ws";
import Button from "./button.ws";
import Input from "./input.ws";
import Form from "./form.ws";
import Image from "./image.ws";
const components = {
Body,
Box,
TextBlock,
Heading,
Paragraph,
Link,
RichTextLink,
Span,
Bold,
Italic,
Superscript,
Subscript,
Button,
Input,
Form,
Image,
} as const;
type RegisteredComponents = Partial<{
[p in ComponentName]: {
// @todo: Anyone knows what the type can be for any React Component?
// eslint-disable-next-line @typescript-eslint/ban-types
Component: {};
meta?: Partial<Omit<typeof components[p], "Component">>;
};
}>;
let registeredComponents: RegisteredComponents | null = null;
export type ComponentName = keyof typeof components;
const componentNames = Object.keys(components) as ComponentName[];
export const getComponentNames = (): ComponentName[] => {
const uniqueNames = new Set([
...componentNames,
...Object.keys(registeredComponents || {}),
]);
return [...uniqueNames.values()] as ComponentName[];
};
export const getComponentMeta = <Name extends ComponentName>(
name: Name
): Omit<typeof components[Name], "Component"> => {
if (
registeredComponents != null &&
name in registeredComponents &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
registeredComponents[name]!.meta != null
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return { ...components[name], ...registeredComponents[name]!.meta };
}
return components[name];
};
/**
* @todo Remove Component from meta and read directly (via import),
* as meta information is not needed during production render.
**/
export const getComponent = <Name extends ComponentName>(
name: Name
): typeof components[Name]["Component"] => {
return registeredComponents != null && name in registeredComponents
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(registeredComponents[name]!
.Component as typeof components[Name]["Component"])
: components[name].Component;
};
/**
* @todo: Allow register any component.
* Now we can register only existings Components, as all our type system would
* break otherwise, see getComponent etc. So its overwriteComponent now
**/
export const registerComponents = (components: RegisteredComponents) => {
registeredComponents = components;
};
+6 -4
View File
@@ -8,6 +8,7 @@ import Input from "./__generated__/input.props.json";
import Italic from "./__generated__/italic.props.json";
import Superscript from "./__generated__/superscript.props.json";
import Subscript from "./__generated__/subscript.props.json";
import RichTextLink from "./__generated__/rich-text-link.props.json";
import Link from "./__generated__/link.props.json";
import Paragraph from "./__generated__/paragraph.props.json";
import Span from "./__generated__/span.props.json";
@@ -25,6 +26,7 @@ const meta = {
Italic,
Superscript,
Subscript,
RichTextLink,
Link,
Paragraph,
Span,
@@ -56,8 +58,8 @@ type MetaProp =
options: string[];
};
type MetaProps = Record<string, MetaProp>;
type MetaProps = { [p in string]?: MetaProp };
// Probably better instead of JSON to generate ts with `as const`
export const componentsMeta: Record<keyof typeof meta, MetaProps> =
meta as Record<keyof typeof meta, MetaProps>;
export const getComponentMetaProps = (name: keyof typeof meta): MetaProps => {
return meta[name] as MetaProps;
};
@@ -0,0 +1,16 @@
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { RichTextLink as LinkPrimitive } from "./rich-text-link";
export default {
title: "Components/RichTextLink",
component: LinkPrimitive,
} as ComponentMeta<typeof LinkPrimitive>;
const Template: ComponentStory<typeof LinkPrimitive> = (args) => (
<LinkPrimitive {...args} />
);
export const RichTextLink = Template.bind({});
RichTextLink.args = {
children: "RichTextLink",
};
@@ -0,0 +1,14 @@
import { forwardRef, type ElementRef, type ComponentProps } from "react";
const defaultTag = "a";
type RichTextLinkProps = Omit<ComponentProps<typeof defaultTag>, "href"> & {
href?: string;
};
export const RichTextLink = forwardRef<
ElementRef<typeof defaultTag>,
RichTextLinkProps
>(({ href = "", ...props }, ref) => <a {...props} href={href} ref={ref} />);
RichTextLink.displayName = "RichTextLink";
@@ -0,0 +1,15 @@
import { Link2Icon } from "@webstudio-is/icons";
import { RichTextLink } from "./rich-text-link";
import type { WsComponentMeta } from "./component-type";
const meta: WsComponentMeta<typeof RichTextLink> = {
Icon: Link2Icon,
Component: RichTextLink,
canAcceptChildren: false,
isContentEditable: false,
isInlineOnly: true,
isListed: false,
label: "Link",
};
export default meta;
+1
View File
@@ -60,6 +60,7 @@ const size = [
"maxHeight",
"overflow",
"objectFit",
"aspectRatio",
] as const;
const position = [
+2 -2
View File
@@ -1,12 +1,12 @@
import { CssRule } from "@webstudio-is/css-data";
import { z } from "zod";
import * as components from "../components";
import { ComponentName } from "../components";
// This should be used when passing a lot of data is potentially costly.
// For example, when passing data from an iframe.
export type BaseInstance = {
id: string;
component: keyof typeof components;
component: ComponentName;
cssRules: Array<CssRule>;
};
+3 -2
View File
@@ -1,8 +1,9 @@
export * from "./css";
export * from "./tree";
export * as components from "./components";
export * from "./components";
export * from "./components/meta";
export * from "./user-props";
export * from "./pubsub";
export * from "./db";
export * from "./remix";
export * from "./app";
export * as customComponents from "./app/custom-components";
+8
View File
@@ -5,6 +5,9 @@ import { WrapperComponent } from "./wrapper-component";
import type { Asset } from "@webstudio-is/asset-uploader";
import { type ComponentProps } from "react";
import type { Breakpoint } from "@webstudio-is/css-data";
import { registerComponents } from "../components";
import * as customComponents from "../app/custom-components";
import { setParams, type Params } from "../app/params";
export type Data = {
tree: Tree | null;
@@ -12,6 +15,7 @@ export type Data = {
designTokens: Array<any>;
props: Array<InstanceProps>;
assets: Array<Asset>;
params?: Params;
};
type RootProps = {
@@ -27,6 +31,10 @@ export const InstanceRoot = ({
throw new Error("Tree is null");
}
useAllUserProps(data.props);
setParams(data.params ?? null);
registerComponents(customComponents);
return createElementsTree({
instance: data.tree.root,
Component: Component ?? WrapperComponent,
@@ -1,6 +1,6 @@
import { Fragment } from "react";
import type { Instance } from "../db";
import * as components from "../components";
import { getComponent } from "../components";
import { useUserProps } from "../user-props/use-user-props";
import type { OnChangeChildren } from "./create-elements-tree";
@@ -37,7 +37,7 @@ export const WrapperComponent = ({
children,
...rest
}: WrapperComponentProps) => {
const { Component } = components[instance.component];
const Component = getComponent(instance.component);
const userProps = useUserProps(instance.id);
const props = { ...userProps, ...rest, [idAttribute]: instance.id };
return (
@@ -0,0 +1,26 @@
import { useMemo } from "react";
import { type Instance } from "../db";
import { type UserProp } from "./schema";
import { useAllUserProps } from "./all-user-props";
import type { Asset } from "@webstudio-is/asset-uploader";
/**
* Get asset for prop, like src on the Image component
*/
export const useUserPropsAsset = (
instanceId: Instance["id"],
propName: UserProp["prop"]
): Asset | null => {
const [allUserProps] = useAllUserProps();
const propsData = allUserProps[instanceId];
const asset = useMemo(() => {
if (propsData == null) return null;
const prop = propsData.props.find((prop) => prop.prop === propName);
if (prop == null) return null;
return prop.asset ?? null;
}, [propName, propsData]);
return asset;
};
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useMemo } from "react";
import { type Instance } from "../db";
import { type UserProp } from "./schema";
import { useAllUserProps } from "./all-user-props";
@@ -13,24 +13,13 @@ export const useUserProps = (instanceId: Instance["id"]) => {
const [allUserProps] = useAllUserProps();
const propsData = allUserProps[instanceId];
const initialUserProps = useMemo(() => {
if (propsData === undefined) return {};
const props = useMemo(() => {
if (propsData == null) return {};
return propsData.props.reduce((props, { prop, value }) => {
props[prop] = value;
return props;
}, {} as UserProps);
}, [propsData]);
const [props, setProps] = useState<UserProps>(initialUserProps);
useEffect(() => {
if (propsData == undefined) return;
const nextProps: UserProps = {};
for (const prop of propsData.props) {
nextProps[prop.prop] = prop.value;
}
setProps(nextProps);
}, [propsData]);
return props;
};
+4 -4
View File
@@ -23,10 +23,6 @@
"path": "../css-data"
},
{
"path": "../fonts"
},
{
"path": "../generate-arg-types"
},
@@ -35,6 +31,10 @@
"path": "../icons"
},
{
"path": "../image"
},
{
"path": "../prisma-client"
}
+1 -10
View File
@@ -5553,16 +5553,7 @@
dependencies:
"@types/react" "*"
"@types/react@*":
version "18.0.15"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.15.tgz#d355644c26832dc27f3e6cbf0c4f4603fc4ab7fe"
integrity sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@^17", "@types/react@^17.0.24":
"@types/react@*", "@types/react@^17", "@types/react@^17.0.24":
version "17.0.47"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.47.tgz#4ee71aaf4c5a9e290e03aa4d0d313c5d666b3b78"
integrity sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==