feat: Grid child area picker (#5636)

## 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
This commit is contained in:
Oleg Isonen authored and GitHub committed 2026-03-11 20:21:55 +01:00
1 parent 994a4f787a
commit 8aeb55aec4
4 files changed
+76 -37

No files matched your search

@@ -13,6 +13,7 @@ import {
} from "@webstudio-is/design-system";
import type { CssProperty, StyleValue } from "@webstudio-is/css-engine";
import { toValue } from "@webstudio-is/css-engine";
import { parseGridAreas, type AreaInfo } from "@webstudio-is/css-data";
import { StyleSection } from "../../shared/style-section";
import { PropertyLabel } from "../../property-label";
import { ToggleGroupTooltip } from "../../controls/toggle-group/toggle-group-control";
@@ -30,8 +31,8 @@ import {
deleteProperty,
resetEphemeralStyles,
} from "../../shared/use-style-data";
import { parseGridAreas } from "@webstudio-is/css-data";
import { getGridDimensions } from "../layout/shared/grid-areas";
import { GridAreaPicker } from "../layout/shared/grid-area-picker";
import {
GridPositionInputs,
type GridPosition,
@@ -41,6 +42,7 @@ import { AlignSelfControl, JustifySelfControl } from "../shared/align-self";
import { OrderControl } from "../shared/order";
import { useStore } from "@nanostores/react";
import { $selectedInstancePath, selectInstance } from "~/shared/awareness";
import { $isStylePanelGridVisible } from "~/builder/shared/nano-states";
import { ExternalLinkIcon } from "@webstudio-is/icons";
export const properties = [
@@ -112,6 +114,19 @@ const derivePositionMode = (
return "auto";
};
/**
* Shows grid guides on the canvas while this component is mounted.
*/
const GridChildGuides = () => {
useEffect(() => {
$isStylePanelGridVisible.set(true);
return () => {
$isStylePanelGridVisible.set(false);
};
}, []);
return null;
};
export const Section = () => {
const [columnStart, columnEnd, rowStart, rowEnd] = useComputedStyles([
"grid-column-start",
@@ -221,6 +236,7 @@ export const Section = () => {
)
}
>
<GridChildGuides />
<Flex css={{ flexDirection: "column", gap: theme.spacing[5] }}>
<GridChildPositionMode
value={positionMode}
@@ -645,25 +661,50 @@ const GridChildPositionManual = () => {
batch.publish({ isEphemeral: true });
};
const handlePickerChange = (area: AreaInfo) => {
const newPosition: GridPosition = {
columnStart: area.columnStart,
columnEnd: area.columnEnd,
rowStart: area.rowStart,
rowEnd: area.rowEnd,
};
localValue.set(newPosition);
handleChange(newPosition);
localValue.save();
resetEphemeralStyles();
};
return (
<Grid
css={{
gridTemplateColumns: "3fr 8fr",
alignItems: "start",
}}
>
<div />
<GridPositionInputs
value={localValue.value}
onChange={handleChange}
onBlur={() => {
localValue.save();
resetEphemeralStyles();
<Flex direction="column" gap="2">
<Grid
css={{
gridTemplateColumns: "3fr 8fr",
alignItems: "start",
}}
>
<div />
<GridPositionInputs
value={localValue.value}
onChange={handleChange}
onBlur={() => {
localValue.save();
resetEphemeralStyles();
}}
gridColumns={gridColumns}
gridRows={gridRows}
/>
</Grid>
<GridAreaPicker
value={{
name: "",
...localValue.value,
}}
onChange={handlePickerChange}
gridColumns={gridColumns}
gridRows={gridRows}
otherAreas={[]}
/>
</Grid>
</Flex>
);
};
@@ -61,7 +61,7 @@ import { FlexAlignment } from "./shared/flex-alignment";
import { GridGenerator } from "./shared/grid-generator";
import { GridSettings } from "./shared/grid-settings";
import { GridAlignment } from "./shared/grid-alignment";
import { $openGridEditorPanel } from "~/builder/shared/nano-states";
import { $isStylePanelGridVisible } from "~/builder/shared/nano-states";
import { humanizeString } from "~/shared/string-utils";
import { DEFAULT_GRID_TRACK_COUNT, DEFAULT_GRID_GAP } from "./shared/constants";
@@ -573,21 +573,27 @@ const applyDefaultGridStyles = (columnsValue: string, rowsValue: string) => {
};
const LayoutSectionGrid = () => {
const openPanel = useStore($openGridEditorPanel);
const [openPanel, setOpenPanel] = useState({
generator: false,
settings: false,
});
useEffect(() => {
$isStylePanelGridVisible.set(true);
return () => {
$isStylePanelGridVisible.set(false);
};
}, []);
return (
<Flex direction="column" gap="2">
<GridGenerator
open={openPanel.generator}
onOpenChange={(open) =>
$openGridEditorPanel.set({ ...openPanel, generator: open })
}
onOpenChange={(open) => setOpenPanel({ ...openPanel, generator: open })}
/>
<GridSettings
open={openPanel.settings}
onOpenChange={(open) =>
$openGridEditorPanel.set({ ...openPanel, settings: open })
}
onOpenChange={(open) => setOpenPanel({ ...openPanel, settings: open })}
/>
<Grid
css={{
@@ -21,7 +21,7 @@ import { applyScale } from "./outline";
import {
$clampingRect,
$scale,
$openGridEditorPanel,
$isStylePanelGridVisible,
} from "~/builder/shared/nano-states";
import { BlockChildHoveredInstanceOutline } from "./outline/block-instance-outline";
import { TextEditorContextMenu } from "./block-editor-context-menu";
@@ -49,7 +49,7 @@ export const CanvasTools = () => {
const instances = useStore($instances);
const scale = useStore($scale);
const clampingRect = useStore($clampingRect);
const openGridEditorPanel = useStore($openGridEditorPanel);
const isStylePanelGridVisible = useStore($isStylePanelGridVisible);
if (!canvasToolsVisible) {
return;
@@ -86,9 +86,7 @@ export const CanvasTools = () => {
<>
{isPreviewMode === false && (
<>
{(openGridEditorPanel.generator || openGridEditorPanel.settings) && (
<GridGuides />
)}
{isStylePanelGridVisible && <GridGuides />}
<SelectedInstanceOutline />
<HoveredInstanceOutline />
<CollaborativeInstanceOutline />
@@ -168,12 +168,6 @@ export type GridEditingArea = {
export const $gridEditingArea = atom<GridEditingArea | undefined>(undefined);
// Which grid editor panels are open.
// Used to conditionally show grid guides on the canvas.
export const $openGridEditorPanel = atom<{
generator: boolean;
settings: boolean;
}>({
generator: false,
settings: false,
});
// Whether a grid-related section is visible in the style panel.
// Grid guides are shown on the canvas when true.
export const $isStylePanelGridVisible = atom(false);