mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 21:15:18 +09:00
Merge branch 'main' into 494-primitive-tokens
This commit is contained in:
67 files changed
+1124
-283
No files matched your search
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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,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,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,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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,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,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,
|
||||
|
||||
@@ -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";
|
||||
File renamed without changes.
@@ -1,2 +1,3 @@
|
||||
export * from "./root";
|
||||
export * from "./handle-request.server";
|
||||
export * from "./params";
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -60,6 +60,7 @@ const size = [
|
||||
"maxHeight",
|
||||
"overflow",
|
||||
"objectFit",
|
||||
"aspectRatio",
|
||||
] as const;
|
||||
|
||||
const position = [
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
};
|
||||
@@ -23,10 +23,6 @@
|
||||
"path": "../css-data"
|
||||
},
|
||||
|
||||
{
|
||||
"path": "../fonts"
|
||||
},
|
||||
|
||||
{
|
||||
"path": "../generate-arg-types"
|
||||
},
|
||||
@@ -35,6 +31,10 @@
|
||||
"path": "../icons"
|
||||
},
|
||||
|
||||
{
|
||||
"path": "../image"
|
||||
},
|
||||
|
||||
{
|
||||
"path": "../prisma-client"
|
||||
}
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in new issue
Block a user