Skip to content

Commit 8b60ff7

Browse files
authored
feat: infer states from tags in style panel (#5229)
Ref #3632 Now states are always inferred from tags. User action states work for all tags. <img width="233" alt="Screenshot 2025-05-22 at 19 27 18" src="https://github.com/user-attachments/assets/f0dc6a07-8a01-4878-950e-61b6e489da7a" />
1 parent c630054 commit 8b60ff7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+168
-318
lines changed

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState } from "react";
22
import { useStore } from "@nanostores/react";
33
import { nanoid } from "nanoid";
44
import { computed, type WritableAtom } from "nanostores";
5+
import { pseudoClassesByTag } from "@webstudio-is/html-data";
56
import {
67
type Instance,
78
type StyleSource,
@@ -41,6 +42,8 @@ import { removeByMutable } from "~/shared/array-utils";
4142
import { cloneStyles } from "~/shared/tree-utils";
4243
import { serverSyncStore } from "~/shared/sync";
4344
import { $selectedInstance } from "~/shared/awareness";
45+
import { $instanceTags } from "./shared/model";
46+
import { humanizeString } from "~/shared/string-utils";
4447

4548
const selectStyleSource = (
4649
styleSourceId: StyleSource["id"],
@@ -325,12 +328,26 @@ const clearStyles = (styleSourceId: StyleSource["id"]) => {
325328
};
326329

327330
const $componentStates = computed(
328-
[$selectedInstance, $registeredComponentMetas],
329-
(selectedInstance, registeredComponentMetas) => {
331+
[$selectedInstance, $registeredComponentMetas, $instanceTags],
332+
(selectedInstance, registeredComponentMetas, instanceTags) => {
330333
if (selectedInstance === undefined) {
331334
return;
332335
}
333-
return registeredComponentMetas.get(selectedInstance.component)?.states;
336+
const tag = instanceTags.get(selectedInstance.id);
337+
const tagStates = [
338+
...pseudoClassesByTag["*"],
339+
...(pseudoClassesByTag[tag ?? ""] ?? []),
340+
].map((state) => ({
341+
category: "states" as const,
342+
label: humanizeString(state),
343+
selector: state,
344+
}));
345+
const meta = registeredComponentMetas.get(selectedInstance.component);
346+
const componentStates = (meta?.states ?? []).map((item) => ({
347+
category: "component-states" as const,
348+
...item,
349+
}));
350+
return [...tagStates, ...componentStates];
334351
}
335352
);
336353

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ const Menu = (props: MenuProps) => {
9898
</DropdownMenuTrigger>
9999
<DropdownMenuContent
100100
onCloseAutoFocus={(event) => event.preventDefault()}
101-
css={{ maxWidth: theme.spacing[24] }}
101+
css={{ maxWidth: theme.spacing[26] }}
102102
>
103103
{props.children}
104104
</DropdownMenuContent>

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import {
5050
useCallback,
5151
} from "react";
5252
import { mergeRefs } from "@react-aria/utils";
53-
import { type ComponentState, stateCategories } from "@webstudio-is/sdk";
5453
import {
5554
type ItemSource,
5655
type StyleSourceError,
@@ -268,6 +267,17 @@ const TextFieldBase: ForwardRefRenderFunction<
268267
const TextField = forwardRef(TextFieldBase);
269268
TextField.displayName = "TextField";
270269

270+
type ComponentState = {
271+
category: "states" | "component-states";
272+
selector: string;
273+
label: string;
274+
};
275+
276+
const categories = [
277+
"states",
278+
"component-states",
279+
] satisfies ComponentState["category"][];
280+
271281
type StyleSourceInputProps<Item extends IntermediateItem> = {
272282
$styleSourceInputElement: WritableAtom<HTMLInputElement | undefined>;
273283
error?: StyleSourceError;
@@ -417,9 +427,9 @@ const renderMenuItems = (props: {
417427
</DropdownMenuItem>
418428
)}
419429

420-
{stateCategories.map((currentCategory) => {
430+
{categories.map((currentCategory) => {
421431
const categoryStates = props.states.filter(
422-
({ category }) => (category ?? "states") === currentCategory
432+
({ category }) => category === currentCategory
423433
);
424434
// prevent rendering empty category
425435
if (categoryStates.length === 0) {

packages/html-data/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./__generated__/elements";
22
export * from "./__generated__/attributes";
33
export * from "./__generated__/aria";
4+
export * from "./pseudo-classes";
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// https://drafts.csswg.org/selectors
2+
3+
const location = [
4+
// ':link',
5+
":visited",
6+
// ':any-link',
7+
// ':local-link',
8+
// ':target',
9+
// ':target-within',
10+
];
11+
12+
const userAction = [":hover", ":focus-visible", ":focus-within", ":active"];
13+
14+
const ability = [
15+
// ":enabled",
16+
":disabled",
17+
];
18+
19+
const validity = [
20+
// ":valid",
21+
":invalid",
22+
// ":user-valid",
23+
":user-invalid",
24+
];
25+
26+
const required = [
27+
":required",
28+
// ":optional"
29+
];
30+
31+
export const pseudoClassesByTag: Record<string, string[]> = {
32+
"*": userAction,
33+
a: [...location],
34+
area: [...location],
35+
button: [...ability],
36+
label: [],
37+
input: [
38+
":placeholder-shown",
39+
// @todo temporary until proper pseudo elements support is added
40+
"::placeholder",
41+
...ability,
42+
...validity,
43+
...required,
44+
":checked",
45+
// ":indeterminate",
46+
// :in-range
47+
// :out-of-range
48+
// ":open",
49+
],
50+
textarea: [
51+
":placeholder-shown",
52+
// @todo temporary until proper pseudo elements support is added
53+
"::placeholder",
54+
...ability,
55+
...validity,
56+
...required,
57+
],
58+
select: [
59+
...ability,
60+
...validity,
61+
...required,
62+
// ":open"
63+
],
64+
optgroup: [...ability],
65+
option: [...ability, ":checked"],
66+
fieldset: [...ability, ...validity],
67+
progress: [":indeterminate"],
68+
details: [":open"],
69+
dialog: [":open"],
70+
};

packages/sdk-components-react-radix/src/accordion.ws.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
TriggerIcon,
66
ContentIcon,
77
} from "@webstudio-is/icons/svg";
8-
import { defaultStates, type WsComponentMeta } from "@webstudio-is/sdk";
8+
import type { WsComponentMeta } from "@webstudio-is/sdk";
99
import { div, h3, button } from "@webstudio-is/sdk/normalize.css";
1010
import { radix } from "./shared/meta";
1111
import { buttonReset } from "./shared/preset-styles";
@@ -74,14 +74,7 @@ export const metaAccordionTrigger: WsComponentMeta = {
7474
category: "none",
7575
children: ["instance", "rich-text"],
7676
},
77-
states: [
78-
...defaultStates,
79-
{
80-
category: "component-states",
81-
label: "Open",
82-
selector: "[data-state=open]",
83-
},
84-
],
77+
states: [{ label: "Open", selector: "[data-state=open]" }],
8578
presetStyle: {
8679
button: [button, buttonReset].flat(),
8780
},

packages/sdk-components-react-radix/src/checkbox.ws.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CheckboxCheckedIcon, TriggerIcon } from "@webstudio-is/icons/svg";
2-
import { defaultStates, type WsComponentMeta } from "@webstudio-is/sdk";
2+
import type { WsComponentMeta } from "@webstudio-is/sdk";
33
import { button, span } from "@webstudio-is/sdk/normalize.css";
44
import { radix } from "./shared/meta";
55
import { buttonReset } from "./shared/preset-styles";
@@ -16,17 +16,8 @@ export const metaCheckbox: WsComponentMeta = {
1616
descendants: [radix.CheckboxIndicator],
1717
},
1818
states: [
19-
...defaultStates,
20-
{
21-
label: "Checked",
22-
selector: "[data-state=checked]",
23-
category: "component-states",
24-
},
25-
{
26-
label: "Unchecked",
27-
selector: "[data-state=unchecked]",
28-
category: "component-states",
29-
},
19+
{ label: "Checked", selector: "[data-state=checked]" },
20+
{ label: "Unchecked", selector: "[data-state=unchecked]" },
3021
],
3122
presetStyle: {
3223
button: [button, buttonReset].flat(),
@@ -41,7 +32,6 @@ export const metaCheckboxIndicator: WsComponentMeta = {
4132
category: "none",
4233
children: ["instance", "rich-text"],
4334
},
44-
states: defaultStates,
4535
presetStyle: {
4636
span,
4737
},

packages/sdk-components-react-radix/src/dialog.ws.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
TextIcon,
88
ButtonElementIcon,
99
} from "@webstudio-is/icons/svg";
10-
import { defaultStates, type WsComponentMeta } from "@webstudio-is/sdk";
10+
import type { WsComponentMeta } from "@webstudio-is/sdk";
1111
import { div, button, h2, p } from "@webstudio-is/sdk/normalize.css";
1212
import { radix } from "./shared/meta";
1313
import {
@@ -84,7 +84,6 @@ export const metaDialogClose: WsComponentMeta = {
8484
category: "none",
8585
children: ["instance", "rich-text"],
8686
},
87-
states: defaultStates,
8887
presetStyle: {
8988
button: [buttonReset, button].flat(),
9089
},
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { LabelIcon } from "@webstudio-is/icons/svg";
2-
import { defaultStates, type WsComponentMeta } from "@webstudio-is/sdk";
2+
import type { WsComponentMeta } from "@webstudio-is/sdk";
33
import { label } from "@webstudio-is/sdk/normalize.css";
44
import { props } from "./__generated__/label.props";
55

66
export const meta: WsComponentMeta = {
77
icon: LabelIcon,
8-
states: defaultStates,
9-
presetStyle: {
10-
label,
11-
},
8+
presetStyle: { label },
129
initialProps: ["id", "class", "for"],
1310
props,
1411
};

packages/sdk-components-react-radix/src/popover.ws.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
ContentIcon,
55
ButtonElementIcon,
66
} from "@webstudio-is/icons/svg";
7-
import { defaultStates, type WsComponentMeta } from "@webstudio-is/sdk";
7+
import type { WsComponentMeta } from "@webstudio-is/sdk";
88
import { button, div } from "@webstudio-is/sdk/normalize.css";
99
import { radix } from "./shared/meta";
1010
import {
@@ -57,7 +57,6 @@ export const metaPopoverClose: WsComponentMeta = {
5757
category: "none",
5858
children: ["instance", "rich-text"],
5959
},
60-
states: defaultStates,
6160
presetStyle: {
6261
button: [buttonReset, button].flat(),
6362
},

0 commit comments

Comments
 (0)