Skip to content

Commit 1a4f663

Browse files
authored
experimental: allow pasting styles into style source (#4200)
Ref #3399 He added support for paste style declarations into style source or state. It will let us create collections of custom properties like open props with a few clicks. The downside is browser prompt with permission confirmation because we use "unsafe" navigator.clipboard.readText() instead of paste event. https://github.com/user-attachments/assets/8e0153b6-5b98-4f07-bb2c-aac6bc51c6d3
1 parent bbf37d3 commit 1a4f663

File tree

3 files changed

+52
-1
lines changed

3 files changed

+52
-1
lines changed

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type StyleSources,
1212
getStyleDeclKey,
1313
} from "@webstudio-is/sdk";
14+
import { parseCss } from "@webstudio-is/css-data";
1415
import {
1516
Flex,
1617
Dialog,
@@ -40,6 +41,7 @@ import {
4041
$styleSourceSelections,
4142
$styleSources,
4243
$styles,
44+
$selectedBreakpoint,
4345
} from "~/shared/nano-states";
4446
import { removeByMutable } from "~/shared/array-utils";
4547
import { cloneStyles } from "~/shared/tree-utils";
@@ -392,6 +394,30 @@ const renameStyleSource = (
392394
});
393395
};
394396

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+
const breakpointId = $selectedBreakpoint.get()?.id;
404+
if (breakpointId === undefined) {
405+
return;
406+
}
407+
serverSyncStore.createTransaction([$styles], (styles) => {
408+
for (const { property, value } of parsedStyles) {
409+
const styleDecl: StyleDecl = {
410+
breakpointId,
411+
styleSourceId,
412+
state,
413+
property,
414+
value,
415+
};
416+
styles.set(getStyleDeclKey(styleDecl), styleDecl);
417+
}
418+
});
419+
};
420+
395421
const clearStyles = (styleSourceId: StyleSource["id"]) => {
396422
serverSyncStore.createTransaction([$styles], (styles) => {
397423
for (const [styleDeclKey, styleDecl] of styles) {
@@ -536,6 +562,12 @@ export const StyleSourcesSection = () => {
536562
convertLocalStyleSourceToToken(id);
537563
setEditingItem(id);
538564
}}
565+
onPasteStyles={(styleSourceSelector) => {
566+
pasteStyles(
567+
styleSourceSelector.styleSourceId,
568+
styleSourceSelector.state
569+
);
570+
}}
539571
onClearStyles={clearStyles}
540572
onRemoveItem={(id) => {
541573
removeStyleSourceFromInstance(id);

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
*/
1313

1414
import { nanoid } from "nanoid";
15+
import { useFocusWithin } from "@react-aria/interactions";
16+
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
1517
import {
1618
Box,
1719
ComboboxListbox,
@@ -57,7 +59,6 @@ import { useSortable } from "./use-sortable";
5759
import { matchSorter } from "match-sorter";
5860
import { StyleSourceBadge } from "./style-source-badge";
5961
import { humanizeString } from "~/shared/string-utils";
60-
import { useFocusWithin } from "@react-aria/interactions";
6162

6263
type IntermediateItem = {
6364
id: string;
@@ -252,6 +253,7 @@ type StyleSourceInputProps<Item extends IntermediateItem> = {
252253
onSelectAutocompleteItem?: (item: Item) => void;
253254
onRemoveItem?: (id: Item["id"]) => void;
254255
onDeleteItem?: (id: Item["id"]) => void;
256+
onPasteStyles?: (item: ItemSelector) => void;
255257
onClearStyles?: (id: Item["id"]) => void;
256258
onDuplicateItem?: (id: Item["id"]) => void;
257259
onConvertToToken?: (id: Item["id"]) => void;
@@ -325,6 +327,7 @@ const renderMenuItems = (props: {
325327
onEnable?: (itemId: IntermediateItem["id"]) => void;
326328
onRemove?: (itemId: IntermediateItem["id"]) => void;
327329
onDelete?: (itemId: IntermediateItem["id"]) => void;
330+
onPasteStyles?: (item: ItemSelector) => void;
328331
onClearStyles?: (itemId: IntermediateItem["id"]) => void;
329332
}) => {
330333
return (
@@ -347,6 +350,20 @@ const renderMenuItems = (props: {
347350
Convert to token
348351
</DropdownMenuItem>
349352
)}
353+
{isFeatureEnabled("pasteStyles") && (
354+
<DropdownMenuItem
355+
onSelect={() => {
356+
if (props.selectedItemSelector?.styleSourceId === props.item.id) {
357+
// allow paste into state when selected
358+
props.onPasteStyles?.(props.selectedItemSelector);
359+
} else {
360+
props.onPasteStyles?.({ styleSourceId: props.item.id });
361+
}
362+
}}
363+
>
364+
Paste styles
365+
</DropdownMenuItem>
366+
)}
350367
{props.item.source === "local" && (
351368
<DropdownMenuItem
352369
destructive={true}
@@ -528,6 +545,7 @@ export const StyleSourceInput = (
528545
onEdit: props.onEditItem,
529546
onRemove: props.onRemoveItem,
530547
onDelete: props.onDeleteItem,
548+
onPasteStyles: props.onPasteStyles,
531549
onClearStyles: props.onClearStyles,
532550
})
533551
}

packages/feature-flags/src/flags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ 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)