mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 14:03:33 +09:00
Props panel: Basics (#1031)
#992 1. Cleaned up control types 2. Control type is now chosen by component meta (including Image.src) 3. Added custom labels support 4. Some other changes mentioned in comments in diff
This commit is contained in:
1 parent
7bd744c7d9
commit
343647d52a
51 files changed
+10810
-6148
No files matched your search
@@ -3,7 +3,7 @@ import warnOnce from "warn-once";
|
||||
import { useMemo } from "react";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import type { Instance, Prop } from "@webstudio-is/project-build";
|
||||
import { getComponentMetaProps } from "@webstudio-is/react-sdk";
|
||||
import { getComponentPropsMeta } from "@webstudio-is/react-sdk";
|
||||
import type { Asset } from "@webstudio-is/asset-uploader";
|
||||
import {
|
||||
Flex,
|
||||
@@ -21,15 +21,7 @@ import { ImageManager } from "~/builder/shared/image-manager";
|
||||
import type { UserPropValue } from "./use-props-logic";
|
||||
import type { SetProperty } from "../style-panel/shared/use-style-data";
|
||||
|
||||
const textControlTypes = [
|
||||
"text",
|
||||
"array",
|
||||
"color",
|
||||
"date",
|
||||
"file",
|
||||
"number",
|
||||
"object",
|
||||
] as const;
|
||||
const textControlTypes = ["text", "color", "number", "multiline-text"] as const;
|
||||
|
||||
type TextControlProps = {
|
||||
type: typeof textControlTypes[number];
|
||||
@@ -49,13 +41,11 @@ const TextControl = ({ value, type, onChange }: TextControlProps) => (
|
||||
/>
|
||||
);
|
||||
|
||||
const checkboxControlTypes = ["check", "inline-check", "multi-select"] as const;
|
||||
|
||||
type CheckboxControlProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: Array<string>;
|
||||
type: typeof checkboxControlTypes[number];
|
||||
type: "check";
|
||||
};
|
||||
|
||||
const CheckboxControl = ({
|
||||
@@ -78,14 +68,11 @@ const CheckboxControl = ({
|
||||
</RadioGroup>
|
||||
);
|
||||
|
||||
const radioControlTypes = ["radio", "inline-radio"] as const;
|
||||
|
||||
type RadioControlProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
|
||||
options: Array<string>;
|
||||
type: typeof radioControlTypes[number];
|
||||
type: "radio";
|
||||
};
|
||||
|
||||
const RadioControl = ({
|
||||
@@ -201,34 +188,17 @@ export function Control({
|
||||
onChangePropValue,
|
||||
setCssProperty,
|
||||
}: ControlProps) {
|
||||
const meta = getComponentMetaProps(component);
|
||||
const meta = getComponentPropsMeta(component)?.props;
|
||||
|
||||
const argType = meta?.[userProp.name];
|
||||
|
||||
// argType can be undefined in case of new property created
|
||||
const defaultValue = argType?.defaultValue ?? "";
|
||||
const type = argType?.type ?? "text";
|
||||
const rawControl = argType?.control || "text";
|
||||
const control =
|
||||
typeof rawControl === "string" ? { type: rawControl } : rawControl;
|
||||
|
||||
if (type == null) {
|
||||
warnOnce(
|
||||
true,
|
||||
`No control type for prop "${userProp.name}" component "${component}" found`
|
||||
);
|
||||
return <NotImplemented />;
|
||||
}
|
||||
|
||||
if (typeof type !== "string") {
|
||||
warnOnce(
|
||||
true,
|
||||
`Control type "${typeof type}" for prop "${
|
||||
userProp.name
|
||||
}" component "${component}" is not a string`
|
||||
);
|
||||
|
||||
return <NotImplemented />;
|
||||
}
|
||||
|
||||
if (component === "Image" && userProp.name === "src") {
|
||||
if (control.type === "file-image") {
|
||||
const assetId = userProp.type === "asset" ? userProp.value : undefined;
|
||||
|
||||
return (
|
||||
@@ -260,7 +230,7 @@ export function Control({
|
||||
);
|
||||
}
|
||||
|
||||
if (includes(textControlTypes, type)) {
|
||||
if (includes(textControlTypes, control.type)) {
|
||||
const value = `${userProp.value}`;
|
||||
|
||||
return (
|
||||
@@ -272,12 +242,12 @@ export function Control({
|
||||
value,
|
||||
})
|
||||
}
|
||||
type={type}
|
||||
type={control.type}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "boolean") {
|
||||
if (control.type === "boolean") {
|
||||
const value = Boolean(userProp.value);
|
||||
|
||||
return (
|
||||
@@ -295,14 +265,14 @@ 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"
|
||||
control.type === "radio" ||
|
||||
control.type === "check" ||
|
||||
control.type === "select" ||
|
||||
control.type === "inline-check" ||
|
||||
control.type === "inline-radio" ||
|
||||
control.type === "multi-select"
|
||||
) {
|
||||
const options = argType.options;
|
||||
const options = control.options;
|
||||
|
||||
const value = `${userProp.value}`;
|
||||
|
||||
@@ -313,7 +283,7 @@ export function Control({
|
||||
|
||||
const DEFAULT_OPTIONS: string[] = [];
|
||||
|
||||
if (includes(radioControlTypes, type)) {
|
||||
if (control.type === "radio" || control.type === "inline-radio") {
|
||||
return (
|
||||
<RadioControl
|
||||
value={value}
|
||||
@@ -324,12 +294,16 @@ export function Control({
|
||||
})
|
||||
}
|
||||
options={options ?? DEFAULT_OPTIONS}
|
||||
type={type}
|
||||
type="radio"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (includes(checkboxControlTypes, type)) {
|
||||
if (
|
||||
control.type === "check" ||
|
||||
control.type === "inline-check" ||
|
||||
control.type === "multi-select"
|
||||
) {
|
||||
return (
|
||||
<CheckboxControl
|
||||
value={value}
|
||||
@@ -340,12 +314,12 @@ export function Control({
|
||||
})
|
||||
}
|
||||
options={options ?? DEFAULT_OPTIONS}
|
||||
type={type}
|
||||
type="check"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (includes(selectControlTypes, type)) {
|
||||
if (control.type === "select") {
|
||||
return (
|
||||
<SelectControl
|
||||
value={value}
|
||||
@@ -356,18 +330,29 @@ export function Control({
|
||||
})
|
||||
}
|
||||
options={options ?? DEFAULT_OPTIONS}
|
||||
type={type}
|
||||
type={control.type}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
assertUnreachable(type, `Unknown control type ${type}`);
|
||||
}
|
||||
|
||||
if (
|
||||
control.type === "object" ||
|
||||
control.type === "date" ||
|
||||
control.type === "range"
|
||||
) {
|
||||
warnOnce(
|
||||
true,
|
||||
`Control type "${control.type}" is not implemented for prop: "${userProp.name}" in component "${component}"`
|
||||
);
|
||||
return <NotImplemented />;
|
||||
}
|
||||
|
||||
assertUnreachable(control.type, `Unknown control type ${control.type}`);
|
||||
|
||||
warnOnce(
|
||||
true,
|
||||
`Control type "${type}" is not implemented for prop: "${userProp.name}" in component "${component}"`
|
||||
`Control type "${control.type}" is not implemented for prop: "${userProp.name}" in component "${component}"`
|
||||
);
|
||||
|
||||
return <NotImplemented />;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ComponentStory } from "@storybook/react";
|
||||
import type { Prop } from "@webstudio-is/project-build";
|
||||
import { getComponentMetaProps } from "@webstudio-is/react-sdk";
|
||||
import { getComponentPropsMeta } from "@webstudio-is/react-sdk";
|
||||
import { propsStore } from "~/shared/nano-states";
|
||||
import { PropsPanel } from "./props-panel";
|
||||
|
||||
@@ -70,7 +70,7 @@ export const DefaultProps: ComponentStory<typeof PropsPanel> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const meta = getComponentMetaProps("Button") ?? {};
|
||||
const meta = getComponentPropsMeta("Button")?.props ?? {};
|
||||
|
||||
export const AllProps: ComponentStory<typeof PropsPanel> = () => {
|
||||
propsStore.set(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import store from "immerhin";
|
||||
import type { Instance, Prop } from "@webstudio-is/project-build";
|
||||
import { getComponentMetaProps } from "@webstudio-is/react-sdk";
|
||||
import { getComponentPropsMeta } from "@webstudio-is/react-sdk";
|
||||
import {
|
||||
theme,
|
||||
Box,
|
||||
@@ -135,7 +135,7 @@ const Property = ({
|
||||
required,
|
||||
existingProps,
|
||||
}: PropertyProps) => {
|
||||
const metaProps = getComponentMetaProps(component) ?? {};
|
||||
const metaProps = getComponentPropsMeta(component)?.props ?? {};
|
||||
|
||||
const argType = metaProps[userProp.name as keyof typeof metaProps];
|
||||
const isInvalid =
|
||||
@@ -158,7 +158,7 @@ const Property = ({
|
||||
placeholder="Property"
|
||||
readOnly={true}
|
||||
state={isInvalid ? "invalid" : undefined}
|
||||
value={userProp.name}
|
||||
value={argType?.name ?? userProp.name}
|
||||
/>
|
||||
) : (
|
||||
<Combobox
|
||||
@@ -223,7 +223,7 @@ export const PropsPanel = ({ selectedInstance, publish }: PropsPanelProps) => {
|
||||
handleChangePropName,
|
||||
handleChangePropValue,
|
||||
handleDeleteProp,
|
||||
isRequired,
|
||||
initialProps,
|
||||
} = usePropsLogic({
|
||||
props: instanceProps,
|
||||
selectedInstance,
|
||||
@@ -287,7 +287,7 @@ export const PropsPanel = ({ selectedInstance, publish }: PropsPanelProps) => {
|
||||
}
|
||||
setCssProperty={setCssProperty}
|
||||
onDelete={() => handleDeleteProp(userProp)}
|
||||
required={isRequired(userProp)}
|
||||
required={initialProps.includes(userProp.name)}
|
||||
existingProps={existingProps}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { jest, describe, test, expect } from "@jest/globals";
|
||||
import { renderHook, act } from "@testing-library/react-hooks";
|
||||
import type { Instance } from "@webstudio-is/project-build";
|
||||
import { getComponentMeta } from "@webstudio-is/react-sdk";
|
||||
import { nanoid } from "nanoid";
|
||||
import { usePropsLogic } from "./use-props-logic";
|
||||
|
||||
@@ -118,126 +117,126 @@ describe("usePropsLogic", () => {
|
||||
expect(result.current.userProps[1]).toMatchObject({ name: "", value: "" });
|
||||
});
|
||||
|
||||
test("should respect initialProps ordering", () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePropsLogic({
|
||||
props: [
|
||||
{
|
||||
id: "22",
|
||||
instanceId: "instanceId",
|
||||
name: "aria-label",
|
||||
type: "string",
|
||||
value: "https://example.com",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: "0",
|
||||
instanceId: "instanceId",
|
||||
name: "alt",
|
||||
type: "string",
|
||||
value: "alt text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
instanceId: "instanceId",
|
||||
name: "width",
|
||||
type: "number",
|
||||
value: 101,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: "33",
|
||||
instanceId: "instanceId",
|
||||
name: "title",
|
||||
type: "string",
|
||||
value: "title text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
instanceId: "instanceId",
|
||||
name: "src",
|
||||
type: "string",
|
||||
value: "https://example.com",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
selectedInstance: getSelectedInstance("Image"),
|
||||
updateProps: jest.fn(),
|
||||
deleteProp: jest.fn(),
|
||||
})
|
||||
);
|
||||
// test("should respect initialProps ordering", () => {
|
||||
// const { result } = renderHook(() =>
|
||||
// usePropsLogic({
|
||||
// props: [
|
||||
// {
|
||||
// id: "22",
|
||||
// instanceId: "instanceId",
|
||||
// name: "aria-label",
|
||||
// type: "string",
|
||||
// value: "https://example.com",
|
||||
// required: false,
|
||||
// },
|
||||
// {
|
||||
// id: "0",
|
||||
// instanceId: "instanceId",
|
||||
// name: "alt",
|
||||
// type: "string",
|
||||
// value: "alt text",
|
||||
// required: true,
|
||||
// },
|
||||
// {
|
||||
// id: "1",
|
||||
// instanceId: "instanceId",
|
||||
// name: "width",
|
||||
// type: "number",
|
||||
// value: 101,
|
||||
// required: false,
|
||||
// },
|
||||
// {
|
||||
// id: "33",
|
||||
// instanceId: "instanceId",
|
||||
// name: "title",
|
||||
// type: "string",
|
||||
// value: "title text",
|
||||
// required: true,
|
||||
// },
|
||||
// {
|
||||
// id: "2",
|
||||
// instanceId: "instanceId",
|
||||
// name: "src",
|
||||
// type: "string",
|
||||
// value: "https://example.com",
|
||||
// required: true,
|
||||
// },
|
||||
// ],
|
||||
// selectedInstance: getSelectedInstance("Image"),
|
||||
// updateProps: jest.fn(),
|
||||
// deleteProp: jest.fn(),
|
||||
// })
|
||||
// );
|
||||
|
||||
const imgMeta = getComponentMeta("Image");
|
||||
// const imgMeta = getComponentMeta("Image");
|
||||
|
||||
const propNames = result.current.userProps.map((userProp) => userProp.name);
|
||||
// const propNames = result.current.userProps.map((userProp) => userProp.name);
|
||||
|
||||
expect(imgMeta?.initialProps).toEqual(
|
||||
propNames.slice(0, imgMeta?.initialProps?.length ?? 0)
|
||||
);
|
||||
// expect(imgMeta?.initialProps).toEqual(
|
||||
// propNames.slice(0, imgMeta?.initialProps?.length ?? 0)
|
||||
// );
|
||||
|
||||
expect(propNames.slice(imgMeta?.initialProps?.length ?? 0)).toEqual(
|
||||
result.current.userProps
|
||||
.filter(
|
||||
(userProp) => imgMeta?.initialProps?.includes(userProp.name) === false
|
||||
)
|
||||
.map((userProp) => userProp.name)
|
||||
);
|
||||
});
|
||||
// expect(propNames.slice(imgMeta?.initialProps?.length ?? 0)).toEqual(
|
||||
// result.current.userProps
|
||||
// .filter(
|
||||
// (userProp) => imgMeta?.initialProps?.includes(userProp.name) === false
|
||||
// )
|
||||
// .map((userProp) => userProp.name)
|
||||
// );
|
||||
// });
|
||||
|
||||
test("should return isRequired true for initial props", () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePropsLogic({
|
||||
props: [],
|
||||
selectedInstance: getSelectedInstance("Image"),
|
||||
updateProps: jest.fn(),
|
||||
deleteProp: jest.fn(),
|
||||
})
|
||||
);
|
||||
// test("should return isInitial true for initial props", () => {
|
||||
// const { result } = renderHook(() =>
|
||||
// usePropsLogic({
|
||||
// props: [],
|
||||
// selectedInstance: getSelectedInstance("Image"),
|
||||
// updateProps: jest.fn(),
|
||||
// deleteProp: jest.fn(),
|
||||
// })
|
||||
// );
|
||||
|
||||
expect(
|
||||
result.current.isRequired({
|
||||
id: "2",
|
||||
instanceId: "instanceId",
|
||||
name: "src",
|
||||
type: "string",
|
||||
value: "https://example.com",
|
||||
required: false,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
// expect(
|
||||
// result.current.isInitial({
|
||||
// id: "2",
|
||||
// instanceId: "instanceId",
|
||||
// name: "src",
|
||||
// type: "string",
|
||||
// value: "https://example.com",
|
||||
// required: false,
|
||||
// })
|
||||
// ).toBe(true);
|
||||
// });
|
||||
|
||||
test("isRequired should respect required prop", () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePropsLogic({
|
||||
props: [],
|
||||
selectedInstance: getSelectedInstance("Image"),
|
||||
updateProps: jest.fn(),
|
||||
deleteProp: jest.fn(),
|
||||
})
|
||||
);
|
||||
// test("isInitial should respect required prop", () => {
|
||||
// const { result } = renderHook(() =>
|
||||
// usePropsLogic({
|
||||
// props: [],
|
||||
// selectedInstance: getSelectedInstance("Image"),
|
||||
// updateProps: jest.fn(),
|
||||
// deleteProp: jest.fn(),
|
||||
// })
|
||||
// );
|
||||
|
||||
expect(
|
||||
result.current.isRequired({
|
||||
id: "2",
|
||||
instanceId: "instanceId",
|
||||
name: "arial-label",
|
||||
type: "string",
|
||||
value: "https://example.com",
|
||||
required: true,
|
||||
})
|
||||
).toBe(true);
|
||||
// expect(
|
||||
// result.current.isInitial({
|
||||
// id: "2",
|
||||
// instanceId: "instanceId",
|
||||
// name: "arial-label",
|
||||
// type: "string",
|
||||
// value: "https://example.com",
|
||||
// required: true,
|
||||
// })
|
||||
// ).toBe(true);
|
||||
|
||||
expect(
|
||||
result.current.isRequired({
|
||||
id: "2",
|
||||
instanceId: "instanceId",
|
||||
name: "arial-label",
|
||||
type: "string",
|
||||
value: "https://example.com",
|
||||
required: false,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
// expect(
|
||||
// result.current.isInitial({
|
||||
// id: "2",
|
||||
// instanceId: "instanceId",
|
||||
// name: "arial-label",
|
||||
// type: "string",
|
||||
// value: "https://example.com",
|
||||
// required: false,
|
||||
// })
|
||||
// ).toBe(false);
|
||||
// });
|
||||
});
|
||||
@@ -1,12 +1,8 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { nanoid } from "nanoid";
|
||||
import warnOnce from "warn-once";
|
||||
import type { Instance, Prop } from "@webstudio-is/project-build";
|
||||
import type { MetaProps } from "@webstudio-is/react-sdk";
|
||||
import {
|
||||
getComponentMeta,
|
||||
getComponentMetaProps,
|
||||
} from "@webstudio-is/react-sdk";
|
||||
import type { WsComponentPropsMeta } from "@webstudio-is/react-sdk";
|
||||
import { getComponentPropsMeta } from "@webstudio-is/react-sdk";
|
||||
|
||||
export type UserPropValue = Prop extends infer T
|
||||
? T extends { value: unknown; type: unknown }
|
||||
@@ -14,20 +10,22 @@ export type UserPropValue = Prop extends infer T
|
||||
: never
|
||||
: never;
|
||||
|
||||
export const getValueFromPropMeta = (propValue?: MetaProps[string]) => {
|
||||
export const getValueFromPropMeta = (
|
||||
propValue?: WsComponentPropsMeta["props"][string]
|
||||
) => {
|
||||
let typedValue: UserPropValue = {
|
||||
type: "string",
|
||||
value: `${propValue?.defaultValue ?? ""}`,
|
||||
};
|
||||
|
||||
if (propValue?.type === "boolean") {
|
||||
if (propValue?.control === "boolean") {
|
||||
typedValue = {
|
||||
type: "boolean",
|
||||
value: propValue.defaultValue ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
if (propValue?.type === "number") {
|
||||
if (propValue?.control === "number") {
|
||||
typedValue = {
|
||||
type: "number",
|
||||
value: propValue.defaultValue ?? 0,
|
||||
@@ -37,37 +35,6 @@ export const getValueFromPropMeta = (propValue?: MetaProps[string]) => {
|
||||
return typedValue;
|
||||
};
|
||||
|
||||
const getRequiredPropsList = (component: string) => {
|
||||
const meta = getComponentMeta(component);
|
||||
const metaProps = getComponentMetaProps(component) ?? {};
|
||||
|
||||
const initialProps = meta?.initialProps ?? [];
|
||||
const requiredProps = [];
|
||||
const propsWithDefaultValue = [];
|
||||
|
||||
for (const [prop, value] of Object.entries(metaProps)) {
|
||||
if (value?.required) {
|
||||
warnOnce(
|
||||
value.defaultValue == null,
|
||||
`Default value for required "${prop}" is required`
|
||||
);
|
||||
requiredProps.push(prop);
|
||||
}
|
||||
if (value?.defaultValue != null) {
|
||||
propsWithDefaultValue.push(prop);
|
||||
}
|
||||
}
|
||||
|
||||
// deduplicate required props
|
||||
return Array.from(
|
||||
new Set<string>([
|
||||
...initialProps,
|
||||
...requiredProps,
|
||||
...propsWithDefaultValue,
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
type UsePropsLogic = {
|
||||
props: Prop[];
|
||||
selectedInstance: Instance;
|
||||
@@ -77,7 +44,7 @@ type UsePropsLogic = {
|
||||
|
||||
const getPropFromMetaProps = (
|
||||
instanceId: Instance["id"],
|
||||
metaProps: MetaProps,
|
||||
metaProps: WsComponentPropsMeta["props"],
|
||||
name: string
|
||||
) => {
|
||||
const metaPropValue = metaProps[name];
|
||||
@@ -103,19 +70,15 @@ export const usePropsLogic = ({
|
||||
}: UsePropsLogic) => {
|
||||
const { id: instanceId, component } = selectedInstance;
|
||||
|
||||
const metaProps = useMemo(
|
||||
() => getComponentMetaProps(component) ?? {},
|
||||
[component]
|
||||
);
|
||||
const requiredPropsList = useMemo(
|
||||
() => getRequiredPropsList(component),
|
||||
[component]
|
||||
);
|
||||
const requiredPropsByName = new Map<string, Prop>();
|
||||
for (const name of requiredPropsList) {
|
||||
const meta = getComponentPropsMeta(component);
|
||||
const metaProps = meta?.props ?? {};
|
||||
const initialProps = meta?.initialProps ?? [];
|
||||
|
||||
const initialPropsByName = new Map<string, Prop>();
|
||||
for (const name of initialProps) {
|
||||
const prop = getPropFromMetaProps(instanceId, metaProps, name);
|
||||
if (prop) {
|
||||
requiredPropsByName.set(name, prop);
|
||||
initialPropsByName.set(name, prop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,8 +90,8 @@ export const usePropsLogic = ({
|
||||
|
||||
const propsById = new Map<string, Prop>();
|
||||
for (const prop of props) {
|
||||
if (requiredPropsByName.has(prop.name)) {
|
||||
requiredPropsByName.set(prop.name, prop);
|
||||
if (initialPropsByName.has(prop.name)) {
|
||||
initialPropsByName.set(prop.name, prop);
|
||||
continue;
|
||||
}
|
||||
if (addedPropsById.has(prop.id)) {
|
||||
@@ -141,14 +104,14 @@ export const usePropsLogic = ({
|
||||
// maintain added props order until panel is closed
|
||||
// to prevent inputs jumping while user typing
|
||||
const userProps = [
|
||||
...requiredPropsByName.values(),
|
||||
...initialPropsByName.values(),
|
||||
...propsById.values(),
|
||||
...addedPropsById.values(),
|
||||
];
|
||||
|
||||
const handleChangePropName = (prop: Prop, name: string) => {
|
||||
// prevent changing name of required props
|
||||
if (isRequired(prop)) {
|
||||
if (initialProps.includes(prop.name)) {
|
||||
return;
|
||||
}
|
||||
const typedValue = getValueFromPropMeta(metaProps[name]);
|
||||
@@ -161,7 +124,7 @@ export const usePropsLogic = ({
|
||||
|
||||
const handleDeleteProp = (prop: Prop) => {
|
||||
// required prop should never be deleted
|
||||
if (isRequired(prop)) {
|
||||
if (initialProps.includes(prop.name)) {
|
||||
return;
|
||||
}
|
||||
const id = prop.id;
|
||||
@@ -182,16 +145,12 @@ export const usePropsLogic = ({
|
||||
]);
|
||||
};
|
||||
|
||||
const isRequired = (prop: Prop) => {
|
||||
return prop.required || requiredPropsList.includes(prop.name);
|
||||
};
|
||||
|
||||
return {
|
||||
userProps,
|
||||
handleChangePropName,
|
||||
handleChangePropValue,
|
||||
handleDeleteProp,
|
||||
addEmptyProp,
|
||||
isRequired,
|
||||
initialProps,
|
||||
};
|
||||
};
|
||||
@@ -5,8 +5,10 @@ import type { CanvasData } from "@webstudio-is/project";
|
||||
import {
|
||||
createElementsTree,
|
||||
registerComponents,
|
||||
registerComponentsMeta,
|
||||
customComponentsMeta,
|
||||
registerComponentPropsMetas,
|
||||
registerComponentMetas,
|
||||
customComponentMetas,
|
||||
customComponentPropsMetas,
|
||||
setParams,
|
||||
type OnChangeChildren,
|
||||
} from "@webstudio-is/react-sdk";
|
||||
@@ -129,8 +131,8 @@ export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
|
||||
const [isPreviewMode] = useIsPreviewMode();
|
||||
|
||||
registerComponents(customComponents);
|
||||
|
||||
registerComponentsMeta(customComponentsMeta);
|
||||
registerComponentMetas(customComponentMetas);
|
||||
registerComponentPropsMetas(customComponentPropsMetas);
|
||||
|
||||
// e.g. toggling preview is still needed in both modes
|
||||
useShortcuts();
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"homepage": "https://webstudio.is",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "build-package --watch",
|
||||
"build": "build-package",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"checks": "pnpm typecheck && pnpm lint",
|
||||
"lint": "eslint ./src --ext .ts,.tsx --max-warnings 0"
|
||||
@@ -19,6 +21,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@webstudio-is/tsconfig": "workspace:^",
|
||||
"@webstudio-is/scripts": "workspace:^",
|
||||
"esbuild": "^0.14.25",
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
@@ -28,8 +31,10 @@
|
||||
"!*.test.*"
|
||||
],
|
||||
"bin": {
|
||||
"generate-arg-types": "./src/index.ts"
|
||||
"generate-arg-types": "./src/cli.ts"
|
||||
},
|
||||
"exports": "./lib/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"sideEffects": false
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PropItem } from "react-docgen-typescript";
|
||||
import { PropMeta } from "./types";
|
||||
|
||||
export type FilterPredicate = (prop: PropItem) => boolean;
|
||||
|
||||
@@ -6,7 +7,7 @@ const validAttributes = (prop: PropItem) => {
|
||||
if (prop.parent) {
|
||||
// Pass *HTML (both ButtonHTMLAttributes and HTMLAttributes), Aria, and SVG attributes through
|
||||
const matcher = /.?(HTML|SVG|Aria)Attributes/;
|
||||
// TODO: Add a test for this
|
||||
// @todo: Add a test for this
|
||||
return prop.parent.name.match(matcher);
|
||||
}
|
||||
// Always allow component's own props
|
||||
@@ -20,9 +21,7 @@ export const propsToArgTypes = (
|
||||
const filterFn = filter ?? validAttributes;
|
||||
const entries = Object.entries(props);
|
||||
return entries.reduce((result, current) => {
|
||||
// @todo need halp
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [propName, prop] = current as any;
|
||||
const [propName, prop] = current;
|
||||
|
||||
// Filter out props
|
||||
if (!filterFn(prop)) {
|
||||
@@ -34,8 +33,7 @@ export const propsToArgTypes = (
|
||||
result[propName] = argType;
|
||||
}
|
||||
return result;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}, {} as Record<string, any>);
|
||||
}, {} as Record<string, PropMeta>);
|
||||
};
|
||||
|
||||
const matchers = {
|
||||
@@ -43,58 +41,46 @@ const matchers = {
|
||||
date: /Date$/,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const getArgType = (propItem: any) => {
|
||||
const { type, name } = propItem;
|
||||
if (!type) {
|
||||
return undefined;
|
||||
}
|
||||
export const getArgType = (propItem: PropItem): PropMeta | undefined => {
|
||||
const { type, name, description, defaultValue } = propItem;
|
||||
|
||||
const overrides = {
|
||||
defaultValue: propItem.defaultValue?.value ?? null,
|
||||
options: propItem.options,
|
||||
required: propItem.required,
|
||||
};
|
||||
const common = (typeName: string = type.name) => ({
|
||||
type: { name: typeName, required: propItem.required },
|
||||
...(defaultValue?.value == null
|
||||
? {}
|
||||
: { defaultValue: defaultValue.value }),
|
||||
...(description ? { description } : {}),
|
||||
});
|
||||
|
||||
// args that end with background or color e.g. iconColor
|
||||
if (matchers.color && matchers.color.test(name)) {
|
||||
const controlType = propItem.type.name;
|
||||
|
||||
if (controlType === "string") {
|
||||
return { ...overrides, type: "color" };
|
||||
}
|
||||
if (matchers.color && matchers.color.test(name) && type.name === "string") {
|
||||
return PropMeta.parse({ ...common(), control: "color" });
|
||||
}
|
||||
|
||||
// args that end with date e.g. purchaseDate
|
||||
if (matchers.date && matchers.date.test(name)) {
|
||||
return { ...overrides, type: "date" };
|
||||
}
|
||||
|
||||
switch (type?.name) {
|
||||
case "array":
|
||||
return { ...overrides, type: "object" };
|
||||
switch (type.name) {
|
||||
case "boolean":
|
||||
case "Booleanish":
|
||||
return { ...overrides, type: "boolean" };
|
||||
case "string":
|
||||
return { ...overrides, type: "text" };
|
||||
return PropMeta.parse({ ...common("boolean"), control: "boolean" });
|
||||
case "number":
|
||||
return { ...overrides, type: "number" };
|
||||
return PropMeta.parse({ ...common("number"), control: "number" });
|
||||
case "enum": {
|
||||
const { value } = type;
|
||||
// Remove additional quotes from enum values
|
||||
// @ts-expect-error Original type has `any` type
|
||||
const values = value.map((val) => val.value.replace(/^"(.+)"$/, "$1"));
|
||||
return {
|
||||
...overrides,
|
||||
type: values?.length <= 5 ? "radio" : "select",
|
||||
options: values,
|
||||
};
|
||||
const values = type.value.map(({ value }: { value: string }) =>
|
||||
// remove additional quotes from enum values
|
||||
value.replace(/^"(.*)"$/, "$1")
|
||||
);
|
||||
return PropMeta.parse({
|
||||
...common("string"),
|
||||
control: {
|
||||
type: values.length <= 5 ? "radio" : "select",
|
||||
options: values,
|
||||
},
|
||||
});
|
||||
}
|
||||
case "function":
|
||||
case "symbol":
|
||||
return null;
|
||||
return;
|
||||
default:
|
||||
return { ...overrides, type: "text" };
|
||||
// @todo: we need some checks here. for example type.name can be "ImageLoader"
|
||||
return PropMeta.parse({ ...common(), control: "text" });
|
||||
}
|
||||
};
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env tsx
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import path from "path";
|
||||
import { withCustomConfig } from "react-docgen-typescript";
|
||||
import fg from "fast-glob";
|
||||
|
||||
import fs from "fs-extra";
|
||||
import { propsToArgTypes } from "./arg-types";
|
||||
|
||||
const GENERATED_FILES_DIR = "__generated__";
|
||||
|
||||
const options = {
|
||||
shouldExtractLiteralValuesFromEnum: true,
|
||||
shouldRemoveUndefinedFromOptional: true,
|
||||
};
|
||||
|
||||
const componentsGlobString = process.argv.pop();
|
||||
const tsConfigPath = path.resolve(process.cwd(), "./tsconfig.json");
|
||||
|
||||
if (typeof componentsGlobString === "undefined") {
|
||||
throw new Error(
|
||||
"Please provide glob patterns (space separated) as arguments to match your components"
|
||||
);
|
||||
}
|
||||
|
||||
// Search for components
|
||||
const globs = componentsGlobString.split(" ");
|
||||
const componentFiles = fg.sync(globs);
|
||||
|
||||
console.log(`Resolved tscofig.json at ${tsConfigPath}\n`);
|
||||
console.log(`Glob patterns used: \n${globs.join("\n")}\n`);
|
||||
console.log(`Found files to process: \n${componentFiles.join("\n")}\n`);
|
||||
|
||||
if (componentFiles.length === 0) {
|
||||
throw new Error("No component files found");
|
||||
}
|
||||
|
||||
// Create a parser with using your typescript config
|
||||
const tsConfigParser = withCustomConfig(tsConfigPath, options);
|
||||
|
||||
// For each component file generate argTypes based on the propTypes
|
||||
componentFiles.forEach((filePath) => {
|
||||
const generatedJsonDir = path.join(
|
||||
path.dirname(filePath),
|
||||
GENERATED_FILES_DIR
|
||||
);
|
||||
|
||||
if (!fs.existsSync(generatedJsonDir)) {
|
||||
fs.mkdirSync(generatedJsonDir, { recursive: true });
|
||||
}
|
||||
|
||||
const generateJsonFile = `${path.basename(filePath, ".tsx")}.props.json`;
|
||||
const generateJsonPath = path.join(generatedJsonDir, generateJsonFile);
|
||||
|
||||
const res = tsConfigParser.parse(filePath);
|
||||
const argTypes = propsToArgTypes(res[0].props);
|
||||
fs.ensureFileSync(generateJsonPath);
|
||||
fs.writeJsonSync(generateJsonPath, argTypes, { spaces: 2 });
|
||||
console.log(`Done generating argTypes for ${generateJsonPath}`);
|
||||
});
|
||||
Executable → Regular
+1
-72
@@ -1,72 +1 @@
|
||||
#!/usr/bin/env tsx
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
import { withCustomConfig } from "react-docgen-typescript";
|
||||
import fg from "fast-glob";
|
||||
|
||||
import fs from "fs-extra";
|
||||
import { propsToArgTypes } from "./arg-types";
|
||||
|
||||
const GENERATED_FILES_DIR = "__generated__";
|
||||
|
||||
const options = {
|
||||
shouldExtractLiteralValuesFromEnum: true,
|
||||
shouldRemoveUndefinedFromOptional: true,
|
||||
};
|
||||
|
||||
const componentsGlobString = process.argv.pop();
|
||||
const tsConfigPath = path.resolve(process.cwd(), "./tsconfig.json");
|
||||
|
||||
if (typeof componentsGlobString === "undefined") {
|
||||
throw new Error(
|
||||
"Please provide glob patterns (space separated) as arguments to match your components"
|
||||
);
|
||||
}
|
||||
|
||||
// Search for components
|
||||
const globs = componentsGlobString.split(" ");
|
||||
const componentFiles = fg.sync(globs);
|
||||
|
||||
console.log(`Resolved tscofig.json at ${tsConfigPath}\n`);
|
||||
console.log(`Glob patterns used: \n${globs.join("\n")}\n`);
|
||||
console.log(`Found files to process: \n${componentFiles.join("\n")}\n`);
|
||||
|
||||
if (componentFiles.length === 0) {
|
||||
throw new Error("No component files found");
|
||||
}
|
||||
|
||||
// Create a parser with using your typescript config
|
||||
const tsConfigParser = withCustomConfig(tsConfigPath, options);
|
||||
|
||||
const ArgTypes = z.record(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
required: z.boolean(),
|
||||
defaultValue: z.union([z.null(), z.string(), z.number(), z.boolean()]),
|
||||
options: z.array(z.string()).optional(),
|
||||
})
|
||||
);
|
||||
|
||||
// For each component file generate argTypes based on the propTypes
|
||||
componentFiles.forEach((filePath) => {
|
||||
const generatedJsonDir = path.join(
|
||||
path.dirname(filePath),
|
||||
GENERATED_FILES_DIR
|
||||
);
|
||||
|
||||
if (!fs.existsSync(generatedJsonDir)) {
|
||||
fs.mkdirSync(generatedJsonDir, { recursive: true });
|
||||
}
|
||||
|
||||
const generateJsonFile = `${path.basename(filePath, ".tsx")}.props.json`;
|
||||
const generateJsonPath = path.join(generatedJsonDir, generateJsonFile);
|
||||
|
||||
const res = tsConfigParser.parse(filePath);
|
||||
const argTypes = propsToArgTypes(res[0].props);
|
||||
ArgTypes.parse(argTypes);
|
||||
fs.ensureFileSync(generateJsonPath);
|
||||
fs.writeJsonSync(generateJsonPath, argTypes, { spaces: 2 });
|
||||
console.log(`Done generating argTypes for ${generateJsonPath}`);
|
||||
});
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,170 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// We're trying to have the same API as Storybook's ArgTypes here
|
||||
// https://storybook.js.org/docs/react/api/argtypes
|
||||
// https://github.com/ComponentDriven/csf/blob/next/src/story.ts#L63
|
||||
//
|
||||
// And have the same list of controls as Storybook (with some additions)
|
||||
// https://storybook.js.org/docs/react/essentials/controls
|
||||
|
||||
const dataType = <T extends string>(name: T) =>
|
||||
z.object({ name: z.literal(name), required: z.boolean() });
|
||||
|
||||
const controlType = <T extends string>(name: T) =>
|
||||
z.union([z.literal(name), z.object({ type: z.literal(name) })]);
|
||||
|
||||
const controlTypeWithOptions = <T extends string>(name: T) =>
|
||||
z.object({ type: z.literal(name), options: z.array(z.string()) });
|
||||
|
||||
const common = {
|
||||
/**
|
||||
* Label for the control (may not match the property name)
|
||||
*/
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
};
|
||||
|
||||
const Number = z.object({
|
||||
...common,
|
||||
type: dataType("number"),
|
||||
control: controlType("number"),
|
||||
defaultValue: z.number().optional(),
|
||||
});
|
||||
|
||||
const Range = z.object({
|
||||
...common,
|
||||
type: dataType("number"),
|
||||
control: controlType("range"),
|
||||
defaultValue: z.number().optional(),
|
||||
});
|
||||
|
||||
const Text = z.object({
|
||||
...common,
|
||||
type: z.object({
|
||||
// we don't specify a supported type here,
|
||||
// because we use Text as a fallback control for all sorts of types
|
||||
// for example: `boolean | "true" | "false" | "grammar" | "spelling"`
|
||||
name: z.string(),
|
||||
required: z.boolean(),
|
||||
}),
|
||||
control: controlType("text"),
|
||||
defaultValue: z.string().optional(),
|
||||
});
|
||||
|
||||
// Discussed with Storybook team here:
|
||||
// https://discord.com/channels/486522875931656193/689023212519948302/1075001187776069682
|
||||
// https://github.com/storybookjs/storybook/issues/21100
|
||||
const MultilineText = z.object({
|
||||
...common,
|
||||
type: dataType("string"),
|
||||
control: controlType("multiline-text"),
|
||||
defaultValue: z.string().optional(),
|
||||
});
|
||||
|
||||
const Color = z.object({
|
||||
...common,
|
||||
type: dataType("string"),
|
||||
control: controlType("color"),
|
||||
defaultValue: z.string().optional(),
|
||||
});
|
||||
|
||||
const Boolean = z.object({
|
||||
...common,
|
||||
type: dataType("boolean"),
|
||||
control: controlType("boolean"),
|
||||
defaultValue: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const Radio = z.object({
|
||||
...common,
|
||||
type: dataType("string"),
|
||||
control: controlTypeWithOptions("radio"),
|
||||
defaultValue: z.string().optional(),
|
||||
});
|
||||
|
||||
const InlineRadio = z.object({
|
||||
...common,
|
||||
type: dataType("string"),
|
||||
control: controlTypeWithOptions("inline-radio"),
|
||||
defaultValue: z.string().optional(),
|
||||
});
|
||||
|
||||
const Select = z.object({
|
||||
...common,
|
||||
type: dataType("string"),
|
||||
control: controlTypeWithOptions("select"),
|
||||
defaultValue: z.string().optional(),
|
||||
});
|
||||
|
||||
const MultiSelect = z.object({
|
||||
...common,
|
||||
type: dataType("string[]"),
|
||||
control: controlTypeWithOptions("multi-select"),
|
||||
defaultValue: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const Check = z.object({
|
||||
...common,
|
||||
type: dataType("string[]"),
|
||||
control: controlTypeWithOptions("check"),
|
||||
defaultValue: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const InlineCheck = z.object({
|
||||
...common,
|
||||
type: dataType("string[]"),
|
||||
control: controlTypeWithOptions("inline-check"),
|
||||
defaultValue: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
// @todo
|
||||
// remove this and add a generic "file" control instead with an "accept" option
|
||||
// to be in line with Storybook
|
||||
const FileImage = z.object({
|
||||
...common,
|
||||
type: dataType("string"),
|
||||
control: controlType("file-image"),
|
||||
defaultValue: z.string().optional(),
|
||||
});
|
||||
|
||||
// we neither generate object nor support it in props panel, listed here for completeness
|
||||
// can't use name "Object" for the variable because it causes bugs when Object.assign() is used in compiled output
|
||||
const ObjectType = z.object({
|
||||
...common,
|
||||
type: z.object({
|
||||
name: z.string(), // @todo not sure what type should be here
|
||||
required: z.boolean(),
|
||||
}),
|
||||
control: controlType("object"),
|
||||
defaultValue: z.any().optional(), // @todo maybe z.record(z.any())?
|
||||
...common,
|
||||
});
|
||||
|
||||
// we neither generate date nor support it in props panel, listed here for completeness
|
||||
const Date = z.object({
|
||||
...common,
|
||||
type: dataType("string"), // @todo not sure we should use string here and for defaultValue
|
||||
control: controlType("date"),
|
||||
defaultValue: z.string().optional(),
|
||||
...common,
|
||||
});
|
||||
|
||||
export const PropMeta = z.union([
|
||||
Number,
|
||||
Range,
|
||||
Text,
|
||||
MultilineText,
|
||||
Color,
|
||||
Boolean,
|
||||
Radio,
|
||||
InlineRadio,
|
||||
Select,
|
||||
MultiSelect,
|
||||
Check,
|
||||
InlineCheck,
|
||||
FileImage,
|
||||
ObjectType,
|
||||
Date,
|
||||
]);
|
||||
|
||||
export type PropMeta = z.infer<typeof PropMeta>;
|
||||
@@ -6,6 +6,6 @@
|
||||
"target": "ES2020",
|
||||
"baseUrl": ".",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./.bin"
|
||||
"outDir": "./lib"
|
||||
}
|
||||
}
|
||||
+653
-361
File diff suppressed because it is too large.
Load diff
@@ -2,7 +2,7 @@ import { Image } from "./image";
|
||||
import { Link } from "./link";
|
||||
import { RichTextLink } from "./rich-text-link";
|
||||
import { imageProps } from "@webstudio-is/image";
|
||||
import type { MetaProps } from "../../components/component-type";
|
||||
import { WsComponentPropsMeta } from "../../components/component-type";
|
||||
|
||||
export const customComponents = {
|
||||
Image,
|
||||
@@ -10,8 +10,14 @@ export const customComponents = {
|
||||
RichTextLink,
|
||||
};
|
||||
|
||||
export const customComponentsMeta = {
|
||||
Image: {
|
||||
props: imageProps as MetaProps,
|
||||
},
|
||||
export const customComponentPropsMetas = {
|
||||
Image: WsComponentPropsMeta.parse({
|
||||
props: {
|
||||
...imageProps,
|
||||
src: { ...imageProps.src, control: "file-image", name: "Source" },
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
// just for completeness, maybe we add soemthing here later
|
||||
export const customComponentMetas = {};
|
||||
+557
-305
File diff suppressed because it is too large.
Load diff
+557
-305
File diff suppressed because it is too large.
Load diff
+577
-320
File diff suppressed because it is too large.
Load diff
+616
-339
File diff suppressed because it is too large.
Load diff
+597
-329
File diff suppressed because it is too large.
Load diff
+566
-309
File diff suppressed because it is too large.
Load diff
+638
-352
File diff suppressed because it is too large.
Load diff
+725
-405
File diff suppressed because it is too large.
Load diff
+557
-305
File diff suppressed because it is too large.
Load diff
+616
-343
File diff suppressed because it is too large.
Load diff
+557
-305
File diff suppressed because it is too large.
Load diff
+616
-343
File diff suppressed because it is too large.
Load diff
+557
-305
File diff suppressed because it is too large.
Load diff
+557
-305
File diff suppressed because it is too large.
Load diff
+557
-305
File diff suppressed because it is too large.
Load diff
+557
-305
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
import { BodyIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/body.props.json";
|
||||
|
||||
const presetStyle = {
|
||||
@@ -52,12 +52,13 @@ const presetStyle = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "body",
|
||||
label: "Body",
|
||||
Icon: BodyIcon,
|
||||
props: props as MetaProps,
|
||||
presetStyle,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { FontBoldIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/bold.props.json";
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text-child",
|
||||
label: "Bold Text",
|
||||
Icon: FontBoldIcon,
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SquareIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/box.props.json";
|
||||
|
||||
const presetStyle = {
|
||||
@@ -9,12 +9,14 @@ const presetStyle = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "container",
|
||||
label: "Box",
|
||||
Icon: SquareIcon,
|
||||
presetStyle,
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
initialProps: ["tag"],
|
||||
});
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ButtonIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/button.props.json";
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text",
|
||||
label: "Button",
|
||||
Icon: ButtonIcon,
|
||||
children: ["Button text you can edit"],
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
initialProps: ["type"],
|
||||
});
|
||||
@@ -2,8 +2,16 @@ import { z } from "zod";
|
||||
import type { FunctionComponent } from "react";
|
||||
import type { IconProps } from "@webstudio-is/icons";
|
||||
import type { Style } from "@webstudio-is/css-data";
|
||||
import { PropMeta } from "@webstudio-is/generate-arg-types";
|
||||
|
||||
export type MetaProps = Partial<z.infer<typeof Props>>;
|
||||
// props are separated from the rest of the meta
|
||||
// so they can be exported separately and potentially tree-shaken
|
||||
export const WsComponentPropsMeta = z.object({
|
||||
props: z.record(PropMeta),
|
||||
initialProps: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type WsComponentPropsMeta = z.infer<typeof WsComponentPropsMeta>;
|
||||
|
||||
export type WsComponentMeta = {
|
||||
/**
|
||||
@@ -25,67 +33,19 @@ export type WsComponentMeta = {
|
||||
Icon: FunctionComponent<IconProps>;
|
||||
presetStyle?: Style;
|
||||
children?: Array<string>;
|
||||
props: MetaProps;
|
||||
initialProps?: Array<string>;
|
||||
};
|
||||
|
||||
const Props = z.record(
|
||||
z.union([
|
||||
z.object({
|
||||
type: z.literal("number"),
|
||||
required: z.boolean(),
|
||||
defaultValue: z.number().nullable(),
|
||||
}),
|
||||
|
||||
z.object({
|
||||
type: z.literal("text"),
|
||||
required: z.boolean(),
|
||||
defaultValue: z.string().nullable(),
|
||||
}),
|
||||
|
||||
z.object({
|
||||
type: z.literal("color"),
|
||||
required: z.boolean(),
|
||||
defaultValue: z.string().nullable(),
|
||||
}),
|
||||
|
||||
z.object({
|
||||
type: z.literal("boolean"),
|
||||
required: z.boolean(),
|
||||
defaultValue: z.boolean().nullable(),
|
||||
}),
|
||||
|
||||
z.object({
|
||||
type: z.enum([
|
||||
"radio",
|
||||
"inline-radio",
|
||||
"check",
|
||||
"inline-check",
|
||||
"multi-select",
|
||||
"select",
|
||||
]),
|
||||
required: z.boolean(),
|
||||
options: z.array(z.string()),
|
||||
defaultValue: z.string().nullable(),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
export const WsComponentMeta = z.lazy(() =>
|
||||
z.object({
|
||||
type: z.enum([
|
||||
"body",
|
||||
"container",
|
||||
"control",
|
||||
"embed",
|
||||
"rich-text",
|
||||
"rich-text-child",
|
||||
]),
|
||||
label: z.string(),
|
||||
Icon: z.any(),
|
||||
presetStyle: z.optional(z.any()),
|
||||
children: z.optional(z.array(z.string())),
|
||||
props: Props,
|
||||
initialProps: z.optional(z.array(z.string())),
|
||||
})
|
||||
);
|
||||
export const WsComponentMeta = z.object({
|
||||
type: z.enum([
|
||||
"body",
|
||||
"container",
|
||||
"control",
|
||||
"embed",
|
||||
"rich-text",
|
||||
"rich-text-child",
|
||||
]),
|
||||
label: z.string(),
|
||||
Icon: z.any(),
|
||||
presetStyle: z.optional(z.any()),
|
||||
children: z.optional(z.array(z.string())),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/form.props.json";
|
||||
|
||||
const presetStyle = {
|
||||
@@ -14,12 +14,13 @@ const presetStyle = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "container",
|
||||
label: "Form",
|
||||
Icon: FormIcon,
|
||||
presetStyle,
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,13 +1,15 @@
|
||||
import { HeadingIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/heading.props.json";
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text",
|
||||
label: "Heading",
|
||||
Icon: HeadingIcon,
|
||||
children: ["Heading you can edit"],
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
initialProps: ["tag"],
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ImageIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/image.props.json";
|
||||
|
||||
const presetStyle = {
|
||||
@@ -17,13 +17,17 @@ const presetStyle = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "embed",
|
||||
label: "Image",
|
||||
Icon: ImageIcon,
|
||||
presetStyle,
|
||||
props: props as MetaProps,
|
||||
initialProps: ["src", "width", "height", "alt", "loading"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props: {
|
||||
...props,
|
||||
src: { ...props.src, control: "file-image", name: "Source" },
|
||||
},
|
||||
initialProps: ["src", "width", "height", "alt", "loading"],
|
||||
});
|
||||
@@ -1,29 +1,7 @@
|
||||
import { test, expect } from "@jest/globals";
|
||||
import {
|
||||
getComponentNames,
|
||||
getComponentMeta,
|
||||
getComponentMetaProps,
|
||||
registerComponentsMeta,
|
||||
} from "./index";
|
||||
import { test } from "@jest/globals";
|
||||
import { getComponentNames, getComponentMeta } from "./index";
|
||||
import { WsComponentMeta } from "./component-type";
|
||||
|
||||
test.each(getComponentNames())("validating meta definition of %s", (name) => {
|
||||
WsComponentMeta.parse(getComponentMeta(name));
|
||||
});
|
||||
|
||||
test("defaultValue will be used from Native Component", () => {
|
||||
registerComponentsMeta({
|
||||
Image: {
|
||||
props: {
|
||||
src: {
|
||||
defaultValue: null,
|
||||
required: false,
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const props = getComponentMetaProps("Image");
|
||||
expect(props?.src?.defaultValue).toEqual("");
|
||||
});
|
||||
@@ -1,19 +1,40 @@
|
||||
import BodyMeta from "./body.ws";
|
||||
import BoxMeta from "./box.ws";
|
||||
import TextBlockMeta from "./text-block.ws";
|
||||
import HeadingMeta from "./heading.ws";
|
||||
import ParagraphMeta from "./paragraph.ws";
|
||||
import LinkMeta from "./link.ws";
|
||||
import RichTextLinkMeta from "./rich-text-link.ws";
|
||||
import SpanMeta from "./span.ws";
|
||||
import BoldMeta from "./bold.ws";
|
||||
import ItalicMeta from "./italic.ws";
|
||||
import SuperscriptMeta from "./superscript.ws";
|
||||
import SubscriptMeta from "./subscript.ws";
|
||||
import ButtonMeta from "./button.ws";
|
||||
import InputMeta from "./input.ws";
|
||||
import FormMeta from "./form.ws";
|
||||
import ImageMeta from "./image.ws";
|
||||
import { PropMeta } from "@webstudio-is/generate-arg-types";
|
||||
import type { WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
|
||||
import { meta as BodyMeta } from "./body.ws";
|
||||
import { meta as BoxMeta } from "./box.ws";
|
||||
import { meta as TextBlockMeta } from "./text-block.ws";
|
||||
import { meta as HeadingMeta } from "./heading.ws";
|
||||
import { meta as ParagraphMeta } from "./paragraph.ws";
|
||||
import { meta as LinkMeta } from "./link.ws";
|
||||
import { meta as RichTextLinkMeta } from "./rich-text-link.ws";
|
||||
import { meta as SpanMeta } from "./span.ws";
|
||||
import { meta as BoldMeta } from "./bold.ws";
|
||||
import { meta as ItalicMeta } from "./italic.ws";
|
||||
import { meta as SuperscriptMeta } from "./superscript.ws";
|
||||
import { meta as SubscriptMeta } from "./subscript.ws";
|
||||
import { meta as ButtonMeta } from "./button.ws";
|
||||
import { meta as InputMeta } from "./input.ws";
|
||||
import { meta as FormMeta } from "./form.ws";
|
||||
import { meta as ImageMeta } from "./image.ws";
|
||||
|
||||
// these are huge JSON objects that we want to be tree-shaken when not used!
|
||||
import { propsMeta as BodyMetaPropsMeta } from "./body.ws";
|
||||
import { propsMeta as BoxMetaPropsMeta } from "./box.ws";
|
||||
import { propsMeta as TextBlockMetaPropsMeta } from "./text-block.ws";
|
||||
import { propsMeta as HeadingMetaPropsMeta } from "./heading.ws";
|
||||
import { propsMeta as ParagraphMetaPropsMeta } from "./paragraph.ws";
|
||||
import { propsMeta as LinkMetaPropsMeta } from "./link.ws";
|
||||
import { propsMeta as RichTextLinkMetaPropsMeta } from "./rich-text-link.ws";
|
||||
import { propsMeta as SpanMetaPropsMeta } from "./span.ws";
|
||||
import { propsMeta as BoldMetaPropsMeta } from "./bold.ws";
|
||||
import { propsMeta as ItalicMetaPropsMeta } from "./italic.ws";
|
||||
import { propsMeta as SuperscriptMetaPropsMeta } from "./superscript.ws";
|
||||
import { propsMeta as SubscriptMetaPropsMeta } from "./subscript.ws";
|
||||
import { propsMeta as ButtonMetaPropsMeta } from "./button.ws";
|
||||
import { propsMeta as InputMetaPropsMeta } from "./input.ws";
|
||||
import { propsMeta as FormMetaPropsMeta } from "./form.ws";
|
||||
import { propsMeta as ImageMetaPropsMeta } from "./image.ws";
|
||||
|
||||
import { Body } from "./body";
|
||||
import { Box } from "./box";
|
||||
@@ -32,9 +53,7 @@ import { Input } from "./input";
|
||||
import { Form } from "./form";
|
||||
import { Image } from "./image";
|
||||
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
|
||||
const meta = {
|
||||
const defaultMetas: Record<string, WsComponentMeta> = {
|
||||
Box: BoxMeta,
|
||||
Body: BodyMeta,
|
||||
TextBlock: TextBlockMeta,
|
||||
@@ -51,9 +70,79 @@ const meta = {
|
||||
Input: InputMeta,
|
||||
Form: FormMeta,
|
||||
Image: ImageMeta,
|
||||
} as const;
|
||||
};
|
||||
|
||||
const components = {
|
||||
let currentMetas = defaultMetas;
|
||||
|
||||
export const getComponentMeta = (name: string): WsComponentMeta | undefined => {
|
||||
return currentMetas[name];
|
||||
};
|
||||
|
||||
export const registerComponentMetas = (
|
||||
overrides: Record<string, Partial<WsComponentMeta>>
|
||||
) => {
|
||||
const result: typeof currentMetas = {};
|
||||
for (const name of Object.keys(defaultMetas)) {
|
||||
result[name] = { ...defaultMetas[name], ...overrides[name] };
|
||||
}
|
||||
currentMetas = result;
|
||||
};
|
||||
|
||||
const defaultPropsMetas: Record<string, WsComponentPropsMeta> = {
|
||||
Box: BoxMetaPropsMeta,
|
||||
Body: BodyMetaPropsMeta,
|
||||
TextBlock: TextBlockMetaPropsMeta,
|
||||
Heading: HeadingMetaPropsMeta,
|
||||
Paragraph: ParagraphMetaPropsMeta,
|
||||
Link: LinkMetaPropsMeta,
|
||||
RichTextLink: RichTextLinkMetaPropsMeta,
|
||||
Span: SpanMetaPropsMeta,
|
||||
Bold: BoldMetaPropsMeta,
|
||||
Italic: ItalicMetaPropsMeta,
|
||||
Superscript: SuperscriptMetaPropsMeta,
|
||||
Subscript: SubscriptMetaPropsMeta,
|
||||
Button: ButtonMetaPropsMeta,
|
||||
Input: InputMetaPropsMeta,
|
||||
Form: FormMetaPropsMeta,
|
||||
Image: ImageMetaPropsMeta,
|
||||
};
|
||||
|
||||
let registeredPropsMetas: Record<string, Partial<WsComponentPropsMeta>> = {};
|
||||
|
||||
// we start as `undefined` because pre-computing will likely kill tree-shaking
|
||||
let currentPropsMetas: Record<string, WsComponentPropsMeta> | undefined =
|
||||
undefined;
|
||||
|
||||
export const getComponentPropsMeta = (
|
||||
name: string
|
||||
): WsComponentPropsMeta | undefined => {
|
||||
if (currentPropsMetas === undefined) {
|
||||
currentPropsMetas = {};
|
||||
for (const name of Object.keys(defaultPropsMetas)) {
|
||||
const props = computeProps(
|
||||
defaultPropsMetas[name],
|
||||
registeredPropsMetas[name] ?? {}
|
||||
);
|
||||
const initialProps = computeInitialProps(
|
||||
props,
|
||||
defaultPropsMetas[name],
|
||||
registeredPropsMetas[name] ?? {}
|
||||
);
|
||||
currentPropsMetas[name] = { props, initialProps };
|
||||
}
|
||||
}
|
||||
|
||||
return currentPropsMetas[name];
|
||||
};
|
||||
|
||||
export const registerComponentPropsMetas = (
|
||||
metas: Record<string, WsComponentPropsMeta>
|
||||
) => {
|
||||
registeredPropsMetas = metas;
|
||||
currentPropsMetas = undefined;
|
||||
};
|
||||
|
||||
const defaultComponents = {
|
||||
Box,
|
||||
Body,
|
||||
TextBlock,
|
||||
@@ -72,20 +161,15 @@ const components = {
|
||||
Image,
|
||||
} as const;
|
||||
|
||||
export type ComponentName = keyof typeof components;
|
||||
export type ComponentName = keyof typeof defaultComponents;
|
||||
type RegisteredComponents = Partial<{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
[p in ComponentName]: {};
|
||||
}>;
|
||||
|
||||
type RegisteredComponentsMeta = Partial<{
|
||||
[p in ComponentName]: Partial<WsComponentMeta>;
|
||||
[name in ComponentName]: {};
|
||||
}>;
|
||||
|
||||
let registeredComponents: RegisteredComponents | null = null;
|
||||
let registeredComponentsMeta: RegisteredComponentsMeta | null = null;
|
||||
|
||||
const componentNames = Object.keys(components) as ComponentName[];
|
||||
const componentNames = Object.keys(defaultComponents) as ComponentName[];
|
||||
|
||||
export const getComponentNames = (): ComponentName[] => {
|
||||
const uniqueNames = new Set([
|
||||
@@ -96,60 +180,14 @@ export const getComponentNames = (): ComponentName[] => {
|
||||
return [...uniqueNames.values()] as ComponentName[];
|
||||
};
|
||||
|
||||
export const getComponentMeta = (name: string): undefined | WsComponentMeta => {
|
||||
const componentMeta = meta[name as ComponentName];
|
||||
if (registeredComponentsMeta != null && name in registeredComponentsMeta) {
|
||||
return {
|
||||
...componentMeta,
|
||||
...registeredComponentsMeta[name as ComponentName],
|
||||
};
|
||||
}
|
||||
|
||||
return componentMeta;
|
||||
};
|
||||
|
||||
export const getComponent = (
|
||||
name: string
|
||||
): undefined | typeof components[ComponentName] => {
|
||||
): undefined | typeof defaultComponents[ComponentName] => {
|
||||
return registeredComponents != null && name in registeredComponents
|
||||
? (registeredComponents[
|
||||
name as ComponentName
|
||||
] as typeof components[ComponentName])
|
||||
: components[name as ComponentName];
|
||||
};
|
||||
|
||||
export const getComponentMetaProps = (name: string): undefined | MetaProps => {
|
||||
const componentMeta = meta[name as ComponentName];
|
||||
if (registeredComponentsMeta != null && name in registeredComponentsMeta) {
|
||||
const registeredComponentMeta =
|
||||
registeredComponentsMeta[name as ComponentName];
|
||||
const allMetaPropKeys = new Set([
|
||||
...Object.keys(componentMeta?.props ?? {}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
...Object.keys(registeredComponentMeta!.props!),
|
||||
]);
|
||||
|
||||
const props: MetaProps = {};
|
||||
/**
|
||||
* Merge props, taking non null defaultValue and required=true from meta
|
||||
**/
|
||||
for (const key of allMetaPropKeys.values()) {
|
||||
props[key] = {
|
||||
...componentMeta?.props[key],
|
||||
...registeredComponentMeta?.props?.[key],
|
||||
defaultValue:
|
||||
registeredComponentMeta?.props?.[key]?.defaultValue ??
|
||||
componentMeta?.props[key]?.defaultValue ??
|
||||
null,
|
||||
required:
|
||||
registeredComponentMeta?.props?.[key]?.required ||
|
||||
componentMeta?.props[key]?.required,
|
||||
} as MetaProps[string];
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
return componentMeta?.props;
|
||||
] as typeof defaultComponents[ComponentName])
|
||||
: defaultComponents[name as ComponentName];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -161,8 +199,44 @@ export const registerComponents = (components: RegisteredComponents) => {
|
||||
registeredComponents = components;
|
||||
};
|
||||
|
||||
export const registerComponentsMeta = (
|
||||
componentsMeta: RegisteredComponentsMeta
|
||||
const computeProps = (
|
||||
defaults: WsComponentPropsMeta,
|
||||
overrides: Partial<WsComponentPropsMeta>
|
||||
) => {
|
||||
registeredComponentsMeta = componentsMeta;
|
||||
if (overrides) {
|
||||
const allNames = new Set([
|
||||
...Object.keys(defaults.props ?? {}),
|
||||
...Object.keys(overrides?.props ?? {}),
|
||||
]).values();
|
||||
|
||||
const result: WsComponentPropsMeta["props"] = {};
|
||||
for (const name of allNames) {
|
||||
result[name] = PropMeta.parse({
|
||||
...defaults.props[name],
|
||||
...overrides?.props?.[name],
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return defaults.props;
|
||||
};
|
||||
|
||||
const computeInitialProps = (
|
||||
props: WsComponentPropsMeta["props"],
|
||||
defaults: WsComponentPropsMeta,
|
||||
overrides: Partial<WsComponentPropsMeta>
|
||||
): Array<string> => {
|
||||
const initialProps = overrides?.initialProps ?? defaults?.initialProps ?? [];
|
||||
const requiredProps = props
|
||||
? Object.entries(props)
|
||||
.filter(
|
||||
([name, value]) =>
|
||||
value?.type.required && initialProps.includes(name) === false
|
||||
)
|
||||
.map(([name]) => name)
|
||||
: [];
|
||||
|
||||
// order of initialProps must be preserved
|
||||
return [...initialProps, ...requiredProps];
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
import { InputIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/input.props.json";
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "control",
|
||||
label: "Input",
|
||||
Icon: InputIcon,
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FontItalicIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/italic.props.json";
|
||||
|
||||
const presetStyle = {
|
||||
@@ -9,12 +9,13 @@ const presetStyle = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text-child",
|
||||
label: "Italic Text",
|
||||
Icon: FontItalicIcon,
|
||||
presetStyle,
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link2Icon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/link.props.json";
|
||||
|
||||
const presetStyle = {
|
||||
@@ -14,13 +14,15 @@ const presetStyle = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text",
|
||||
label: "Link",
|
||||
Icon: Link2Icon,
|
||||
presetStyle,
|
||||
children: ["Link text you can edit"],
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
initialProps: ["href"],
|
||||
});
|
||||
@@ -1,13 +1,14 @@
|
||||
import { TextAlignLeftIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/paragraph.props.json";
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text",
|
||||
label: "Paragraph",
|
||||
Icon: TextAlignLeftIcon,
|
||||
children: ["Pragraph you can edit"],
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Link2Icon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/rich-text-link.props.json";
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text-child",
|
||||
label: "Link",
|
||||
Icon: Link2Icon,
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { BrushIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/span.props.json";
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text-child",
|
||||
label: "Styled Text",
|
||||
Icon: BrushIcon,
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { SubscriptIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/subscript.props.json";
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text-child",
|
||||
label: "Subscript Text",
|
||||
Icon: SubscriptIcon,
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { SuperscriptIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/superscript.props.json";
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text-child",
|
||||
label: "Superscript Text",
|
||||
Icon: SuperscriptIcon,
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TextIcon } from "@webstudio-is/icons";
|
||||
import type { WsComponentMeta, MetaProps } from "./component-type";
|
||||
import { type WsComponentMeta, WsComponentPropsMeta } from "./component-type";
|
||||
import props from "./__generated__/text-block.props.json";
|
||||
|
||||
const presetStyle = {
|
||||
@@ -10,13 +10,14 @@ const presetStyle = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const meta: WsComponentMeta = {
|
||||
export const meta: WsComponentMeta = {
|
||||
type: "rich-text",
|
||||
label: "Text Block",
|
||||
Icon: TextIcon,
|
||||
presetStyle,
|
||||
children: ["Block of text you can edit"],
|
||||
props: props as MetaProps,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const propsMeta = WsComponentPropsMeta.parse({
|
||||
props,
|
||||
});
|
||||
@@ -5,6 +5,10 @@ export * from "./pubsub";
|
||||
export * from "./app";
|
||||
export {
|
||||
customComponents,
|
||||
customComponentsMeta,
|
||||
customComponentMetas,
|
||||
customComponentPropsMetas,
|
||||
} from "./app/custom-components";
|
||||
export type { MetaProps, WsComponentMeta } from "./components/component-type";
|
||||
export type {
|
||||
WsComponentPropsMeta,
|
||||
WsComponentMeta,
|
||||
} from "./components/component-type";
|
||||
Generated
+2
@@ -616,6 +616,7 @@ importers:
|
||||
packages/generate-arg-types:
|
||||
specifiers:
|
||||
'@types/fs-extra': ^9.0.13
|
||||
'@webstudio-is/scripts': workspace:^
|
||||
'@webstudio-is/tsconfig': workspace:^
|
||||
esbuild: 0.15.13
|
||||
fast-glob: ^3.2.12
|
||||
@@ -630,6 +631,7 @@ importers:
|
||||
react-docgen-typescript: 2.2.2_typescript@4.9.5
|
||||
zod: 3.19.1
|
||||
devDependencies:
|
||||
'@webstudio-is/scripts': link:../scripts
|
||||
'@webstudio-is/tsconfig': link:../tsconfig
|
||||
esbuild: 0.15.13
|
||||
typescript: 4.9.5
|
||||
|
||||
Reference in new issue
Block a user