Skip to content

Commit 326007b

Browse files
authored
feat: Manage CSS variables (webstudio-is#5490)
ref webstudio-is#4608 ## Description 1. What is this PR about (link the issue and add a short description) ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent 7fce2be commit 326007b

16 files changed

Lines changed: 2112 additions & 208 deletions

File tree

apps/builder/app/builder/builder.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { mergeRefs } from "@react-aria/utils";
6161
import { CommandPanel } from "./features/command-panel";
6262
import { DeleteUnusedTokensDialog } from "~/builder/shared/style-source-utils";
6363
import { DeleteUnusedDataVariablesDialog } from "~/builder/shared/data-variable-utils";
64+
import { DeleteUnusedCssVariablesDialog } from "~/builder/shared/css-variable-utils";
6465

6566
import {
6667
initCopyPaste,
@@ -443,6 +444,7 @@ export const Builder = ({
443444
<CommandPanel />
444445
<DeleteUnusedTokensDialog />
445446
<DeleteUnusedDataVariablesDialog />
447+
<DeleteUnusedCssVariablesDialog />
446448
<RemoteDialog />
447449
</div>
448450
</TooltipProvider>

apps/builder/app/builder/features/command-panel/command-state.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,12 @@ export const closeCommandPanel = ({
4242
export const $commandContent = atom<ReactNode>();
4343

4444
export const $commandSearch = atom("");
45+
46+
export const focusCommandPanel = () => {
47+
requestAnimationFrame(() => {
48+
const input = document.querySelector<HTMLInputElement>(
49+
"[cmdk-root] [cmdk-input]"
50+
);
51+
input?.focus();
52+
});
53+
};
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { useState } from "react";
2+
import { computed } from "nanostores";
3+
import { useStore } from "@nanostores/react";
4+
import {
5+
CommandGroup,
6+
CommandGroupHeading,
7+
CommandItem,
8+
Text,
9+
toast,
10+
useSelectedAction,
11+
useResetActionIndex,
12+
} from "@webstudio-is/design-system";
13+
import type { Instance } from "@webstudio-is/sdk";
14+
import type { CssProperty } from "@webstudio-is/css-engine";
15+
import {
16+
DeleteCssVariableDialog,
17+
RenameCssVariableDialog,
18+
$usedCssVariablesInInstances,
19+
$cssVariableInstancesByVariable,
20+
$cssVariableDefinitionsByVariable,
21+
} from "~/builder/shared/css-variable-utils";
22+
import { deleteProperty } from "~/builder/features/style-panel/shared/use-style-data";
23+
import { InstanceList, showInstance } from "../shared/instance-list";
24+
import {
25+
$commandContent,
26+
closeCommandPanel,
27+
focusCommandPanel,
28+
} from "../command-state";
29+
import type { BaseOption } from "../shared/types";
30+
import { getInstanceLabel } from "~/builder/shared/instance-label";
31+
import { $instances } from "~/shared/nano-states";
32+
import { $registeredComponentMetas } from "~/shared/nano-states";
33+
34+
export type CssVariableOption = BaseOption & {
35+
type: "cssVariable";
36+
property: string;
37+
instanceId: Instance["id"];
38+
usages: number;
39+
};
40+
41+
export const $cssVariableOptions = computed(
42+
[
43+
$cssVariableDefinitionsByVariable,
44+
$usedCssVariablesInInstances,
45+
$instances,
46+
$registeredComponentMetas,
47+
],
48+
(definitionsByVariable, usedVariablesInInstances, instances, metas) => {
49+
const cssVariableOptions: CssVariableOption[] = [];
50+
51+
// Create options for each defined CSS variable on each instance
52+
for (const [property, instanceIds] of definitionsByVariable) {
53+
for (const instanceId of instanceIds) {
54+
const instance = instances.get(instanceId);
55+
if (!instance) {
56+
continue;
57+
}
58+
const meta = metas.get(instance.component);
59+
const instanceLabel = getInstanceLabel(instance, meta);
60+
61+
cssVariableOptions.push({
62+
terms: [
63+
"css variables",
64+
property,
65+
property.slice(2), // Include name without --
66+
instanceLabel,
67+
],
68+
type: "cssVariable",
69+
property,
70+
instanceId,
71+
usages: usedVariablesInInstances.get(property) ?? 0,
72+
});
73+
}
74+
}
75+
76+
return cssVariableOptions;
77+
}
78+
);
79+
80+
const CssVariableInstances = ({ property }: { property: string }) => {
81+
const instancesByVariable = useStore($cssVariableInstancesByVariable);
82+
const usedInInstanceIds = instancesByVariable.get(property) ?? new Set();
83+
84+
return (
85+
<InstanceList
86+
instanceIds={usedInInstanceIds}
87+
onSelect={(instanceId) => {
88+
showInstance(instanceId, "style");
89+
closeCommandPanel();
90+
}}
91+
/>
92+
);
93+
};
94+
95+
export const CssVariablesGroup = ({
96+
options,
97+
}: {
98+
options: CssVariableOption[];
99+
}) => {
100+
const action = useSelectedAction();
101+
const resetActionIndex = useResetActionIndex();
102+
const instances = useStore($instances);
103+
const metas = useStore($registeredComponentMetas);
104+
const [variableDialog, setVariableDialog] = useState<
105+
{ action: "rename" | "delete"; property: string } | undefined
106+
>();
107+
108+
return (
109+
<>
110+
<CommandGroup
111+
name="cssVariable"
112+
heading={<CommandGroupHeading>CSS Variables</CommandGroupHeading>}
113+
actions={["select", "find usages", "rename", "delete"]}
114+
>
115+
{options.map(({ property, instanceId, usages }) => {
116+
const instance = instances.get(instanceId);
117+
const meta = instance ? metas.get(instance.component) : undefined;
118+
const instanceLabel = instance
119+
? getInstanceLabel(instance, meta)
120+
: "";
121+
122+
return (
123+
<CommandItem
124+
keywords={["test"]}
125+
key={`${property}-${instanceId}`}
126+
// preserve selected state when rerender
127+
value={`${property}-${instanceId}`}
128+
onSelect={() => {
129+
if (action === "select") {
130+
showInstance(instanceId, "style");
131+
closeCommandPanel();
132+
}
133+
if (action === "find usages") {
134+
$commandContent.set(
135+
<CssVariableInstances property={property} />
136+
);
137+
}
138+
if (action === "rename") {
139+
setVariableDialog({ action: "rename", property });
140+
}
141+
if (action === "delete") {
142+
setVariableDialog({ action: "delete", property });
143+
}
144+
}}
145+
>
146+
<Text>
147+
{property}{" "}
148+
<Text as="span" color="moreSubtle">
149+
{usages === 0
150+
? "unused"
151+
: `${usages} ${usages === 1 ? "usage" : "usages"}`}
152+
</Text>
153+
</Text>
154+
<Text as="span" color="moreSubtle">
155+
{instanceLabel}
156+
</Text>
157+
</CommandItem>
158+
);
159+
})}
160+
</CommandGroup>
161+
<RenameCssVariableDialog
162+
cssVariable={
163+
variableDialog?.action === "rename" ? variableDialog : undefined
164+
}
165+
onClose={() => {
166+
setVariableDialog(undefined);
167+
resetActionIndex();
168+
focusCommandPanel();
169+
}}
170+
onConfirm={(_oldProperty, newProperty) => {
171+
toast.success(
172+
`CSS variable renamed from "${variableDialog?.property}" to "${newProperty}"`
173+
);
174+
setVariableDialog(undefined);
175+
}}
176+
/>
177+
<DeleteCssVariableDialog
178+
cssVariable={
179+
variableDialog?.action === "delete" ? variableDialog : undefined
180+
}
181+
onClose={() => {
182+
setVariableDialog(undefined);
183+
resetActionIndex();
184+
focusCommandPanel();
185+
}}
186+
onConfirm={(property) => {
187+
deleteProperty(property as CssProperty);
188+
toast.success(`CSS variable "${variableDialog?.property}" deleted`);
189+
setVariableDialog(undefined);
190+
}}
191+
/>
192+
</>
193+
);
194+
};

0 commit comments

Comments
 (0)