From 257d76022207caa941b0d0771e7bd4ca4ca7edca Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Mon, 21 Oct 2024 12:07:24 +0300 Subject: [PATCH] feat: support paste styles in advanced section (#4323) Ref https://github.com/webstudio-is/webstudio/issues/3399 https://github.com/webstudio-is/webstudio/issues/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 --- .../sections/advanced/advanced.tsx | 135 ++++++++++++++---- .../style-panel/style-source-section.tsx | 52 ------- .../style-source/style-source-input.tsx | 18 --- packages/feature-flags/src/flags.ts | 1 - 4 files changed, 109 insertions(+), 97 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index 95d5ae7d85fe..89ecbb2613a6 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -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"; @@ -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, @@ -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 {description}; +}; + +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( @@ -152,31 +202,63 @@ const AdvancedSearch = ({ label: "", }); + const combobox = useCombobox({ + 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 ( - 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 {description}; - }} - match={matchOrSuggestToCreate} - onChange={(value) => { - setItem({ value: value ?? "", label: value ?? "" }); - }} - onItemSelect={(item) => onSelect(item.value as StyleProperty)} - /> + +
{ + event.preventDefault(); + const isInserted = insertStyles(item.value); + if (isInserted) { + onClose(); + } + }} + > + + + } + /> + + + + + {combobox.items.map((item, index) => ( + + {item.label} + + ))} + + {description && ( + + {description} + + )} + + +
+
); }; @@ -438,6 +520,7 @@ export const Section = () => { { listed: true } ); }} + onClose={() => setIsAdding(false)} /> )} diff --git a/apps/builder/app/builder/features/style-panel/style-source-section.tsx b/apps/builder/app/builder/features/style-panel/style-source-section.tsx index 39ec7f5bc359..9ed450e9d44f 100644 --- a/apps/builder/app/builder/features/style-panel/style-source-section.tsx +++ b/apps/builder/app/builder/features/style-panel/style-source-section.tsx @@ -11,7 +11,6 @@ import { type StyleSources, getStyleDeclKey, } from "@webstudio-is/sdk"; -import { parseCss } from "@webstudio-is/css-data"; import { Flex, Dialog, @@ -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"; @@ -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) { @@ -582,12 +536,6 @@ export const StyleSourcesSection = () => { convertLocalStyleSourceToToken(id); setEditingItem(id); }} - onPasteStyles={(styleSourceSelector) => { - pasteStyles( - styleSourceSelector.styleSourceId, - styleSourceSelector.state - ); - }} onClearStyles={clearStyles} onRemoveItem={(id) => { removeStyleSourceFromInstance(id); diff --git a/apps/builder/app/builder/features/style-panel/style-source/style-source-input.tsx b/apps/builder/app/builder/features/style-panel/style-source/style-source-input.tsx index e89bb7db7840..0323440c2d4d 100644 --- a/apps/builder/app/builder/features/style-panel/style-source/style-source-input.tsx +++ b/apps/builder/app/builder/features/style-panel/style-source/style-source-input.tsx @@ -13,7 +13,6 @@ import { nanoid } from "nanoid"; import { useFocusWithin } from "@react-aria/interactions"; -import { isFeatureEnabled } from "@webstudio-is/feature-flags"; import { Box, ComboboxListbox, @@ -248,7 +247,6 @@ type StyleSourceInputProps = { 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; @@ -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 ( @@ -345,20 +342,6 @@ const renderMenuItems = (props: { Convert to token )} - {isFeatureEnabled("pasteStyles") && ( - { - 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 - - )} {props.item.source === "local" && (