mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
feat: use resettable blue label for properties (#5000)
Closes https://github.com/webstudio-is/webstudio/issues/4787 - properties now have blue labels when value is specified - clicking on label shows tooltip - alt+click does reset/delete - responsive layout in text control now wraps when label is >30% <img width="317" alt="Screenshot 2025-03-14 at 20 25 50" src="https://github.com/user-attachments/assets/acfa1487-59b8-4742-9374-17f5501f5cbf" /> <img width="362" alt="Screenshot 2025-03-14 at 20 25 40" src="https://github.com/user-attachments/assets/12d07775-f926-4b40-9fd9-571723eb43b0" />
This commit is contained in:
1 parent
b14676bfd6
commit
d7bb1141ba
16 files changed
+280
-249
No files matched your search
@@ -6,22 +6,19 @@ import {
|
||||
} from "~/builder/shared/binding-popover";
|
||||
import {
|
||||
type ControlProps,
|
||||
Label,
|
||||
RemovePropButton,
|
||||
$selectedInstanceScope,
|
||||
updateExpressionValue,
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
export const BooleanControl = ({
|
||||
meta,
|
||||
prop,
|
||||
propName,
|
||||
computedValue,
|
||||
deletable,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: ControlProps<"boolean">) => {
|
||||
const label = humanizeAttribute(meta.label || propName);
|
||||
const { scope, aliases } = useStore($selectedInstanceScope);
|
||||
@@ -34,18 +31,14 @@ export const BooleanControl = ({
|
||||
return (
|
||||
<Grid
|
||||
css={{
|
||||
gridTemplateColumns: deletable
|
||||
? `1fr max-content max-content`
|
||||
: `1fr max-content`,
|
||||
gridTemplateColumns: `1fr max-content`,
|
||||
minHeight: theme.spacing[13],
|
||||
justifyItems: "start",
|
||||
}}
|
||||
align="center"
|
||||
gap="2"
|
||||
>
|
||||
<Label description={meta.description} readOnly={overwritable === false}>
|
||||
{label}
|
||||
</Label>
|
||||
<PropertyLabel name={propName} readOnly={overwritable === false} />
|
||||
<BindingControl>
|
||||
<Switch
|
||||
disabled={overwritable === false}
|
||||
@@ -76,7 +69,6 @@ export const BooleanControl = ({
|
||||
}
|
||||
/>
|
||||
</BindingControl>
|
||||
{deletable && <RemovePropButton onClick={onDelete} />}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
const add = (array: string[], item: string) => {
|
||||
if (array.includes(item)) {
|
||||
@@ -34,9 +35,7 @@ export const CheckControl = ({
|
||||
prop,
|
||||
propName,
|
||||
computedValue,
|
||||
deletable,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: ControlProps<"check" | "inline-check" | "multi-select">) => {
|
||||
const value = Array.isArray(computedValue)
|
||||
? computedValue.map((item) => String(item))
|
||||
@@ -57,16 +56,8 @@ export const CheckControl = ({
|
||||
return (
|
||||
<VerticalLayout
|
||||
label={
|
||||
<Label
|
||||
htmlFor={`${id}:${options[0]}`}
|
||||
description={meta.description}
|
||||
readOnly={overwritable === false}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
<PropertyLabel name={propName} readOnly={overwritable === false} />
|
||||
}
|
||||
deletable={deletable}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<BindingControl>
|
||||
{options.map((option) => (
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { CodeEditor } from "~/builder/shared/code-editor";
|
||||
import {
|
||||
BindingControl,
|
||||
BindingPopover,
|
||||
} from "~/builder/shared/binding-popover";
|
||||
import {
|
||||
useLocalValue,
|
||||
type ControlProps,
|
||||
VerticalLayout,
|
||||
Label,
|
||||
updateExpressionValue,
|
||||
$selectedInstanceScope,
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
@@ -29,6 +14,21 @@ import {
|
||||
theme,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { InfoCircleIcon } from "@webstudio-is/icons";
|
||||
import { CodeEditor } from "~/builder/shared/code-editor";
|
||||
import {
|
||||
BindingControl,
|
||||
BindingPopover,
|
||||
} from "~/builder/shared/binding-popover";
|
||||
import {
|
||||
useLocalValue,
|
||||
type ControlProps,
|
||||
VerticalLayout,
|
||||
updateExpressionValue,
|
||||
$selectedInstanceScope,
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
const ErrorInfo = ({
|
||||
error,
|
||||
@@ -126,9 +126,7 @@ export const CodeControl = ({
|
||||
prop,
|
||||
propName,
|
||||
computedValue,
|
||||
deletable,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: ControlProps<"code"> | ControlProps<"codetext">) => {
|
||||
const [error, setError] = useState<Error>();
|
||||
const metaOverride = {
|
||||
@@ -177,17 +175,10 @@ export const CodeControl = ({
|
||||
<VerticalLayout
|
||||
label={
|
||||
<Flex gap="1" align="center">
|
||||
<Label
|
||||
description={metaOverride.description}
|
||||
readOnly={overwritable === false}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
<PropertyLabel name={propName} readOnly={overwritable === false} />
|
||||
{errorInfo}
|
||||
</Flex>
|
||||
}
|
||||
deletable={deletable}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<BindingControl>
|
||||
<CodeEditor
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
type ControlProps,
|
||||
VerticalLayout,
|
||||
useLocalValue,
|
||||
Label,
|
||||
updateExpressionValue,
|
||||
$selectedInstanceScope,
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import { SelectAsset } from "./select-asset";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
const UrlInput = ({
|
||||
id,
|
||||
@@ -47,7 +47,6 @@ export const FileControl = ({
|
||||
prop,
|
||||
propName,
|
||||
computedValue,
|
||||
deletable,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: ControlProps<"file">) => {
|
||||
@@ -81,15 +80,7 @@ export const FileControl = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<VerticalLayout
|
||||
label={
|
||||
<Label htmlFor={id} description={meta.description}>
|
||||
{label}
|
||||
</Label>
|
||||
}
|
||||
deletable={deletable}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<VerticalLayout label={<PropertyLabel name={propName} />}>
|
||||
<Flex css={{ gap: theme.spacing[3] }} direction="column" justify="center">
|
||||
<BindingControl>
|
||||
<UrlInput
|
||||
|
||||
@@ -5,11 +5,9 @@ import {
|
||||
type ControlProps,
|
||||
useLocalValue,
|
||||
VerticalLayout,
|
||||
Label,
|
||||
updateExpressionValue,
|
||||
$selectedInstanceScope,
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import {
|
||||
ExpressionEditor,
|
||||
@@ -19,15 +17,13 @@ import {
|
||||
BindingControl,
|
||||
BindingPopover,
|
||||
} from "~/builder/shared/binding-popover";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
export const JsonControl = ({
|
||||
meta,
|
||||
prop,
|
||||
propName,
|
||||
computedValue,
|
||||
deletable,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: ControlProps<"json">) => {
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const valueString = formatValue(computedValue ?? "");
|
||||
@@ -50,7 +46,6 @@ export const JsonControl = ({
|
||||
// empty block
|
||||
}
|
||||
});
|
||||
const label = humanizeAttribute(meta.label || propName);
|
||||
|
||||
const { scope, aliases } = useStore($selectedInstanceScope);
|
||||
const expression = prop?.type === "expression" ? prop.value : valueString;
|
||||
@@ -61,12 +56,8 @@ export const JsonControl = ({
|
||||
return (
|
||||
<VerticalLayout
|
||||
label={
|
||||
<Label description={meta.description} readOnly={overwritable === false}>
|
||||
{label}
|
||||
</Label>
|
||||
<PropertyLabel name={propName} readOnly={overwritable === false} />
|
||||
}
|
||||
deletable={deletable}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<BindingControl>
|
||||
<ExpressionEditor
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
type ControlProps,
|
||||
useLocalValue,
|
||||
ResponsiveLayout,
|
||||
Label,
|
||||
updateExpressionValue,
|
||||
$selectedInstanceScope,
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
export const NumberControl = ({
|
||||
meta,
|
||||
@@ -22,8 +22,6 @@ export const NumberControl = ({
|
||||
propName,
|
||||
computedValue,
|
||||
onChange,
|
||||
deletable,
|
||||
onDelete,
|
||||
}: ControlProps<"number">) => {
|
||||
const id = useId();
|
||||
|
||||
@@ -56,16 +54,8 @@ export const NumberControl = ({
|
||||
return (
|
||||
<ResponsiveLayout
|
||||
label={
|
||||
<Label
|
||||
htmlFor={id}
|
||||
description={meta.description}
|
||||
readOnly={overwritable === false}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
<PropertyLabel name={propName} readOnly={overwritable === false} />
|
||||
}
|
||||
deletable={deletable}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<BindingControl>
|
||||
<InputField
|
||||
|
||||
@@ -14,15 +14,14 @@ import {
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
export const RadioControl = ({
|
||||
meta,
|
||||
prop,
|
||||
propName,
|
||||
computedValue,
|
||||
deletable,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: ControlProps<"radio" | "inline-radio">) => {
|
||||
const value = computedValue === undefined ? undefined : String(computedValue);
|
||||
// making sure that the current value is in the list of options
|
||||
@@ -43,16 +42,8 @@ export const RadioControl = ({
|
||||
return (
|
||||
<VerticalLayout
|
||||
label={
|
||||
<Label
|
||||
htmlFor={id}
|
||||
description={meta.description}
|
||||
readOnly={overwritable === false}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
<PropertyLabel name={propName} readOnly={overwritable === false} />
|
||||
}
|
||||
deletable={deletable}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<BindingControl>
|
||||
<RadioGroup
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { useId } from "react";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { InputField } from "@webstudio-is/design-system";
|
||||
@@ -10,22 +11,20 @@ import {
|
||||
import {
|
||||
type ControlProps,
|
||||
useLocalValue,
|
||||
ResponsiveLayout,
|
||||
Label,
|
||||
humanizeAttribute,
|
||||
VerticalLayout,
|
||||
} from "../shared";
|
||||
import { $resources } from "~/shared/nano-states";
|
||||
import { $selectedInstanceResourceScope } from "../resource-panel";
|
||||
import { computeExpression } from "~/shared/data-variables";
|
||||
import { updateWebstudioData } from "~/shared/instance-utils";
|
||||
import { nanoid } from "nanoid";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
export const ResourceControl = ({
|
||||
meta,
|
||||
prop,
|
||||
propName,
|
||||
instanceId,
|
||||
deletable,
|
||||
}: ControlProps<"resource">) => {
|
||||
const resources = useStore($resources);
|
||||
const { variableValues, scope, aliases } = useStore(
|
||||
@@ -98,17 +97,6 @@ export const ResourceControl = ({
|
||||
});
|
||||
};
|
||||
|
||||
const deletePropAndResource = () => {
|
||||
updateWebstudioData((data) => {
|
||||
if (prop?.type === "resource") {
|
||||
data.resources.delete(prop.value);
|
||||
}
|
||||
if (prop) {
|
||||
data.props.delete(prop.id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const id = useId();
|
||||
const label = humanizeAttribute(meta.label || propName);
|
||||
let variant: BindingVariant = "bound";
|
||||
@@ -122,14 +110,8 @@ export const ResourceControl = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<ResponsiveLayout
|
||||
label={
|
||||
<Label htmlFor={id} description={meta.description} readOnly={readOnly}>
|
||||
{label}
|
||||
</Label>
|
||||
}
|
||||
deletable={deletable}
|
||||
onDelete={deletePropAndResource}
|
||||
<VerticalLayout
|
||||
label={<PropertyLabel name={propName} readOnly={readOnly} />}
|
||||
>
|
||||
<BindingControl>
|
||||
<InputField
|
||||
@@ -156,6 +138,6 @@ export const ResourceControl = ({
|
||||
}
|
||||
/>
|
||||
</BindingControl>
|
||||
</ResponsiveLayout>
|
||||
</VerticalLayout>
|
||||
);
|
||||
};
|
||||
@@ -8,21 +8,19 @@ import {
|
||||
import {
|
||||
type ControlProps,
|
||||
VerticalLayout,
|
||||
Label,
|
||||
$selectedInstanceScope,
|
||||
updateExpressionValue,
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
export const SelectControl = ({
|
||||
meta,
|
||||
prop,
|
||||
propName,
|
||||
computedValue,
|
||||
deletable,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: ControlProps<"select">) => {
|
||||
const id = useId();
|
||||
|
||||
@@ -44,16 +42,8 @@ export const SelectControl = ({
|
||||
return (
|
||||
<VerticalLayout
|
||||
label={
|
||||
<Label
|
||||
htmlFor={id}
|
||||
description={meta.description}
|
||||
readOnly={overwritable === false}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
<PropertyLabel name={propName} readOnly={overwritable === false} />
|
||||
}
|
||||
deletable={deletable}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<BindingControl>
|
||||
<Select
|
||||
|
||||
@@ -84,8 +84,6 @@ export const TextContent = ({
|
||||
{label}
|
||||
</Label>
|
||||
}
|
||||
deletable={false}
|
||||
onDelete={() => {}}
|
||||
>
|
||||
<BindingControl>
|
||||
<TextArea
|
||||
|
||||
@@ -9,21 +9,19 @@ import {
|
||||
type ControlProps,
|
||||
useLocalValue,
|
||||
ResponsiveLayout,
|
||||
Label,
|
||||
updateExpressionValue,
|
||||
$selectedInstanceScope,
|
||||
useBindingState,
|
||||
humanizeAttribute,
|
||||
} from "../shared";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
export const TextControl = ({
|
||||
meta,
|
||||
prop,
|
||||
propName,
|
||||
deletable,
|
||||
computedValue,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: ControlProps<"text">) => {
|
||||
const localValue = useLocalValue(String(computedValue ?? ""), (value) => {
|
||||
if (prop?.type === "expression") {
|
||||
@@ -76,21 +74,11 @@ export const TextControl = ({
|
||||
</BindingControl>
|
||||
);
|
||||
|
||||
const labelElement = (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
description={meta.description}
|
||||
readOnly={overwritable === false}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
);
|
||||
|
||||
return (
|
||||
<ResponsiveLayout
|
||||
label={labelElement}
|
||||
deletable={deletable}
|
||||
onDelete={onDelete}
|
||||
label={
|
||||
<PropertyLabel name={propName} readOnly={overwritable === false} />
|
||||
}
|
||||
>
|
||||
{input}
|
||||
</ResponsiveLayout>
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
} from "../shared";
|
||||
import { SelectAsset } from "./select-asset";
|
||||
import { createRootFolder } from "@webstudio-is/project-build";
|
||||
import { PropertyLabel } from "../property-label";
|
||||
|
||||
type UrlControlProps = ControlProps<"url">;
|
||||
|
||||
@@ -461,7 +462,6 @@ export const UrlControl = ({
|
||||
prop,
|
||||
propName,
|
||||
computedValue,
|
||||
deletable,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: UrlControlProps) => {
|
||||
@@ -484,15 +484,7 @@ export const UrlControl = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<VerticalLayout
|
||||
label={
|
||||
<Label htmlFor={id} description={meta.description}>
|
||||
{label}
|
||||
</Label>
|
||||
}
|
||||
deletable={deletable}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<VerticalLayout label={<PropertyLabel name={propName} />}>
|
||||
<Flex
|
||||
css={{
|
||||
py: theme.spacing[2],
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { computed } from "nanostores";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Kbd,
|
||||
Label,
|
||||
Text,
|
||||
Tooltip,
|
||||
rawTheme,
|
||||
theme,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { InfoCircleIcon } from "@webstudio-is/icons";
|
||||
import type { Prop } from "@webstudio-is/sdk";
|
||||
import { showAttribute } from "@webstudio-is/react-sdk";
|
||||
import { updateWebstudioData } from "~/shared/instance-utils";
|
||||
import { $selectedInstance } from "~/shared/awareness";
|
||||
import { $props, $registeredComponentPropsMetas } from "~/shared/nano-states";
|
||||
import { humanizeAttribute, showAttributeMeta } from "./shared";
|
||||
|
||||
const usePropMeta = (name: string) => {
|
||||
const store = useMemo(() => {
|
||||
return computed(
|
||||
[$selectedInstance, $registeredComponentPropsMetas],
|
||||
(instance, propsMetas) => {
|
||||
if (name === showAttribute) {
|
||||
return showAttributeMeta;
|
||||
}
|
||||
const metas = propsMetas.get(instance?.component ?? "");
|
||||
const propMeta = metas?.props[name];
|
||||
return propMeta;
|
||||
}
|
||||
);
|
||||
}, [name]);
|
||||
return useStore(store);
|
||||
};
|
||||
|
||||
const $selectedInstanceProps = computed(
|
||||
[$selectedInstance, $props],
|
||||
(instance, props) => {
|
||||
const instanceProps = new Map<Prop["name"], Prop>();
|
||||
for (const prop of props.values()) {
|
||||
if (prop.instanceId === instance?.id) {
|
||||
instanceProps.set(prop.name, prop);
|
||||
}
|
||||
}
|
||||
return instanceProps;
|
||||
}
|
||||
);
|
||||
|
||||
const useProp = (name: string) => {
|
||||
const store = useMemo(() => {
|
||||
return computed([$selectedInstanceProps], (selectedInstanceProps) =>
|
||||
selectedInstanceProps.get(name)
|
||||
);
|
||||
}, [name]);
|
||||
return useStore(store);
|
||||
};
|
||||
|
||||
const deleteProp = (name: string) => {
|
||||
const instance = $selectedInstance.get();
|
||||
const instanceProps = $selectedInstanceProps.get();
|
||||
updateWebstudioData((data) => {
|
||||
const prop = instanceProps.get(name);
|
||||
if (prop) {
|
||||
data.props.delete(prop.id);
|
||||
}
|
||||
if (prop?.type === "resource") {
|
||||
data.resources.delete(prop.value);
|
||||
}
|
||||
if (instance?.component === "Image" && name === "src") {
|
||||
const widthProp = instanceProps.get("width");
|
||||
if (widthProp) {
|
||||
data.props.delete(widthProp.id);
|
||||
}
|
||||
const heightProp = instanceProps.get("height");
|
||||
if (heightProp) {
|
||||
data.props.delete(heightProp.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const useIsResettable = (name: string) => {
|
||||
const store = useMemo(() => {
|
||||
return computed(
|
||||
[$selectedInstance, $registeredComponentPropsMetas],
|
||||
(instance, propsMetas) => {
|
||||
if (name === showAttribute) {
|
||||
return true;
|
||||
}
|
||||
const metas = propsMetas.get(instance?.component ?? "");
|
||||
return metas?.initialProps?.includes(name);
|
||||
}
|
||||
);
|
||||
}, [name]);
|
||||
return useStore(store);
|
||||
};
|
||||
|
||||
export const PropertyLabel = ({
|
||||
name,
|
||||
readOnly,
|
||||
}: {
|
||||
name: string;
|
||||
readOnly?: boolean;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const propMeta = usePropMeta(name);
|
||||
const prop = useProp(name);
|
||||
const label = propMeta?.label ?? humanizeAttribute(name);
|
||||
// 1. not existing properties cannot be deleted
|
||||
// 2. required properties cannot be deleted
|
||||
// 3. custom attributes like data-* do not have meta and can be deleted
|
||||
const isDeletable = prop && !propMeta?.required;
|
||||
const isResettable = useIsResettable(name);
|
||||
return (
|
||||
<Flex align="center" css={{ gap: theme.spacing[3] }}>
|
||||
{/* prevent label growing */}
|
||||
<div>
|
||||
<Tooltip
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
triggerProps={{
|
||||
onClick: (event) => {
|
||||
if (event.altKey) {
|
||||
event.preventDefault();
|
||||
if (isDeletable) {
|
||||
deleteProp(name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setIsOpen(true);
|
||||
},
|
||||
}}
|
||||
content={
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="2"
|
||||
css={{ maxWidth: theme.spacing[28] }}
|
||||
>
|
||||
<Text variant="titles" css={{ textTransform: "none" }}>
|
||||
{label}
|
||||
</Text>
|
||||
{propMeta?.description && <Text>{propMeta.description}</Text>}
|
||||
{isDeletable && (
|
||||
<Button
|
||||
color="dark"
|
||||
// to align button text in the middle
|
||||
prefix={<div></div>}
|
||||
suffix={<Kbd value={["alt", "click"]} color="moreSubtle" />}
|
||||
css={{ gridTemplateColumns: "1fr max-content 1fr" }}
|
||||
onClick={() => {
|
||||
deleteProp(name);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{isResettable ? "Reset value" : "Delete property"}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Label truncate color={prop ? "local" : "default"}>
|
||||
{label}
|
||||
</Label>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{readOnly && (
|
||||
<Tooltip
|
||||
content="The value is controlled by an expression and cannot be changed."
|
||||
variant="wrapped"
|
||||
>
|
||||
<InfoCircleIcon
|
||||
color={rawTheme.colors.foregroundSubtle}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { useStore } from "@nanostores/react";
|
||||
import type { PropMeta, Instance, Prop } from "@webstudio-is/sdk";
|
||||
import { collectionComponent, descendantComponent } from "@webstudio-is/sdk";
|
||||
import { showAttribute, textContentAttribute } from "@webstudio-is/react-sdk";
|
||||
import type { PropValue } from "../shared";
|
||||
import { showAttributeMeta, type PropValue } from "../shared";
|
||||
import {
|
||||
$isContentMode,
|
||||
$registeredComponentMetas,
|
||||
@@ -133,22 +133,6 @@ const getAndDelete = <Value>(map: Map<string, Value>, key: string) => {
|
||||
return value;
|
||||
};
|
||||
|
||||
const systemPropsMeta: { name: string; meta: PropMeta }[] = [
|
||||
{
|
||||
name: showAttribute,
|
||||
meta: {
|
||||
label: "Show",
|
||||
required: false,
|
||||
control: "boolean",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
// If you are changing it, change the other one too
|
||||
description:
|
||||
"Removes the instance from the DOM. Breakpoints have no effect on this setting.",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** usePropsLogic expects that key={instanceId} is used on the ancestor component */
|
||||
export const usePropsLogic = ({
|
||||
instance,
|
||||
@@ -198,32 +182,17 @@ export const usePropsLogic = ({
|
||||
|
||||
const initialPropsNames = new Set(meta.initialProps ?? []);
|
||||
|
||||
const systemProps: PropAndMeta[] = systemPropsMeta
|
||||
.filter(({ name }) => {
|
||||
// descendant component is not actually rendered
|
||||
// but affects styling of nested elements
|
||||
// hiding descendant does not hide nested elements and confuse users
|
||||
if (
|
||||
instance.component === descendantComponent &&
|
||||
name === showAttribute
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(({ name, meta }) => {
|
||||
let saved = getAndDelete<Prop>(unprocessedSaved, name);
|
||||
if (saved === undefined && meta.defaultValue !== undefined) {
|
||||
saved = getStartingProp(instance.id, meta, name);
|
||||
}
|
||||
getAndDelete(unprocessedKnown, name);
|
||||
initialPropsNames.delete(name);
|
||||
return {
|
||||
prop: saved,
|
||||
propName: name,
|
||||
meta,
|
||||
};
|
||||
const systemProps: PropAndMeta[] = [];
|
||||
// descendant component is not actually rendered
|
||||
// but affects styling of nested elements
|
||||
// hiding descendant does not hide nested elements and confuse users
|
||||
if (instance.component !== descendantComponent) {
|
||||
systemProps.push({
|
||||
propName: showAttribute,
|
||||
prop: getAndDelete(unprocessedSaved, showAttribute),
|
||||
meta: showAttributeMeta,
|
||||
});
|
||||
}
|
||||
|
||||
const canHaveTextContent =
|
||||
instanceMeta?.type === "container" &&
|
||||
|
||||
@@ -39,11 +39,7 @@ export const SettingsSection = () => {
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<HorizontalLayout
|
||||
label={<Label htmlFor={id}>Name</Label>}
|
||||
deletable={false}
|
||||
onDelete={() => {}}
|
||||
>
|
||||
<HorizontalLayout label={<Label htmlFor={id}>Name</Label>}>
|
||||
<InputField
|
||||
id={id}
|
||||
/* Key is required, otherwise when label is undefined, previous value stayed */
|
||||
|
||||
@@ -18,9 +18,8 @@ import {
|
||||
systemParameter,
|
||||
} from "@webstudio-is/sdk";
|
||||
import type { PropMeta, Prop, Asset } from "@webstudio-is/sdk";
|
||||
import { InfoCircleIcon, MinusIcon } from "@webstudio-is/icons";
|
||||
import { InfoCircleIcon } from "@webstudio-is/icons";
|
||||
import {
|
||||
SmallIconButton,
|
||||
Label as BaseLabel,
|
||||
useIsTruncated,
|
||||
Tooltip,
|
||||
@@ -40,6 +39,17 @@ import type { BindingVariant } from "~/builder/shared/binding-popover";
|
||||
import { humanizeString } from "~/shared/string-utils";
|
||||
import { $selectedInstanceKeyWithRoot } from "~/shared/awareness";
|
||||
|
||||
export const showAttributeMeta: PropMeta = {
|
||||
label: "Show",
|
||||
required: false,
|
||||
control: "boolean",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
// If you are changing it, change the other one too
|
||||
description:
|
||||
"Removes the instance from the DOM. Breakpoints have no effect on this setting.",
|
||||
};
|
||||
|
||||
export type PropValue =
|
||||
| { type: "number"; value: number }
|
||||
| { type: "string"; value: string }
|
||||
@@ -60,7 +70,6 @@ export type PropValue =
|
||||
type PropMetaByControl<Control> = Control extends string
|
||||
? Extract<PropMeta, { control: Control }>
|
||||
: never;
|
||||
|
||||
export type ControlProps<Control> = {
|
||||
instanceId: string;
|
||||
meta: PropMetaByControl<Control>;
|
||||
@@ -74,10 +83,6 @@ export type ControlProps<Control> = {
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export const RemovePropButton = (props: { onClick: () => void }) => (
|
||||
<SmallIconButton icon={<MinusIcon />} variant="destructive" {...props} />
|
||||
);
|
||||
|
||||
const SimpleLabel = ({
|
||||
children,
|
||||
...rest
|
||||
@@ -141,7 +146,7 @@ export const Label = ({
|
||||
|
||||
return (
|
||||
<Flex align="center" css={{ gap: theme.spacing[3], width: "100%" }}>
|
||||
{label}
|
||||
<Box>{label}</Box>
|
||||
{readOnly && (
|
||||
<Tooltip
|
||||
content={
|
||||
@@ -234,21 +239,14 @@ export const useLocalValue = <Type,>(
|
||||
|
||||
type LayoutProps = {
|
||||
label: ReturnType<typeof Label>;
|
||||
deletable: boolean;
|
||||
onDelete: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const VerticalLayout = ({
|
||||
label,
|
||||
deletable,
|
||||
onDelete,
|
||||
children,
|
||||
}: LayoutProps) => (
|
||||
export const VerticalLayout = ({ label, children }: LayoutProps) => (
|
||||
<Box>
|
||||
<Grid
|
||||
css={{
|
||||
gridTemplateColumns: deletable ? `1fr max-content` : `1fr`,
|
||||
gridTemplateColumns: `1fr`,
|
||||
justifyItems: "start",
|
||||
}}
|
||||
align="center"
|
||||
@@ -256,23 +254,15 @@ export const VerticalLayout = ({
|
||||
justify="between"
|
||||
>
|
||||
{label}
|
||||
{deletable && <RemovePropButton onClick={onDelete} />}
|
||||
</Grid>
|
||||
<Box css={{ py: theme.spacing[2] }}>{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const HorizontalLayout = ({
|
||||
label,
|
||||
deletable,
|
||||
onDelete,
|
||||
children,
|
||||
}: LayoutProps) => (
|
||||
export const HorizontalLayout = ({ label, children }: LayoutProps) => (
|
||||
<Grid
|
||||
css={{
|
||||
gridTemplateColumns: deletable
|
||||
? `${theme.spacing[19]} 1fr max-content`
|
||||
: `${theme.spacing[19]} 1fr`,
|
||||
gridTemplateColumns: `${theme.spacing[19]} 1fr`,
|
||||
minHeight: theme.spacing[12],
|
||||
}}
|
||||
align="center"
|
||||
@@ -280,29 +270,36 @@ export const HorizontalLayout = ({
|
||||
>
|
||||
{label}
|
||||
{children}
|
||||
{deletable && <RemovePropButton onClick={onDelete} />}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
export const ResponsiveLayout = ({
|
||||
label,
|
||||
deletable,
|
||||
onDelete,
|
||||
children,
|
||||
}: LayoutProps) => {
|
||||
// more than 9 characters in label trigger ellipsis
|
||||
// might not cover all cases though
|
||||
if (label.props.children.length <= 8) {
|
||||
return (
|
||||
<HorizontalLayout label={label} deletable={deletable} onDelete={onDelete}>
|
||||
{children}
|
||||
</HorizontalLayout>
|
||||
);
|
||||
}
|
||||
export const ResponsiveLayout = ({ label, children }: LayoutProps) => {
|
||||
return (
|
||||
<VerticalLayout label={label} deletable={deletable} onDelete={onDelete}>
|
||||
{children}
|
||||
</VerticalLayout>
|
||||
<Flex
|
||||
align="center"
|
||||
wrap="wrap"
|
||||
css={{
|
||||
columnGap: theme.spacing[5],
|
||||
rowGap: theme.spacing[3],
|
||||
paddingBlock: theme.spacing[2],
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
// wrap label and input when label is more than ~9 characters
|
||||
flexBasis: `calc(30% - ${theme.spacing[5]} / 2)`,
|
||||
// allow content overflow flex basis
|
||||
minWidth: "auto",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
<Box
|
||||
css={{ flexBasis: `calc(70% - ${theme.spacing[5]} / 2)`, flexGrow: 1 }}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user