Skip to content

Commit

Permalink
feat: support paste styles in advanced section (#4323)
Browse files Browse the repository at this point in the history
Ref #3399
#3540

Here added two more features to advanced panel

1. When search for property, user can enter css declarations like
`width: 100px; height: 200px;` and press enter instead of searching for
property and editing property after creation.

2. When search for property, user can paste css declarations from some
theme editor or figma, this is useful interop with other tools

Note: this does not support breakpoints, states or tokens. Declarations
are inserted only within currently selected style source and state.


https://github.com/user-attachments/assets/1f37f5d1-11b9-4f05-ae4d-a7c647ae14b6
  • Loading branch information
TrySound authored Oct 21, 2024
1 parent 4168a2d commit 257d760
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@ import { isFeatureEnabled } from "@webstudio-is/feature-flags";
import { PlusIcon } from "@webstudio-is/icons";
import {
Box,
Combobox,
ComboboxAnchor,
ComboboxContent,
ComboboxItemDescription,
ComboboxListbox,
ComboboxListboxItem,
ComboboxRoot,
ComboboxScrollArea,
Flex,
InputField,
Label,
NestedInputButton,
SectionTitle,
SectionTitleButton,
SectionTitleLabel,
Text,
theme,
Tooltip,
useCombobox,
} from "@webstudio-is/design-system";
import {
parseCss,
properties as propertiesData,
propertyDescriptions,
} from "@webstudio-is/css-data";
Expand All @@ -41,7 +51,11 @@ import {
} from "~/builder/shared/collapsible-section";
import { CssValueInputContainer } from "../../shared/css-value-input";
import { styleConfigByName } from "../../shared/configs";
import { deleteProperty, setProperty } from "../../shared/use-style-data";
import {
createBatchUpdate,
deleteProperty,
setProperty,
} from "../../shared/use-style-data";
import {
$availableVariables,
$matchingBreakpoints,
Expand Down Expand Up @@ -125,12 +139,48 @@ const matchOrSuggestToCreate = (
return matched;
};

const getNewPropertyDescription = (item: null | SearchItem) => {
let description = `Create CSS variable.`;
if (item && item.value in propertyDescriptions) {
description =
propertyDescriptions[item.value as keyof typeof propertyDescriptions];
}
return <Box css={{ width: theme.spacing[28] }}>{description}</Box>;
};

const insertStyles = (text: string) => {
const parsedStyles = parseCss(`selector{${text}}`, {
customProperties: true,
});
if (parsedStyles.length === 0) {
return false;
}
const batch = createBatchUpdate();
for (const { property, value } of parsedStyles) {
batch.setProperty(property)(value);
}
batch.publish({ listed: true });
return true;
};

/**
*
* Advanced search control supports following interactions
*
* find property
* create custom property
* submit css declarations
* paste css declarations
*
*/
const AdvancedSearch = ({
usedProperties,
onSelect,
onClose,
}: {
usedProperties: string[];
onSelect: (value: StyleProperty) => void;
onClose: () => void;
}) => {
const availableProperties = useMemo(() => {
const properties = Object.keys(propertiesData).sort(
Expand All @@ -152,31 +202,63 @@ const AdvancedSearch = ({
label: "",
});

const combobox = useCombobox<SearchItem>({
getItems: () => availableProperties,
itemToString: (item) => item?.label ?? "",
value: item,
defaultHighlightedIndex: 0,
getItemProps: () => ({ text: "sentence" }),
match: matchOrSuggestToCreate,
onChange: (value) => setItem({ value: value ?? "", label: value ?? "" }),
onItemSelect: (item) => onSelect(item.value as StyleProperty),
});

const descriptionItem = combobox.items[combobox.highlightedIndex];
const description = getNewPropertyDescription(descriptionItem);
const descriptions = combobox.items.map(getNewPropertyDescription);

return (
<Combobox
autoFocus
placeholder="Find or create a property"
getItems={() => availableProperties}
defaultHighlightedIndex={0}
value={item}
itemToString={(item) => item?.label ?? ""}
getItemProps={() => ({ text: "sentence" })}
getDescription={(item) => {
let description = `Create CSS variable.`;
if (item && item.value in propertyDescriptions) {
description =
propertyDescriptions[
item.value as keyof typeof propertyDescriptions
];
}
return <Box css={{ width: theme.spacing[28] }}>{description}</Box>;
}}
match={matchOrSuggestToCreate}
onChange={(value) => {
setItem({ value: value ?? "", label: value ?? "" });
}}
onItemSelect={(item) => onSelect(item.value as StyleProperty)}
/>
<ComboboxRoot open={combobox.isOpen}>
<form
{...combobox.getComboboxProps()}
onSubmit={(event) => {
event.preventDefault();
const isInserted = insertStyles(item.value);
if (isInserted) {
onClose();
}
}}
>
<input type="submit" hidden />
<ComboboxAnchor>
<InputField
{...combobox.getInputProps()}
autoFocus={true}
placeholder="Add styles"
suffix={<NestedInputButton {...combobox.getToggleButtonProps()} />}
/>
</ComboboxAnchor>
<ComboboxContent>
<ComboboxListbox {...combobox.getMenuProps()}>
<ComboboxScrollArea>
{combobox.items.map((item, index) => (
<ComboboxListboxItem
{...combobox.getItemProps({ item, index })}
key={index}
>
{item.label}
</ComboboxListboxItem>
))}
</ComboboxScrollArea>
{description && (
<ComboboxItemDescription descriptions={descriptions}>
{description}
</ComboboxItemDescription>
)}
</ComboboxListbox>
</ComboboxContent>
</form>
</ComboboxRoot>
);
};

Expand Down Expand Up @@ -438,6 +520,7 @@ export const Section = () => {
{ listed: true }
);
}}
onClose={() => setIsAdding(false)}
/>
)}
<Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
type StyleSources,
getStyleDeclKey,
} from "@webstudio-is/sdk";
import { parseCss } from "@webstudio-is/css-data";
import {
Flex,
Dialog,
Expand Down Expand Up @@ -41,7 +40,6 @@ import {
$styleSourceSelections,
$styleSources,
$styles,
$selectedBreakpoint,
} from "~/shared/nano-states";
import { removeByMutable } from "~/shared/array-utils";
import { cloneStyles } from "~/shared/tree-utils";
Expand Down Expand Up @@ -394,50 +392,6 @@ const renameStyleSource = (
});
};

const pasteStyles = async (
styleSourceId: StyleSource["id"],
state: undefined | string
) => {
const text = await navigator.clipboard.readText();
const parsedStyles = parseCss(`selector{${text}}`, {
customProperties: true,
});
const breakpointId = $selectedBreakpoint.get()?.id;
const instanceId = $selectedInstanceSelector.get()?.[0];
if (breakpointId === undefined || instanceId === undefined) {
return;
}
serverSyncStore.createTransaction(
[$styles, $styleSources, $styleSourceSelections],
(styles, styleSources, styleSourceSelections) => {
// add local style source if does not exist yet
if (styleSources.has(styleSourceId) === false) {
styleSources.set(styleSourceId, { type: "local", id: styleSourceId });
let styleSourceSelection = styleSourceSelections.get(instanceId);
// create new style source selection
if (styleSourceSelection === undefined) {
styleSourceSelection = { instanceId, values: [styleSourceId] };
styleSourceSelections.set(instanceId, styleSourceSelection);
}
// append style source to existing selection
if (styleSourceSelection.values.includes(styleSourceId) === false) {
styleSourceSelection.values.push(styleSourceId);
}
}
for (const { property, value } of parsedStyles) {
const styleDecl: StyleDecl = {
breakpointId,
styleSourceId,
state,
property,
value,
};
styles.set(getStyleDeclKey(styleDecl), styleDecl);
}
}
);
};

const clearStyles = (styleSourceId: StyleSource["id"]) => {
serverSyncStore.createTransaction([$styles], (styles) => {
for (const [styleDeclKey, styleDecl] of styles) {
Expand Down Expand Up @@ -582,12 +536,6 @@ export const StyleSourcesSection = () => {
convertLocalStyleSourceToToken(id);
setEditingItem(id);
}}
onPasteStyles={(styleSourceSelector) => {
pasteStyles(
styleSourceSelector.styleSourceId,
styleSourceSelector.state
);
}}
onClearStyles={clearStyles}
onRemoveItem={(id) => {
removeStyleSourceFromInstance(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

import { nanoid } from "nanoid";
import { useFocusWithin } from "@react-aria/interactions";
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
import {
Box,
ComboboxListbox,
Expand Down Expand Up @@ -248,7 +247,6 @@ type StyleSourceInputProps<Item extends IntermediateItem> = {
onSelectAutocompleteItem?: (item: Item) => void;
onRemoveItem?: (id: Item["id"]) => void;
onDeleteItem?: (id: Item["id"]) => void;
onPasteStyles?: (item: ItemSelector) => void;
onClearStyles?: (id: Item["id"]) => void;
onDuplicateItem?: (id: Item["id"]) => void;
onConvertToToken?: (id: Item["id"]) => void;
Expand Down Expand Up @@ -322,7 +320,6 @@ const renderMenuItems = (props: {
onEnable?: (itemId: IntermediateItem["id"]) => void;
onRemove?: (itemId: IntermediateItem["id"]) => void;
onDelete?: (itemId: IntermediateItem["id"]) => void;
onPasteStyles?: (item: ItemSelector) => void;
onClearStyles?: (itemId: IntermediateItem["id"]) => void;
}) => {
return (
Expand All @@ -345,20 +342,6 @@ const renderMenuItems = (props: {
Convert to token
</DropdownMenuItem>
)}
{isFeatureEnabled("pasteStyles") && (
<DropdownMenuItem
onSelect={() => {
if (props.selectedItemSelector?.styleSourceId === props.item.id) {
// allow paste into state when selected
props.onPasteStyles?.(props.selectedItemSelector);
} else {
props.onPasteStyles?.({ styleSourceId: props.item.id });
}
}}
>
Paste styles
</DropdownMenuItem>
)}
{props.item.source === "local" && (
<DropdownMenuItem
destructive={true}
Expand Down Expand Up @@ -540,7 +523,6 @@ export const StyleSourceInput = (
onEdit: props.onEditItem,
onRemove: props.onRemoveItem,
onDelete: props.onDeleteItem,
onPasteStyles: props.onPasteStyles,
onClearStyles: props.onClearStyles,
})
}
Expand Down
1 change: 0 additions & 1 deletion packages/feature-flags/src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ export const cssVars = false;
export const filters = false;
export const xmlElement = false;
export const staticExport = false;
export const pasteStyles = false;

0 comments on commit 257d760

Please sign in to comment.