Skip to content

Commit 257d760

Browse files
authored
feat: support paste styles in advanced section (#4323)
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
1 parent 4168a2d commit 257d760

File tree

4 files changed

+109
-97
lines changed

4 files changed

+109
-97
lines changed

apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,27 @@ import { isFeatureEnabled } from "@webstudio-is/feature-flags";
1515
import { PlusIcon } from "@webstudio-is/icons";
1616
import {
1717
Box,
18-
Combobox,
18+
ComboboxAnchor,
19+
ComboboxContent,
20+
ComboboxItemDescription,
21+
ComboboxListbox,
22+
ComboboxListboxItem,
23+
ComboboxRoot,
24+
ComboboxScrollArea,
1925
Flex,
26+
InputField,
2027
Label,
28+
NestedInputButton,
2129
SectionTitle,
2230
SectionTitleButton,
2331
SectionTitleLabel,
2432
Text,
2533
theme,
2634
Tooltip,
35+
useCombobox,
2736
} from "@webstudio-is/design-system";
2837
import {
38+
parseCss,
2939
properties as propertiesData,
3040
propertyDescriptions,
3141
} from "@webstudio-is/css-data";
@@ -41,7 +51,11 @@ import {
4151
} from "~/builder/shared/collapsible-section";
4252
import { CssValueInputContainer } from "../../shared/css-value-input";
4353
import { styleConfigByName } from "../../shared/configs";
44-
import { deleteProperty, setProperty } from "../../shared/use-style-data";
54+
import {
55+
createBatchUpdate,
56+
deleteProperty,
57+
setProperty,
58+
} from "../../shared/use-style-data";
4559
import {
4660
$availableVariables,
4761
$matchingBreakpoints,
@@ -125,12 +139,48 @@ const matchOrSuggestToCreate = (
125139
return matched;
126140
};
127141

142+
const getNewPropertyDescription = (item: null | SearchItem) => {
143+
let description = `Create CSS variable.`;
144+
if (item && item.value in propertyDescriptions) {
145+
description =
146+
propertyDescriptions[item.value as keyof typeof propertyDescriptions];
147+
}
148+
return <Box css={{ width: theme.spacing[28] }}>{description}</Box>;
149+
};
150+
151+
const insertStyles = (text: string) => {
152+
const parsedStyles = parseCss(`selector{${text}}`, {
153+
customProperties: true,
154+
});
155+
if (parsedStyles.length === 0) {
156+
return false;
157+
}
158+
const batch = createBatchUpdate();
159+
for (const { property, value } of parsedStyles) {
160+
batch.setProperty(property)(value);
161+
}
162+
batch.publish({ listed: true });
163+
return true;
164+
};
165+
166+
/**
167+
*
168+
* Advanced search control supports following interactions
169+
*
170+
* find property
171+
* create custom property
172+
* submit css declarations
173+
* paste css declarations
174+
*
175+
*/
128176
const AdvancedSearch = ({
129177
usedProperties,
130178
onSelect,
179+
onClose,
131180
}: {
132181
usedProperties: string[];
133182
onSelect: (value: StyleProperty) => void;
183+
onClose: () => void;
134184
}) => {
135185
const availableProperties = useMemo(() => {
136186
const properties = Object.keys(propertiesData).sort(
@@ -152,31 +202,63 @@ const AdvancedSearch = ({
152202
label: "",
153203
});
154204

205+
const combobox = useCombobox<SearchItem>({
206+
getItems: () => availableProperties,
207+
itemToString: (item) => item?.label ?? "",
208+
value: item,
209+
defaultHighlightedIndex: 0,
210+
getItemProps: () => ({ text: "sentence" }),
211+
match: matchOrSuggestToCreate,
212+
onChange: (value) => setItem({ value: value ?? "", label: value ?? "" }),
213+
onItemSelect: (item) => onSelect(item.value as StyleProperty),
214+
});
215+
216+
const descriptionItem = combobox.items[combobox.highlightedIndex];
217+
const description = getNewPropertyDescription(descriptionItem);
218+
const descriptions = combobox.items.map(getNewPropertyDescription);
219+
155220
return (
156-
<Combobox
157-
autoFocus
158-
placeholder="Find or create a property"
159-
getItems={() => availableProperties}
160-
defaultHighlightedIndex={0}
161-
value={item}
162-
itemToString={(item) => item?.label ?? ""}
163-
getItemProps={() => ({ text: "sentence" })}
164-
getDescription={(item) => {
165-
let description = `Create CSS variable.`;
166-
if (item && item.value in propertyDescriptions) {
167-
description =
168-
propertyDescriptions[
169-
item.value as keyof typeof propertyDescriptions
170-
];
171-
}
172-
return <Box css={{ width: theme.spacing[28] }}>{description}</Box>;
173-
}}
174-
match={matchOrSuggestToCreate}
175-
onChange={(value) => {
176-
setItem({ value: value ?? "", label: value ?? "" });
177-
}}
178-
onItemSelect={(item) => onSelect(item.value as StyleProperty)}
179-
/>
221+
<ComboboxRoot open={combobox.isOpen}>
222+
<form
223+
{...combobox.getComboboxProps()}
224+
onSubmit={(event) => {
225+
event.preventDefault();
226+
const isInserted = insertStyles(item.value);
227+
if (isInserted) {
228+
onClose();
229+
}
230+
}}
231+
>
232+
<input type="submit" hidden />
233+
<ComboboxAnchor>
234+
<InputField
235+
{...combobox.getInputProps()}
236+
autoFocus={true}
237+
placeholder="Add styles"
238+
suffix={<NestedInputButton {...combobox.getToggleButtonProps()} />}
239+
/>
240+
</ComboboxAnchor>
241+
<ComboboxContent>
242+
<ComboboxListbox {...combobox.getMenuProps()}>
243+
<ComboboxScrollArea>
244+
{combobox.items.map((item, index) => (
245+
<ComboboxListboxItem
246+
{...combobox.getItemProps({ item, index })}
247+
key={index}
248+
>
249+
{item.label}
250+
</ComboboxListboxItem>
251+
))}
252+
</ComboboxScrollArea>
253+
{description && (
254+
<ComboboxItemDescription descriptions={descriptions}>
255+
{description}
256+
</ComboboxItemDescription>
257+
)}
258+
</ComboboxListbox>
259+
</ComboboxContent>
260+
</form>
261+
</ComboboxRoot>
180262
);
181263
};
182264

@@ -438,6 +520,7 @@ export const Section = () => {
438520
{ listed: true }
439521
);
440522
}}
523+
onClose={() => setIsAdding(false)}
441524
/>
442525
)}
443526
<Box>

apps/builder/app/builder/features/style-panel/style-source-section.tsx

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
type StyleSources,
1212
getStyleDeclKey,
1313
} from "@webstudio-is/sdk";
14-
import { parseCss } from "@webstudio-is/css-data";
1514
import {
1615
Flex,
1716
Dialog,
@@ -41,7 +40,6 @@ import {
4140
$styleSourceSelections,
4241
$styleSources,
4342
$styles,
44-
$selectedBreakpoint,
4543
} from "~/shared/nano-states";
4644
import { removeByMutable } from "~/shared/array-utils";
4745
import { cloneStyles } from "~/shared/tree-utils";
@@ -394,50 +392,6 @@ const renameStyleSource = (
394392
});
395393
};
396394

397-
const pasteStyles = async (
398-
styleSourceId: StyleSource["id"],
399-
state: undefined | string
400-
) => {
401-
const text = await navigator.clipboard.readText();
402-
const parsedStyles = parseCss(`selector{${text}}`, {
403-
customProperties: true,
404-
});
405-
const breakpointId = $selectedBreakpoint.get()?.id;
406-
const instanceId = $selectedInstanceSelector.get()?.[0];
407-
if (breakpointId === undefined || instanceId === undefined) {
408-
return;
409-
}
410-
serverSyncStore.createTransaction(
411-
[$styles, $styleSources, $styleSourceSelections],
412-
(styles, styleSources, styleSourceSelections) => {
413-
// add local style source if does not exist yet
414-
if (styleSources.has(styleSourceId) === false) {
415-
styleSources.set(styleSourceId, { type: "local", id: styleSourceId });
416-
let styleSourceSelection = styleSourceSelections.get(instanceId);
417-
// create new style source selection
418-
if (styleSourceSelection === undefined) {
419-
styleSourceSelection = { instanceId, values: [styleSourceId] };
420-
styleSourceSelections.set(instanceId, styleSourceSelection);
421-
}
422-
// append style source to existing selection
423-
if (styleSourceSelection.values.includes(styleSourceId) === false) {
424-
styleSourceSelection.values.push(styleSourceId);
425-
}
426-
}
427-
for (const { property, value } of parsedStyles) {
428-
const styleDecl: StyleDecl = {
429-
breakpointId,
430-
styleSourceId,
431-
state,
432-
property,
433-
value,
434-
};
435-
styles.set(getStyleDeclKey(styleDecl), styleDecl);
436-
}
437-
}
438-
);
439-
};
440-
441395
const clearStyles = (styleSourceId: StyleSource["id"]) => {
442396
serverSyncStore.createTransaction([$styles], (styles) => {
443397
for (const [styleDeclKey, styleDecl] of styles) {
@@ -582,12 +536,6 @@ export const StyleSourcesSection = () => {
582536
convertLocalStyleSourceToToken(id);
583537
setEditingItem(id);
584538
}}
585-
onPasteStyles={(styleSourceSelector) => {
586-
pasteStyles(
587-
styleSourceSelector.styleSourceId,
588-
styleSourceSelector.state
589-
);
590-
}}
591539
onClearStyles={clearStyles}
592540
onRemoveItem={(id) => {
593541
removeStyleSourceFromInstance(id);

apps/builder/app/builder/features/style-panel/style-source/style-source-input.tsx

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
import { nanoid } from "nanoid";
1515
import { useFocusWithin } from "@react-aria/interactions";
16-
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
1716
import {
1817
Box,
1918
ComboboxListbox,
@@ -248,7 +247,6 @@ type StyleSourceInputProps<Item extends IntermediateItem> = {
248247
onSelectAutocompleteItem?: (item: Item) => void;
249248
onRemoveItem?: (id: Item["id"]) => void;
250249
onDeleteItem?: (id: Item["id"]) => void;
251-
onPasteStyles?: (item: ItemSelector) => void;
252250
onClearStyles?: (id: Item["id"]) => void;
253251
onDuplicateItem?: (id: Item["id"]) => void;
254252
onConvertToToken?: (id: Item["id"]) => void;
@@ -322,7 +320,6 @@ const renderMenuItems = (props: {
322320
onEnable?: (itemId: IntermediateItem["id"]) => void;
323321
onRemove?: (itemId: IntermediateItem["id"]) => void;
324322
onDelete?: (itemId: IntermediateItem["id"]) => void;
325-
onPasteStyles?: (item: ItemSelector) => void;
326323
onClearStyles?: (itemId: IntermediateItem["id"]) => void;
327324
}) => {
328325
return (
@@ -345,20 +342,6 @@ const renderMenuItems = (props: {
345342
Convert to token
346343
</DropdownMenuItem>
347344
)}
348-
{isFeatureEnabled("pasteStyles") && (
349-
<DropdownMenuItem
350-
onSelect={() => {
351-
if (props.selectedItemSelector?.styleSourceId === props.item.id) {
352-
// allow paste into state when selected
353-
props.onPasteStyles?.(props.selectedItemSelector);
354-
} else {
355-
props.onPasteStyles?.({ styleSourceId: props.item.id });
356-
}
357-
}}
358-
>
359-
Paste styles
360-
</DropdownMenuItem>
361-
)}
362345
{props.item.source === "local" && (
363346
<DropdownMenuItem
364347
destructive={true}
@@ -540,7 +523,6 @@ export const StyleSourceInput = (
540523
onEdit: props.onEditItem,
541524
onRemove: props.onRemoveItem,
542525
onDelete: props.onDeleteItem,
543-
onPasteStyles: props.onPasteStyles,
544526
onClearStyles: props.onClearStyles,
545527
})
546528
}

packages/feature-flags/src/flags.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,3 @@ export const cssVars = false;
88
export const filters = false;
99
export const xmlElement = false;
1010
export const staticExport = false;
11-
export const pasteStyles = false;

0 commit comments

Comments
 (0)