Skip to content

Commit 89a8f22

Browse files
committed
first
1 parent 9b9ba34 commit 89a8f22

File tree

15 files changed

+1014
-324
lines changed

15 files changed

+1014
-324
lines changed

apps/builder/app/builder/features/breakpoints/breakpoints-editor.tsx

Lines changed: 102 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {
1414
Separator,
1515
Box,
1616
toast,
17+
Popover,
18+
PopoverTrigger,
19+
PopoverContent,
1720
} from "@webstudio-is/design-system";
1821
import { MinusIcon, PlusIcon } from "@webstudio-is/icons";
1922
import { useStore } from "@nanostores/react";
@@ -30,8 +33,9 @@ type BreakpointEditorItemProps = {
3033

3134
const BreakpointFormData = z.object({
3235
label: z.string(),
33-
type: z.enum(["minWidth", "maxWidth"]),
34-
value: z.string().transform(Number),
36+
type: z.enum(["minWidth", "maxWidth"]).optional(),
37+
value: z.string().transform(Number).optional(),
38+
condition: z.string().optional(),
3539
});
3640

3741
const useHandleChangeComplete = (
@@ -67,8 +71,16 @@ const useHandleChangeComplete = (
6771
const newBreakpoint: Breakpoint = {
6872
id: breakpoint.id,
6973
label: parsed.data.label,
70-
[parsed.data.type]: parsed.data.value,
7174
};
75+
76+
// If condition is set, use it exclusively
77+
if (parsed.data.condition && parsed.data.condition.trim() !== "") {
78+
newBreakpoint.condition = parsed.data.condition.trim();
79+
} else if (parsed.data.type && parsed.data.value !== undefined) {
80+
// Otherwise use width
81+
newBreakpoint[parsed.data.type] = parsed.data.value;
82+
}
83+
7284
onChangeComplete(newBreakpoint);
7385
};
7486
const handleChangeCompleteRef = useRef(handleChangeComplete);
@@ -89,6 +101,11 @@ const BreakpointEditorItem = ({
89101
const { formRef, handleChangeComplete, handleChange } =
90102
useHandleChangeComplete(breakpoint, onChangeComplete);
91103

104+
const [conditionValue, setConditionValue] = useState(
105+
breakpoint.condition ?? ""
106+
);
107+
const hasCondition = conditionValue.trim() !== "";
108+
92109
return (
93110
<Flex gap="2">
94111
<form
@@ -125,14 +142,16 @@ const BreakpointEditorItem = ({
125142
}
126143
defaultValue={breakpoint.maxWidth ? "maxWidth" : "minWidth"}
127144
onChange={handleChangeComplete}
145+
disabled={hasCondition}
128146
/>
129147
<InputField
130148
css={{ flexShrink: 1 }}
131149
defaultValue={breakpoint.minWidth ?? breakpoint.maxWidth ?? 0}
132150
type="number"
133151
name="value"
134152
min={0}
135-
required
153+
required={!hasCondition}
154+
disabled={hasCondition}
136155
suffix={
137156
<Text
138157
variant="unit"
@@ -145,6 +164,18 @@ const BreakpointEditorItem = ({
145164
}
146165
/>
147166
</Flex>
167+
<InputField
168+
name="condition"
169+
css={{ width: "100%" }}
170+
type="text"
171+
value={conditionValue}
172+
onChange={(event) => {
173+
setConditionValue(event.target.value);
174+
handleChange();
175+
}}
176+
placeholder="e.g., orientation: portrait"
177+
onBlur={handleChangeComplete}
178+
/>
148179
</Flex>
149180
</form>
150181
<IconButton
@@ -160,22 +191,38 @@ const BreakpointEditorItem = ({
160191

161192
type BreakpointsEditorProps = {
162193
onDelete: (breakpoint: Breakpoint) => void;
194+
children: React.ReactNode;
195+
open?: boolean;
196+
onOpenChange?: (open: boolean) => void;
163197
};
164198

165-
export const BreakpointsEditor = ({ onDelete }: BreakpointsEditorProps) => {
199+
export const BreakpointsEditor = ({
200+
onDelete,
201+
children,
202+
open,
203+
onOpenChange,
204+
}: BreakpointsEditorProps) => {
166205
const breakpoints = useStore($breakpoints);
167206
const [addedBreakpoints, setAddedBreakpoints] = useState<Breakpoint[]>([]);
168207
const initialBreakpointsRef = useRef(
169208
groupBreakpoints(Array.from(breakpoints.values()))
170209
);
210+
const initialBreakpointsFlat = [
211+
...initialBreakpointsRef.current.widthBased,
212+
...initialBreakpointsRef.current.custom,
213+
];
171214
const allBreakpoints = [
172215
...addedBreakpoints,
173-
...initialBreakpointsRef.current.filter(
216+
...initialBreakpointsFlat.filter(
174217
(breakpoint) =>
175218
addedBreakpoints.find((added) => added.id === breakpoint.id) ===
176219
undefined
177220
),
178-
].filter((breakpoint) => isBaseBreakpoint(breakpoint) === false);
221+
].filter(
222+
(breakpoint) =>
223+
breakpoint.condition !== undefined ||
224+
isBaseBreakpoint(breakpoint) === false
225+
);
179226

180227
const handleChangeComplete = (breakpoint: Breakpoint) => {
181228
serverSyncStore.createTransaction([$breakpoints], (breakpoints) => {
@@ -184,47 +231,54 @@ export const BreakpointsEditor = ({ onDelete }: BreakpointsEditorProps) => {
184231
};
185232

186233
return (
187-
<Flex direction="column">
188-
<PanelTitle
189-
css={{ paddingInline: theme.panel.paddingInline }}
190-
suffix={
191-
<IconButton
192-
onClick={() => {
193-
const newBreakpoint: Breakpoint = {
194-
id: nanoid(),
195-
label: "",
196-
minWidth: 0,
197-
};
198-
setAddedBreakpoints([newBreakpoint, ...addedBreakpoints]);
199-
}}
234+
<Popover open={open} onOpenChange={onOpenChange} modal>
235+
<PopoverTrigger asChild>{children}</PopoverTrigger>
236+
<PopoverContent>
237+
<Flex direction="column">
238+
<PanelTitle
239+
css={{ paddingInline: theme.panel.paddingInline }}
240+
suffix={
241+
<IconButton
242+
onClick={() => {
243+
const newBreakpoint: Breakpoint = {
244+
id: nanoid(),
245+
label: "",
246+
minWidth: 0,
247+
};
248+
setAddedBreakpoints([newBreakpoint, ...addedBreakpoints]);
249+
}}
250+
>
251+
<PlusIcon />
252+
</IconButton>
253+
}
200254
>
201-
<PlusIcon />
202-
</IconButton>
203-
}
204-
>
205-
{"Breakpoints"}
206-
</PanelTitle>
207-
<Separator />
208-
<Fragment>
209-
{allBreakpoints.map((breakpoint, index, all) => {
210-
return (
211-
<Fragment key={breakpoint.id}>
212-
<Box css={{ p: theme.panel.padding }}>
213-
<BreakpointEditorItem
214-
breakpoint={breakpoint}
215-
onChangeComplete={handleChangeComplete}
216-
onDelete={onDelete}
217-
autoFocus={index === 0}
218-
/>
219-
</Box>
220-
{index < all.length - 1 && <PopoverSeparator />}
221-
</Fragment>
222-
);
223-
})}
224-
</Fragment>
225-
{allBreakpoints.length === 0 && (
226-
<Text css={{ margin: theme.spacing[10] }}>No breakpoints found</Text>
227-
)}
228-
</Flex>
255+
{"Breakpoints"}
256+
</PanelTitle>
257+
<Separator />
258+
<Fragment>
259+
{allBreakpoints.map((breakpoint, index, all) => {
260+
return (
261+
<Fragment key={breakpoint.id}>
262+
<Box css={{ p: theme.panel.padding }}>
263+
<BreakpointEditorItem
264+
breakpoint={breakpoint}
265+
onChangeComplete={handleChangeComplete}
266+
onDelete={onDelete}
267+
autoFocus={index === 0}
268+
/>
269+
</Box>
270+
{index < all.length - 1 && <PopoverSeparator />}
271+
</Fragment>
272+
);
273+
})}
274+
</Fragment>
275+
{allBreakpoints.length === 0 && (
276+
<Text css={{ margin: theme.spacing[10] }}>
277+
No breakpoints found
278+
</Text>
279+
)}
280+
</Flex>
281+
</PopoverContent>
282+
</Popover>
229283
);
230284
};

0 commit comments

Comments
 (0)