Files
Oleg Isonen e9a3a1ac00 feat: Support any media query condition as a breakpoint (#5547)
## Description

- [x] add a list of conditions
- [x] allow other units for min/max

## 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
2026-01-04 18:48:45 +01:00

73 lines
2.6 KiB
TypeScript

import { compareMedia, equalMedia } from "@webstudio-is/css-engine";
import type { Breakpoint, Breakpoints } from "@webstudio-is/sdk";
/**
* Check if a breakpoint is the base breakpoint (no min or max width).
* Note: Does not check for custom conditions.
*/
export const isBaseBreakpoint = (breakpoint: {
minWidth?: number;
maxWidth?: number;
}) => breakpoint.minWidth === undefined && breakpoint.maxWidth === undefined;
/**
* Group breakpoints into width-based and custom condition categories.
* Width-based breakpoints are ordered: min-width (largest to smallest), base, max-width (largest to smallest).
* Custom condition breakpoints are kept separate and not sorted.
*
* Note: minWidth/maxWidth and condition are mutually exclusive - a breakpoint has either
* width-based properties OR a custom condition, never both.
*/
export const groupBreakpoints = <
T extends { minWidth?: number; maxWidth?: number; condition?: string },
>(
breakpoints: Array<T>
): { widthBased: Array<T>; custom: Array<T> } => {
const custom = breakpoints.filter(
(breakpoint) => breakpoint.condition !== undefined
);
const widthBased = breakpoints.filter(
(breakpoint) => breakpoint.condition === undefined
);
const sorted = [...widthBased].sort(compareMedia);
const maxs = sorted.filter((breakpoint) => breakpoint.maxWidth !== undefined);
const mins = sorted
.filter((breakpoint) => breakpoint.minWidth !== undefined)
.reverse();
const base = sorted.filter(isBaseBreakpoint);
return {
widthBased: [...mins, ...base, ...maxs],
custom,
};
};
/**
* Build a map of merged breakpoint IDs from fragment breakpoints to existing breakpoints.
* Breakpoints are merged when they have matching minWidth, maxWidth, condition, and label.
* Note: minWidth/maxWidth and condition are mutually exclusive.
*
* @param fragmentBreakpoints - Breakpoints from the fragment being inserted
* @param existingBreakpoints - Existing breakpoints in the project
* @returns Map of fragment breakpoint IDs to existing breakpoint IDs
*/
export const buildMergedBreakpointIds = (
fragmentBreakpoints: Breakpoint[],
existingBreakpoints: Breakpoints
): Map<Breakpoint["id"], Breakpoint["id"]> => {
const mergedBreakpointIds = new Map<Breakpoint["id"], Breakpoint["id"]>();
for (const newBreakpoint of fragmentBreakpoints) {
for (const breakpoint of existingBreakpoints.values()) {
if (equalMedia(breakpoint, newBreakpoint)) {
mergedBreakpointIds.set(newBreakpoint.id, breakpoint.id);
break;
}
}
}
return mergedBreakpointIds;
};
// Doesn't make sense to allow resizing the canvas lower than this.
export const minCanvasWidth = 240;