react-display-scale-engine 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Piyawat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # react-display-scale-engine
2
+
3
+ React utilities for maintaining a consistent visual density on Windows displays with elevated OS display scaling, with first-class Material UI overlay support.
4
+
5
+ The library is split into entry points, so an application only takes the layer it needs:
6
+
7
+ ```ts
8
+ import { detectDisplayScale } from "react-display-scale-engine/core";
9
+ import { DisplayScaleProvider } from "react-display-scale-engine/react";
10
+ import { DisplayScaleMenu } from "react-display-scale-engine/mui";
11
+ ```
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install react-display-scale-engine
17
+ ```
18
+
19
+ `react`, `react-dom`, `@mui/material`, `@emotion/react`, `@emotion/styled`, and `@popperjs/core` are peer dependencies. The MUI peers are necessary only for the `/mui` entry point.
20
+
21
+ ## Quick start
22
+
23
+ Call the optional pre-mount warm-up, then wrap the application once.
24
+
25
+ ```tsx
26
+ import { primeDisplayScaleCompensation } from "react-display-scale-engine/core";
27
+ import { DisplayScaleProvider } from "react-display-scale-engine/react";
28
+
29
+ primeDisplayScaleCompensation();
30
+
31
+ export function AppRoot() {
32
+ return <DisplayScaleProvider><App /></DisplayScaleProvider>;
33
+ }
34
+ ```
35
+
36
+ Use supplied components for MUI overlays whose placement must account for the compensated coordinate space.
37
+
38
+ ```tsx
39
+ import { DisplayScaleMenu } from "react-display-scale-engine/mui";
40
+
41
+ <DisplayScaleMenu anchorEl={anchorEl} open={open} onClose={close}>
42
+ <MenuItem onClick={close}>Action</MenuItem>
43
+ </DisplayScaleMenu>;
44
+ ```
45
+
46
+ See [the engine guide](docs/display-scale-engine.md) for policy, limitations, and migration guidance.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ npm install
52
+ npm run check
53
+ npm run build
54
+ npm pack --dry-run
55
+ ```
56
+
57
+ Public entry points:
58
+
59
+ | Import | Purpose |
60
+ | --- | --- |
61
+ | `react-display-scale-engine` | All public exports |
62
+ | `…/core` | Framework-independent detection and compensation math |
63
+ | `…/react` | Provider and React hooks |
64
+ | `…/mui` | MUI Menu, Popover, Popper, Select, Drawer and token adapters |
65
+
66
+ ## Publishing
67
+
68
+ Before first publishing, make sure the `name` in `package.json` is an npm scope you own, then run `npm login`.
69
+
70
+ ```bash
71
+ npm run release:check
72
+ npm run release:dry-run
73
+ npm run release:publish -- --otp 123456
74
+ ```
75
+
76
+ The package includes a CLI:
77
+
78
+ ```bash
79
+ npx rds-engine check
80
+ npx rds-engine publish --dry-run
81
+ npx rds-engine publish --otp 123456
82
+ ```
83
+
84
+ `publish` always builds and runs `npm pack --dry-run` first. It never changes the version; use `npm version patch|minor|major` before publishing. When npm account policy requires 2FA for publishing, supply the current authenticator code with `--otp` or use an npm granular access token that has bypass-2FA permission.
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+
7
+ const cwd = process.cwd();
8
+ const packagePath = resolve(cwd, "package.json");
9
+
10
+ function run(command, args) {
11
+ const result = spawnSync(command, args, { cwd, stdio: "inherit" });
12
+ if (result.error) {
13
+ console.error(`Could not run ${command}: ${result.error.message}`);
14
+ process.exit(1);
15
+ }
16
+ if (result.status !== 0) process.exit(result.status ?? 1);
17
+ }
18
+
19
+ function readPackage() {
20
+ try {
21
+ return JSON.parse(readFileSync(packagePath, "utf8"));
22
+ } catch {
23
+ console.error("No package.json was found in the current directory.");
24
+ process.exit(1);
25
+ }
26
+ }
27
+
28
+ function usage() {
29
+ console.log(`Usage:
30
+ rds-engine check
31
+ rds-engine publish [--dry-run] [--tag <tag>] [--access public|restricted] [--otp <code>]
32
+
33
+ Run from the package repository root.`);
34
+ }
35
+
36
+ const [command, ...args] = process.argv.slice(2);
37
+
38
+ if (!command || command === "--help" || command === "-h") {
39
+ usage();
40
+ process.exit(0);
41
+ }
42
+
43
+ if (command === "--version" || command === "-v") {
44
+ console.log(readPackage().version);
45
+ process.exit(0);
46
+ }
47
+
48
+ if (command !== "check" && command !== "publish") {
49
+ usage();
50
+ process.exit(1);
51
+ }
52
+
53
+ const pkg = readPackage();
54
+ if (!pkg.name || !pkg.version) {
55
+ console.error("package.json must include both name and version.");
56
+ process.exit(1);
57
+ }
58
+
59
+ run("npm", ["run", "check"]);
60
+ run("npm", ["pack", "--dry-run"]);
61
+
62
+ if (command === "check") {
63
+ console.log(`\\n✓ ${pkg.name}@${pkg.version} is ready to publish.`);
64
+ process.exit(0);
65
+ }
66
+
67
+ const dryRun = args.includes("--dry-run");
68
+ const publishArgs = ["publish"];
69
+ if (dryRun) publishArgs.push("--dry-run");
70
+
71
+ for (let index = 0; index < args.length; index += 1) {
72
+ const arg = args[index];
73
+ if (arg === "--tag" || arg === "--access" || arg === "--otp") {
74
+ const value = args[index + 1];
75
+ if (!value) {
76
+ console.error(`${arg} needs a value.`);
77
+ process.exit(1);
78
+ }
79
+ publishArgs.push(arg, value);
80
+ index += 1;
81
+ }
82
+ }
83
+
84
+ if (pkg.name.startsWith("@") && !args.includes("--access")) {
85
+ publishArgs.push("--access", "public");
86
+ }
87
+
88
+ console.log(`\\nPublishing ${pkg.name}@${pkg.version}${dryRun ? " (dry run)" : ""}…`);
89
+ run("npm", publishArgs);
@@ -0,0 +1,68 @@
1
+ import { toDisplayScaleLogicalPixels, getDisplayScaleOverlayMaxHeight, getDisplayScaleLogicalViewport } from './chunk-U3ZWHUJG.js';
2
+ import { useDisplayScaleCompensation, useDisplayScale } from './chunk-R5CTLLUS.js';
3
+ import { useCallback, useMemo } from 'react';
4
+
5
+ function useDisplayScaleOverlay() {
6
+ const { compensation, overlayStrategy, rootRef } = useDisplayScaleCompensation();
7
+ const toLogicalPixels = useCallback(
8
+ (value) => toDisplayScaleLogicalPixels(value, compensation),
9
+ [compensation]
10
+ );
11
+ const toOverlayContentPixels = useCallback(
12
+ (value) => toDisplayScaleLogicalPixels(value, compensation),
13
+ [compensation]
14
+ );
15
+ const getMaxHeightBelowAnchor = useCallback(
16
+ (anchor, options) => {
17
+ if (overlayStrategy === "document-body") {
18
+ const { offset = 8, viewportInset = 16 } = options ?? {};
19
+ const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
20
+ const visibleHeight = Math.max(
21
+ 0,
22
+ viewportHeight - anchor.getBoundingClientRect().bottom - offset - viewportInset
23
+ );
24
+ const zoom = compensation.zoom > 0 ? compensation.zoom : 1;
25
+ return Math.floor(visibleHeight / zoom);
26
+ }
27
+ return getDisplayScaleOverlayMaxHeight(anchor.getBoundingClientRect(), compensation, options);
28
+ },
29
+ [compensation, overlayStrategy]
30
+ );
31
+ return useMemo(
32
+ () => ({
33
+ portalContainer: overlayStrategy === "document-body" ? typeof document === "undefined" ? null : document.body : rootRef.current,
34
+ logicalViewport: getDisplayScaleLogicalViewport(compensation),
35
+ toLogicalPixels,
36
+ toOverlayContentPixels,
37
+ getMaxHeightBelowAnchor
38
+ }),
39
+ [
40
+ compensation,
41
+ getMaxHeightBelowAnchor,
42
+ overlayStrategy,
43
+ rootRef,
44
+ toLogicalPixels,
45
+ toOverlayContentPixels
46
+ ]
47
+ );
48
+ }
49
+ function useLogicalMinWidth(minWidth) {
50
+ const { snapshot } = useDisplayScale();
51
+ const { compensation } = useDisplayScaleCompensation();
52
+ return useMemo(() => {
53
+ const zoom = compensation.zoom > 0 ? compensation.zoom : 1;
54
+ return snapshot.viewport.width / zoom >= minWidth;
55
+ }, [compensation.zoom, minWidth, snapshot.viewport.width]);
56
+ }
57
+ function useLogicalMaxWidth(maxWidth) {
58
+ const { snapshot } = useDisplayScale();
59
+ const { compensation } = useDisplayScaleCompensation();
60
+ return useMemo(() => {
61
+ const zoom = compensation.zoom > 0 ? compensation.zoom : 1;
62
+ return snapshot.viewport.width / zoom <= maxWidth;
63
+ }, [compensation.zoom, maxWidth, snapshot.viewport.width]);
64
+ }
65
+
66
+ export { useDisplayScaleOverlay, useLogicalMaxWidth, useLogicalMinWidth };
67
+ //# sourceMappingURL=chunk-6UB65SJB.js.map
68
+ //# sourceMappingURL=chunk-6UB65SJB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useDisplayScaleOverlay.ts","../src/useLogicalBreakpoint.ts"],"names":["useMemo"],"mappings":"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","file":"chunk-6UB65SJB.js","sourcesContent":["import { useCallback, useMemo } from \"react\";\nimport { useDisplayScaleCompensation } from \"./DisplayScaleCompensationProvider\";\nimport {\n type DisplayScaleOverlayBoundsOptions,\n getDisplayScaleLogicalViewport,\n getDisplayScaleOverlayMaxHeight,\n toDisplayScaleLogicalPixels,\n} from \"./displayScaleLogicalViewport\";\n\nexport type DisplayScaleOverlay = {\n /** MUI portal target selected by the engine strategy. */\n portalContainer: HTMLElement | null;\n logicalViewport: ReturnType<typeof getDisplayScaleLogicalViewport>;\n toLogicalPixels: (value: number) => number;\n /** Converts a visible size to the CSS size required by density-zoomed content. */\n toOverlayContentPixels: (value: number) => number;\n getMaxHeightBelowAnchor: (\n anchor: HTMLElement,\n options?: DisplayScaleOverlayBoundsOptions,\n ) => number;\n};\n\n/**\n * Overlay geometry adapter. Use this instead of dividing coordinates by the\n * display-scale zoom in feature code.\n */\nexport function useDisplayScaleOverlay(): DisplayScaleOverlay {\n const { compensation, overlayStrategy, rootRef } = useDisplayScaleCompensation();\n\n // A density-zoomed portal consumes positioning values in its logical\n // coordinate space, even when it is hosted by document.body.\n const toLogicalPixels = useCallback(\n (value: number) => toDisplayScaleLogicalPixels(value, compensation),\n [compensation],\n );\n\n const toOverlayContentPixels = useCallback(\n (value: number) => toDisplayScaleLogicalPixels(value, compensation),\n [compensation],\n );\n\n const getMaxHeightBelowAnchor = useCallback(\n (anchor: HTMLElement, options?: DisplayScaleOverlayBoundsOptions) => {\n if (overlayStrategy === \"document-body\") {\n const { offset = 8, viewportInset = 16 } = options ?? {};\n const viewportHeight = window.visualViewport?.height ?? window.innerHeight;\n const visibleHeight = Math.max(\n 0,\n viewportHeight - anchor.getBoundingClientRect().bottom - offset - viewportInset,\n );\n\n // The portal is positioned in browser-visible pixels, but its content\n // inherits MUI's density zoom. Convert only a content dimension back\n // to logical pixels so its visible height reaches this boundary.\n const zoom = compensation.zoom > 0 ? compensation.zoom : 1;\n return Math.floor(visibleHeight / zoom);\n }\n return getDisplayScaleOverlayMaxHeight(anchor.getBoundingClientRect(), compensation, options);\n },\n [compensation, overlayStrategy],\n );\n\n return useMemo(\n () => ({\n portalContainer:\n overlayStrategy === \"document-body\"\n ? typeof document === \"undefined\"\n ? null\n : document.body\n : rootRef.current,\n logicalViewport: getDisplayScaleLogicalViewport(compensation),\n toLogicalPixels,\n toOverlayContentPixels,\n getMaxHeightBelowAnchor,\n }),\n [\n compensation,\n getMaxHeightBelowAnchor,\n overlayStrategy,\n rootRef,\n toLogicalPixels,\n toOverlayContentPixels,\n ],\n );\n}\n","import { useMemo } from \"react\";\nimport { useDisplayScaleCompensation } from \"./DisplayScaleCompensationProvider\";\nimport { useDisplayScale } from \"./useDisplayScale\";\n\n/**\n * Tests a breakpoint against the compensated root width rather than the raw\n * browser viewport. Use for app-shell decisions that must preserve the same\n * layout at Windows 100% and compensated 150%.\n */\nexport function useLogicalMinWidth(minWidth: number): boolean {\n const { snapshot } = useDisplayScale();\n const { compensation } = useDisplayScaleCompensation();\n\n return useMemo(() => {\n const zoom = compensation.zoom > 0 ? compensation.zoom : 1;\n return snapshot.viewport.width / zoom >= minWidth;\n }, [compensation.zoom, minWidth, snapshot.viewport.width]);\n}\n\n/** Logical counterpart to a max-width media query. */\nexport function useLogicalMaxWidth(maxWidth: number): boolean {\n const { snapshot } = useDisplayScale();\n const { compensation } = useDisplayScaleCompensation();\n\n return useMemo(() => {\n const zoom = compensation.zoom > 0 ? compensation.zoom : 1;\n return snapshot.viewport.width / zoom <= maxWidth;\n }, [compensation.zoom, maxWidth, snapshot.viewport.width]);\n}\n"]}
@@ -0,0 +1,413 @@
1
+ import { useDisplayScaleCompensation, useDisplayScaleAnchor } from './chunk-R5CTLLUS.js';
2
+ import { Menu, Popover, Popper, Select } from '@mui/material';
3
+ import { useTheme } from '@mui/material/styles';
4
+ import { useCallback, useLayoutEffect, useRef, useMemo, useId, useState } from 'react';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ // src/createMuiDisplayScaleComponents.ts
8
+ function createMuiDisplayScaleComponents({
9
+ getPortalContainer,
10
+ overlayZoom = 1
11
+ }) {
12
+ const zoom = overlayZoom > 0 ? overlayZoom : 1;
13
+ const resolveContainerZoom = (container) => {
14
+ if (typeof document === "undefined") return zoom;
15
+ const resolvedContainer = typeof container === "function" ? container() : container;
16
+ return resolvedContainer == null || resolvedContainer === document.body ? zoom : 1;
17
+ };
18
+ return {
19
+ MuiModal: {
20
+ defaultProps: {
21
+ container: getPortalContainer
22
+ }
23
+ },
24
+ MuiDialog: {
25
+ defaultProps: {
26
+ container: getPortalContainer
27
+ },
28
+ styleOverrides: {
29
+ paper: ({ ownerState }) => ({ zoom: resolveContainerZoom(ownerState?.container) })
30
+ }
31
+ },
32
+ MuiPopover: {
33
+ defaultProps: {
34
+ container: getPortalContainer
35
+ },
36
+ styleOverrides: {
37
+ paper: ({ ownerState }) => ({ zoom: resolveContainerZoom(ownerState?.container) })
38
+ }
39
+ },
40
+ MuiPopper: {
41
+ defaultProps: {
42
+ container: getPortalContainer
43
+ },
44
+ styleOverrides: {
45
+ root: ({ ownerState }) => ({ zoom: resolveContainerZoom(ownerState?.container) })
46
+ }
47
+ },
48
+ MuiDrawer: {
49
+ defaultProps: {
50
+ // Drawer inherits Modal props. Supplying the portal host here (rather
51
+ // than only through MuiModal) also covers Drawer internals directly.
52
+ container: getPortalContainer
53
+ },
54
+ styleOverrides: {
55
+ paper: ({ ownerState }) => {
56
+ if (ownerState.variant !== "temporary") return {};
57
+ const isVertical = ownerState.anchor === "left" || ownerState.anchor === "right";
58
+ return isVertical ? {
59
+ zoom,
60
+ height: "var(--ep-display-scale-overlay-viewport-height, 100dvh)",
61
+ maxHeight: "var(--ep-display-scale-overlay-viewport-height, 100dvh)"
62
+ } : {
63
+ zoom,
64
+ width: "var(--ep-display-scale-overlay-viewport-width, 100dvw)",
65
+ maxWidth: "var(--ep-display-scale-overlay-viewport-width, 100dvw)"
66
+ };
67
+ }
68
+ }
69
+ }
70
+ };
71
+ }
72
+ function resolveOffset(origin, size) {
73
+ if (typeof origin === "number") return origin;
74
+ if (origin === "center") return size / 2;
75
+ if (origin === "bottom" || origin === "right") return size;
76
+ return 0;
77
+ }
78
+ function resolveOrigins(anchorOrigin, transformOrigin, anchorWidth, anchorHeight, paperWidth, paperHeight) {
79
+ const resolvedAnchorOrigin = anchorOrigin ?? { vertical: "top", horizontal: "left" };
80
+ const resolvedTransformOrigin = transformOrigin ?? { vertical: "top", horizontal: "left" };
81
+ return {
82
+ anchorX: resolveOffset(resolvedAnchorOrigin.horizontal, anchorWidth),
83
+ anchorY: resolveOffset(resolvedAnchorOrigin.vertical, anchorHeight),
84
+ transformX: resolveOffset(resolvedTransformOrigin.horizontal, paperWidth),
85
+ transformY: resolveOffset(resolvedTransformOrigin.vertical, paperHeight)
86
+ };
87
+ }
88
+ function useDisplayScalePopoverPosition({
89
+ open,
90
+ overlayRootRef,
91
+ anchorElement,
92
+ anchorReference,
93
+ anchorPosition,
94
+ anchorOrigin,
95
+ transformOrigin,
96
+ marginThreshold = 16
97
+ }) {
98
+ const { compensation, layoutExtents, rootRef } = useDisplayScaleCompensation();
99
+ const correctPosition = useCallback(() => {
100
+ if (!open || !compensation.active || !layoutExtents) return;
101
+ const overlayRoot = overlayRootRef.current;
102
+ const paper = overlayRoot?.querySelector(".MuiPopover-paper");
103
+ if (!paper) return;
104
+ const zoom = compensation.zoom > 0 ? compensation.zoom : 1;
105
+ const rootRect = rootRef.current?.getBoundingClientRect();
106
+ const rootLeft = rootRect?.left ?? 0;
107
+ const rootTop = rootRect?.top ?? 0;
108
+ const anchorRect = anchorElement?.getBoundingClientRect();
109
+ const anchorX = anchorReference === "anchorPosition" && anchorPosition ? (anchorPosition.left - rootLeft) / zoom : anchorRect ? (anchorRect.left - rootLeft) / zoom : null;
110
+ const anchorY = anchorReference === "anchorPosition" && anchorPosition ? (anchorPosition.top - rootTop) / zoom : anchorRect ? (anchorRect.top - rootTop) / zoom : null;
111
+ if (anchorX == null || anchorY == null) return;
112
+ const paperWidth = paper.offsetWidth;
113
+ const paperHeight = paper.offsetHeight;
114
+ const {
115
+ anchorX: originX,
116
+ anchorY: originY,
117
+ transformX,
118
+ transformY
119
+ } = resolveOrigins(
120
+ anchorOrigin,
121
+ transformOrigin,
122
+ anchorRect ? anchorRect.width / zoom : 0,
123
+ anchorRect ? anchorRect.height / zoom : 0,
124
+ paperWidth,
125
+ paperHeight
126
+ );
127
+ const margin = typeof marginThreshold === "number" ? marginThreshold : 16;
128
+ const maxLeft = Math.max(margin, layoutExtents.layoutWidthPx - margin - paperWidth);
129
+ const maxTop = Math.max(margin, layoutExtents.layoutHeightPx - margin - paperHeight);
130
+ const left = Math.min(maxLeft, Math.max(margin, anchorX + originX - transformX));
131
+ const top = Math.min(maxTop, Math.max(margin, anchorY + originY - transformY));
132
+ paper.style.left = `${Math.round(left)}px`;
133
+ paper.style.top = `${Math.round(top)}px`;
134
+ }, [
135
+ anchorElement,
136
+ anchorOrigin,
137
+ anchorPosition,
138
+ anchorReference,
139
+ compensation.active,
140
+ compensation.zoom,
141
+ layoutExtents,
142
+ marginThreshold,
143
+ open,
144
+ overlayRootRef,
145
+ rootRef,
146
+ transformOrigin
147
+ ]);
148
+ useLayoutEffect(() => {
149
+ if (!open) return;
150
+ const firstFrame = window.requestAnimationFrame(() => {
151
+ correctPosition();
152
+ window.requestAnimationFrame(correctPosition);
153
+ });
154
+ window.addEventListener("resize", correctPosition);
155
+ window.addEventListener("scroll", correctPosition, true);
156
+ return () => {
157
+ window.cancelAnimationFrame(firstFrame);
158
+ window.removeEventListener("resize", correctPosition);
159
+ window.removeEventListener("scroll", correctPosition, true);
160
+ };
161
+ }, [correctPosition, open]);
162
+ }
163
+ function DisplayScaleMenu({ anchorEl, ...props }) {
164
+ const displayScaleAnchor = useDisplayScaleAnchor(anchorEl ?? null);
165
+ const { compensation, rootRef } = useDisplayScaleCompensation();
166
+ const theme = useTheme();
167
+ const overlayRootRef = useRef(null);
168
+ const horizontalOrigin = theme.direction === "rtl" ? "right" : "left";
169
+ const anchorOrigin = props.anchorOrigin ?? {
170
+ vertical: "bottom",
171
+ horizontal: horizontalOrigin
172
+ };
173
+ const transformOrigin = props.transformOrigin ?? {
174
+ vertical: "top",
175
+ horizontal: horizontalOrigin
176
+ };
177
+ const anchorPosition = useMemo(() => {
178
+ if (props.anchorReference !== "anchorPosition" || !props.anchorPosition) {
179
+ return props.anchorPosition;
180
+ }
181
+ const zoom = compensation.zoom > 0 ? compensation.zoom : 1;
182
+ return {
183
+ top: props.anchorPosition.top / zoom,
184
+ left: props.anchorPosition.left / zoom
185
+ };
186
+ }, [compensation.zoom, props.anchorPosition, props.anchorReference]);
187
+ useDisplayScalePopoverPosition({
188
+ open: props.open,
189
+ overlayRootRef,
190
+ anchorElement: anchorEl ?? null,
191
+ anchorReference: props.anchorReference,
192
+ anchorPosition: props.anchorPosition,
193
+ anchorOrigin,
194
+ transformOrigin,
195
+ marginThreshold: props.marginThreshold
196
+ });
197
+ return /* @__PURE__ */ jsx(
198
+ Menu,
199
+ {
200
+ ...props,
201
+ ref: overlayRootRef,
202
+ anchorEl: displayScaleAnchor,
203
+ anchorPosition,
204
+ anchorOrigin,
205
+ container: rootRef.current ?? document.body,
206
+ transformOrigin
207
+ }
208
+ );
209
+ }
210
+ function DisplayScalePopover({
211
+ anchorEl,
212
+ ...props
213
+ }) {
214
+ const displayScaleAnchor = useDisplayScaleAnchor(anchorEl ?? null);
215
+ const { rootRef } = useDisplayScaleCompensation();
216
+ const overlayRootRef = useRef(null);
217
+ useDisplayScalePopoverPosition({
218
+ open: props.open,
219
+ overlayRootRef,
220
+ anchorElement: anchorEl ?? null,
221
+ anchorReference: props.anchorReference,
222
+ anchorPosition: props.anchorPosition,
223
+ anchorOrigin: props.anchorOrigin,
224
+ transformOrigin: props.transformOrigin,
225
+ marginThreshold: props.marginThreshold
226
+ });
227
+ return /* @__PURE__ */ jsx(
228
+ Popover,
229
+ {
230
+ ...props,
231
+ ref: overlayRootRef,
232
+ anchorEl: displayScaleAnchor,
233
+ container: rootRef.current ?? document.body
234
+ }
235
+ );
236
+ }
237
+ var defaultDisplayScalePopperModifiers = [
238
+ {
239
+ name: "offset",
240
+ options: { offset: [0, 8] }
241
+ }
242
+ ];
243
+ function scalePopperOffsets(modifiers, zoom) {
244
+ if (!modifiers || zoom === 1) return modifiers;
245
+ return modifiers.map((modifier) => {
246
+ if (modifier.name !== "offset") return modifier;
247
+ const options = modifier.options;
248
+ const rawOffset = options?.offset;
249
+ if (!rawOffset) return modifier;
250
+ const scaleValue = (value) => value == null ? value : value / zoom;
251
+ const offset = typeof rawOffset === "function" ? (args) => {
252
+ const [skidding, distance] = rawOffset(args);
253
+ return [scaleValue(skidding), scaleValue(distance)];
254
+ } : [scaleValue(rawOffset[0]), scaleValue(rawOffset[1])];
255
+ return { ...modifier, options: { ...options, offset } };
256
+ });
257
+ }
258
+ function DisplayScalePopper({ anchorEl, ...props }) {
259
+ const displayScaleAnchor = useDisplayScaleAnchor(anchorEl ?? null);
260
+ const { compensation, rootRef } = useDisplayScaleCompensation();
261
+ const modifiers = useMemo(
262
+ () => scalePopperOffsets(props.modifiers ?? defaultDisplayScalePopperModifiers, compensation.zoom),
263
+ [compensation.zoom, props.modifiers]
264
+ );
265
+ return /* @__PURE__ */ jsx(
266
+ Popper,
267
+ {
268
+ ...props,
269
+ anchorEl: displayScaleAnchor,
270
+ container: rootRef.current ?? document.body,
271
+ modifiers,
272
+ style: { ...props.style, zoom: 1 }
273
+ }
274
+ );
275
+ }
276
+ function DisplayScaleSelect(props) {
277
+ const {
278
+ MenuProps,
279
+ SelectDisplayProps,
280
+ defaultOpen,
281
+ onClose,
282
+ onOpen,
283
+ open: openProp,
284
+ ...selectProps
285
+ } = props;
286
+ const { rootRef } = useDisplayScaleCompensation();
287
+ const menuRootRef = useRef(null);
288
+ const selectId = useId();
289
+ const [anchorElement, setAnchorElement] = useState(null);
290
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(Boolean(defaultOpen));
291
+ const open = openProp ?? uncontrolledOpen;
292
+ useLayoutEffect(() => {
293
+ if (!open) {
294
+ setAnchorElement(null);
295
+ return;
296
+ }
297
+ setAnchorElement(
298
+ document.querySelector(`[data-ep-display-scale-select="${selectId}"]`)
299
+ );
300
+ }, [open, selectId]);
301
+ const handleOpen = useCallback(
302
+ (event) => {
303
+ if (openProp === void 0) setUncontrolledOpen(true);
304
+ onOpen?.(event);
305
+ },
306
+ [onOpen, openProp]
307
+ );
308
+ const handleClose = useCallback(
309
+ (event) => {
310
+ if (openProp === void 0) setUncontrolledOpen(false);
311
+ onClose?.(event);
312
+ },
313
+ [onClose, openProp]
314
+ );
315
+ useDisplayScalePopoverPosition({
316
+ open,
317
+ overlayRootRef: menuRootRef,
318
+ anchorElement,
319
+ anchorOrigin: { vertical: "bottom", horizontal: "center" },
320
+ transformOrigin: { vertical: "top", horizontal: "center" }
321
+ });
322
+ const menuProps = useMemo(() => {
323
+ const paperSlot = MenuProps?.slotProps?.paper;
324
+ const resolvedPaperSlot = typeof paperSlot === "function" ? void 0 : paperSlot;
325
+ return {
326
+ ...MenuProps,
327
+ container: rootRef.current ?? document.body,
328
+ ref: menuRootRef,
329
+ slotProps: {
330
+ ...MenuProps?.slotProps,
331
+ paper: {
332
+ ...resolvedPaperSlot,
333
+ style: { ...resolvedPaperSlot?.style, zoom: 1 }
334
+ }
335
+ }
336
+ };
337
+ }, [MenuProps, rootRef]);
338
+ return /* @__PURE__ */ jsx(
339
+ Select,
340
+ {
341
+ ...selectProps,
342
+ open,
343
+ onOpen: handleOpen,
344
+ onClose: handleClose,
345
+ MenuProps: menuProps,
346
+ SelectDisplayProps: {
347
+ ...SelectDisplayProps,
348
+ "data-ep-display-scale-select": selectId
349
+ }
350
+ }
351
+ );
352
+ }
353
+ function useDisplayScaleTabsIndicator(tabsRef, value) {
354
+ const { compensation } = useDisplayScaleCompensation();
355
+ useLayoutEffect(() => {
356
+ if (!compensation.active) return;
357
+ const updateIndicator = () => {
358
+ const tabs = tabsRef.current;
359
+ const indicator = tabs?.querySelector(".MuiTabs-indicator");
360
+ const selectedTab = tabs?.querySelector(".MuiTab-root.Mui-selected");
361
+ if (!tabs || !indicator || !selectedTab) return;
362
+ const isRtl = getComputedStyle(tabs).direction === "rtl";
363
+ const left = selectedTab.offsetLeft;
364
+ const width = selectedTab.offsetWidth;
365
+ indicator.style.width = `${width}px`;
366
+ if (isRtl) {
367
+ indicator.style.left = "auto";
368
+ indicator.style.right = `${tabs.offsetWidth - left - width}px`;
369
+ } else {
370
+ indicator.style.right = "auto";
371
+ indicator.style.left = `${left}px`;
372
+ }
373
+ };
374
+ const frame = window.requestAnimationFrame(updateIndicator);
375
+ const observer = new ResizeObserver(updateIndicator);
376
+ if (tabsRef.current) observer.observe(tabsRef.current);
377
+ window.addEventListener("resize", updateIndicator);
378
+ return () => {
379
+ window.cancelAnimationFrame(frame);
380
+ observer.disconnect();
381
+ window.removeEventListener("resize", updateIndicator);
382
+ };
383
+ }, [compensation.active, tabsRef, value]);
384
+ }
385
+
386
+ // src/displayScaleLayoutTokens.ts
387
+ var displayScalePageFillSx = {
388
+ flex: 1,
389
+ minHeight: 0,
390
+ minWidth: 0,
391
+ width: "100%",
392
+ height: "100%"
393
+ };
394
+ var signinViewportShellSx = {
395
+ flex: 1,
396
+ minHeight: 0,
397
+ minWidth: 0,
398
+ width: "100%",
399
+ height: "100%",
400
+ overflow: "hidden",
401
+ boxSizing: "border-box"
402
+ };
403
+ var signinSplitColumnSx = {
404
+ flex: { xs: "0 0 auto", md: "1 1 0" },
405
+ width: { xs: "100%", md: "50%" },
406
+ maxWidth: { xs: "100%", md: "50%" },
407
+ minWidth: 0,
408
+ boxSizing: "border-box"
409
+ };
410
+
411
+ export { DisplayScaleMenu, DisplayScalePopover, DisplayScalePopper, DisplayScaleSelect, createMuiDisplayScaleComponents, displayScalePageFillSx, signinSplitColumnSx, signinViewportShellSx, useDisplayScalePopoverPosition, useDisplayScaleTabsIndicator };
412
+ //# sourceMappingURL=chunk-AGM7V7PR.js.map
413
+ //# sourceMappingURL=chunk-AGM7V7PR.js.map