Private
Public Access
1
0

feat: Fluent UI Outlook Lite + connections mockup

This commit is contained in:
2026-04-14 18:52:25 +00:00
parent 1199eff6c3
commit dfa4010406
34820 changed files with 1003813 additions and 205 deletions

View File

@@ -0,0 +1,157 @@
'use client';
import { mergeClasses } from '@griffel/react';
import { useId } from '@fluentui/react-utilities';
import * as React from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { getRectCorners } from './getRectCorners';
import { getMouseAnchor } from './getMouseAnchor';
import { pointsToSvgPath } from './pointsToSvgPath';
import { useStyles } from './SafeZoneArea.styles';
import { computeOutsideClipPath } from './computeOutsideClipPath';
// ---
const EMPTY_RECT = {
top: 0,
right: 0,
bottom: 0,
left: 0,
width: 0,
height: 0,
x: 0,
y: 0,
toJSON () {
return '';
}
};
export function isSameRect(a, b) {
return a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left && a.width === b.width && a.height === b.height;
}
export function isSameCoordinates(a, b) {
return a[0] === b[0] && a[1] === b[1];
}
// ---
/**
* A component that renders a safe zone area with SVG shapes. Uses `useSyncExternalStore` to manage its active state
* to avoid causing re-renders in `useSafeZoneArea()` as the hook might be used in host components like `Menu`.
*
* Draws two shapes:
* - a triangle that points to the target element which is an actual safe zone
* - a rectangle for a clip path that clips out the target element
*
* @internal
*/ export const SafeZoneArea = /*#__PURE__*/ React.memo((props)=>{
const { debug, onMouseEnter, onMouseMove, onMouseLeave, stateStore } = props;
const clipPathId = useId();
const styles = useStyles();
const active = useSyncExternalStore(stateStore.subscribe, stateStore.isActive);
const svgRef = React.useRef(null);
const [state, setState] = React.useState(()=>({
containerRect: EMPTY_RECT,
targetRect: EMPTY_RECT,
mouseCoordinates: [
0,
0
]
}));
React.useImperativeHandle(props.imperativeRef, ()=>({
updateSVG (newState) {
setState((prevState)=>{
// Heads up!
// A small optimization to avoid unnecessary re-renders
if (isSameRect(prevState.containerRect, newState.containerRect) && isSameRect(prevState.targetRect, newState.targetRect) && isSameCoordinates(prevState.mouseCoordinates, newState.mouseCoordinates)) {
return prevState;
}
return newState;
});
}
}), []);
const { containerRect, targetRect, mouseCoordinates } = state;
const topOffset = Math.min(targetRect.top, containerRect.top);
const leftOffset = Math.min(targetRect.left, containerRect.left);
const bottomOffset = Math.max(targetRect.bottom, containerRect.bottom);
const rightOffset = Math.max(targetRect.right, containerRect.right);
// ---
const containerCorners = getRectCorners(containerRect, [
leftOffset,
topOffset
]);
const targetCorners = getRectCorners(targetRect, [
leftOffset,
topOffset
]);
// Heads up!
// The SVG coordinate system starts at the top-left corner of the SVG element,
// so we need to adjust the mouse coordinates relative to the SVG's top-left corner.
const relativeMouseCoordinates = [
mouseCoordinates[0] - leftOffset,
mouseCoordinates[1] - topOffset
];
const mouseAnchor = getMouseAnchor(containerCorners.topLeft, containerCorners.bottomRight, relativeMouseCoordinates);
const triangleA = [
mouseAnchor,
containerCorners.topLeft,
containerCorners.topRight
];
const triangleB = [
mouseAnchor,
containerCorners.topRight,
containerCorners.bottomRight
];
const triangleC = [
mouseAnchor,
containerCorners.bottomRight,
containerCorners.bottomLeft
];
const triangleD = [
mouseAnchor,
containerCorners.bottomLeft,
containerCorners.topLeft
];
const svgWidth = rightOffset - leftOffset;
const svgHeight = bottomOffset - topOffset;
const clipPath = computeOutsideClipPath(svgWidth, svgHeight, {
x: targetCorners.topLeft[0],
y: targetCorners.topLeft[1],
width: targetRect.width,
height: targetRect.height
}, {
x: containerCorners.topLeft[0],
y: containerCorners.topLeft[1],
width: containerRect.width,
height: containerRect.height
});
return /*#__PURE__*/ React.createElement("div", {
className: mergeClasses(styles.wrapper, active && styles.wrapperActive),
"data-safe-zone": ""
}, active ? /*#__PURE__*/ React.createElement("svg", {
"aria-hidden": true,
className: styles.svg,
xmlns: "http://www.w3.org/2000/svg",
ref: svgRef,
style: {
width: `${svgWidth}px`,
height: `${svgHeight}px`,
transform: `translate(${leftOffset}px, ${topOffset}px)`
}
}, /*#__PURE__*/ React.createElement("g", {
className: mergeClasses(styles.triangle, debug && styles.triangleDebug),
clipPath: `url(#${clipPathId})`,
onMouseEnter: onMouseEnter,
onMouseMove: onMouseMove,
onMouseLeave: onMouseLeave
}, /*#__PURE__*/ React.createElement("path", {
d: pointsToSvgPath(triangleA)
}), /*#__PURE__*/ React.createElement("path", {
d: pointsToSvgPath(triangleB)
}), /*#__PURE__*/ React.createElement("path", {
d: pointsToSvgPath(triangleC)
}), /*#__PURE__*/ React.createElement("path", {
d: pointsToSvgPath(triangleD)
})), /*#__PURE__*/ React.createElement("clipPath", {
id: clipPathId
}, /*#__PURE__*/ React.createElement("path", {
d: clipPath
})), debug && /*#__PURE__*/ React.createElement("path", {
className: styles.rectDebug,
d: clipPath
})) : null);
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
'use client';
import { __styles } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
export const useStyles = /*#__PURE__*/__styles({
wrapper: {
mc9l5x: "fjseox",
Bqenvij: "fniina8",
a9b677: "f3tsq5r",
Bkecrkj: "f1aehjj5"
},
wrapperActive: {
mc9l5x: "ftgm304"
},
svg: {
Bkfmm31: "f1au8mb3",
Bkecrkj: "f1aehjj5",
qhf8xq: "f19dog8a",
Bhzewxz: "f15twtuk",
oyh7mz: ["f1vgc2s3", "f1e31b4d"]
},
triangle: {
Bkecrkj: "f1cguypg"
},
triangleDebug: {
Bceei9c: "f7116n6",
Bkfmm31: "f1xab38x"
},
rectDebug: {
Bkfmm31: "fyegryc"
}
}, {
d: [".fjseox{display:none;}", ".fniina8{height:0;}", ".f3tsq5r{width:0;}", ".f1aehjj5{pointer-events:none;}", ".ftgm304{display:block;}", ".f1au8mb3{fill:transparent;}", ".f19dog8a{position:fixed;}", ".f15twtuk{top:0;}", ".f1vgc2s3{left:0;}", ".f1e31b4d{right:0;}", ".f1cguypg{pointer-events:auto;}", ".f7116n6{cursor:crosshair;}", ".f1xab38x{fill:color-mix(in srgb, var(--colorPaletteGreenBackground3) 20%, transparent);}", ".fyegryc{fill:color-mix(in srgb, var(--colorPaletteRedBackground3) 20%, transparent);}"]
});

View File

@@ -0,0 +1 @@
{"version":3,"names":["__styles","tokens","useStyles","wrapper","mc9l5x","Bqenvij","a9b677","Bkecrkj","wrapperActive","svg","Bkfmm31","qhf8xq","Bhzewxz","oyh7mz","triangle","triangleDebug","Bceei9c","rectDebug","d"],"sources":["SafeZoneArea.styles.js"],"sourcesContent":["'use client';\nimport { makeStyles } from '@griffel/react';\nimport { tokens } from '@fluentui/react-theme';\nexport const useStyles = makeStyles({\n wrapper: {\n display: 'none',\n height: 0,\n width: 0,\n pointerEvents: 'none'\n },\n wrapperActive: {\n display: 'block'\n },\n svg: {\n fill: 'transparent',\n pointerEvents: 'none',\n position: 'fixed',\n top: 0,\n left: 0\n },\n triangle: {\n pointerEvents: 'auto'\n },\n triangleDebug: {\n cursor: 'crosshair',\n fill: `color-mix(in srgb, ${tokens.colorPaletteGreenBackground3} 20%, transparent)`\n },\n rectDebug: {\n fill: `color-mix(in srgb, ${tokens.colorPaletteRedBackground3} 20%, transparent)`\n }\n});\n"],"mappings":"AAAA,YAAY;;AACZ,SAAAA,QAAA,QAA2B,gBAAgB;AAC3C,SAASC,MAAM,QAAQ,uBAAuB;AAC9C,OAAO,MAAMC,SAAS,gBAAGF,QAAA;EAAAG,OAAA;IAAAC,MAAA;IAAAC,OAAA;IAAAC,MAAA;IAAAC,OAAA;EAAA;EAAAC,aAAA;IAAAJ,MAAA;EAAA;EAAAK,GAAA;IAAAC,OAAA;IAAAH,OAAA;IAAAI,MAAA;IAAAC,OAAA;IAAAC,MAAA;EAAA;EAAAC,QAAA;IAAAP,OAAA;EAAA;EAAAQ,aAAA;IAAAC,OAAA;IAAAN,OAAA;EAAA;EAAAO,SAAA;IAAAP,OAAA;EAAA;AAAA;EAAAQ,CAAA;AAAA,CA2BxB,CAAC","ignoreList":[]}

View File

@@ -0,0 +1,31 @@
'use client';
import { makeStyles } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
export const useStyles = makeStyles({
wrapper: {
display: 'none',
height: 0,
width: 0,
pointerEvents: 'none'
},
wrapperActive: {
display: 'block'
},
svg: {
fill: 'transparent',
pointerEvents: 'none',
position: 'fixed',
top: 0,
left: 0
},
triangle: {
pointerEvents: 'auto'
},
triangleDebug: {
cursor: 'crosshair',
fill: `color-mix(in srgb, ${tokens.colorPaletteGreenBackground3} 20%, transparent)`
},
rectDebug: {
fill: `color-mix(in srgb, ${tokens.colorPaletteRedBackground3} 20%, transparent)`
}
});

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useSafeZoneArea/SafeZoneArea.styles.ts"],"sourcesContent":["'use client';\n\nimport { makeStyles } from '@griffel/react';\nimport { tokens } from '@fluentui/react-theme';\n\nexport const useStyles = makeStyles({\n wrapper: {\n display: 'none',\n height: 0,\n width: 0,\n pointerEvents: 'none',\n },\n wrapperActive: {\n display: 'block',\n },\n svg: {\n fill: 'transparent',\n pointerEvents: 'none',\n position: 'fixed',\n top: 0,\n left: 0,\n },\n triangle: {\n pointerEvents: 'auto',\n },\n triangleDebug: {\n cursor: 'crosshair',\n fill: `color-mix(in srgb, ${tokens.colorPaletteGreenBackground3} 20%, transparent)`,\n },\n rectDebug: {\n fill: `color-mix(in srgb, ${tokens.colorPaletteRedBackground3} 20%, transparent)`,\n },\n});\n"],"names":["makeStyles","tokens","useStyles","wrapper","display","height","width","pointerEvents","wrapperActive","svg","fill","position","top","left","triangle","triangleDebug","cursor","colorPaletteGreenBackground3","rectDebug","colorPaletteRedBackground3"],"mappings":"AAAA;AAEA,SAASA,UAAU,QAAQ,iBAAiB;AAC5C,SAASC,MAAM,QAAQ,wBAAwB;AAE/C,OAAO,MAAMC,YAAYF,WAAW;IAClCG,SAAS;QACPC,SAAS;QACTC,QAAQ;QACRC,OAAO;QACPC,eAAe;IACjB;IACAC,eAAe;QACbJ,SAAS;IACX;IACAK,KAAK;QACHC,MAAM;QACNH,eAAe;QACfI,UAAU;QACVC,KAAK;QACLC,MAAM;IACR;IACAC,UAAU;QACRP,eAAe;IACjB;IACAQ,eAAe;QACbC,QAAQ;QACRN,MAAM,CAAC,mBAAmB,EAAET,OAAOgB,4BAA4B,CAAC,kBAAkB,CAAC;IACrF;IACAC,WAAW;QACTR,MAAM,CAAC,mBAAmB,EAAET,OAAOkB,0BAA0B,CAAC,kBAAkB,CAAC;IACnF;AACF,GAAG"}

View File

@@ -0,0 +1,27 @@
function drawRectangle(rect) {
if (rect.width <= 0 || rect.height <= 0) {
return '';
}
let pathData = '';
// Creates a subpath moving in counterclockwise direction to create a hole
pathData += `M ${rect.x},${rect.y} `;
pathData += `V ${rect.y + rect.height} `; // Down to bottom-left
pathData += `H ${rect.x + rect.width} `; // Right to bottom-right
pathData += `V ${rect.y} `; // Up to top-right
pathData += `H ${rect.x} `; // Left to top-left (closing)
pathData += `Z `; // Close path
return pathData;
}
/**
* Computes a clip path that covers the area outside multiple rectangles.
*
* @internal
*/ export function computeOutsideClipPath(svgWidth, svgHeight, targetRect, containerRect) {
let pathData = `M 0,0 H ${svgWidth} V ${svgHeight} H 0 Z `;
// For each rectangle, add a subpath that "cuts out" the rectangle
// The trick is to draw each rectangle in the counterclockwise direction
// which creates a "hole" in the main path
pathData += drawRectangle(targetRect);
pathData += drawRectangle(containerRect);
return pathData;
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useSafeZoneArea/computeOutsideClipPath.ts"],"sourcesContent":["function drawRectangle(rect: { x: number; y: number; width: number; height: number }): string {\n if (rect.width <= 0 || rect.height <= 0) {\n return '';\n }\n\n let pathData = '';\n\n // Creates a subpath moving in counterclockwise direction to create a hole\n\n pathData += `M ${rect.x},${rect.y} `;\n pathData += `V ${rect.y + rect.height} `; // Down to bottom-left\n pathData += `H ${rect.x + rect.width} `; // Right to bottom-right\n pathData += `V ${rect.y} `; // Up to top-right\n pathData += `H ${rect.x} `; // Left to top-left (closing)\n pathData += `Z `; // Close path\n\n return pathData;\n}\n\n/**\n * Computes a clip path that covers the area outside multiple rectangles.\n *\n * @internal\n */\nexport function computeOutsideClipPath(\n svgWidth: number,\n svgHeight: number,\n targetRect: { x: number; y: number; width: number; height: number },\n containerRect: { x: number; y: number; width: number; height: number },\n): string {\n let pathData = `M 0,0 H ${svgWidth} V ${svgHeight} H 0 Z `;\n\n // For each rectangle, add a subpath that \"cuts out\" the rectangle\n // The trick is to draw each rectangle in the counterclockwise direction\n // which creates a \"hole\" in the main path\n\n pathData += drawRectangle(targetRect);\n pathData += drawRectangle(containerRect);\n\n return pathData;\n}\n"],"names":["drawRectangle","rect","width","height","pathData","x","y","computeOutsideClipPath","svgWidth","svgHeight","targetRect","containerRect"],"mappings":"AAAA,SAASA,cAAcC,IAA6D;IAClF,IAAIA,KAAKC,KAAK,IAAI,KAAKD,KAAKE,MAAM,IAAI,GAAG;QACvC,OAAO;IACT;IAEA,IAAIC,WAAW;IAEf,0EAA0E;IAE1EA,YAAY,CAAC,EAAE,EAAEH,KAAKI,CAAC,CAAC,CAAC,EAAEJ,KAAKK,CAAC,CAAC,CAAC,CAAC;IACpCF,YAAY,CAAC,EAAE,EAAEH,KAAKK,CAAC,GAAGL,KAAKE,MAAM,CAAC,CAAC,CAAC,EAAE,sBAAsB;IAChEC,YAAY,CAAC,EAAE,EAAEH,KAAKI,CAAC,GAAGJ,KAAKC,KAAK,CAAC,CAAC,CAAC,EAAE,wBAAwB;IACjEE,YAAY,CAAC,EAAE,EAAEH,KAAKK,CAAC,CAAC,CAAC,CAAC,EAAE,kBAAkB;IAC9CF,YAAY,CAAC,EAAE,EAAEH,KAAKI,CAAC,CAAC,CAAC,CAAC,EAAE,6BAA6B;IACzDD,YAAY,CAAC,EAAE,CAAC,EAAE,aAAa;IAE/B,OAAOA;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASG,uBACdC,QAAgB,EAChBC,SAAiB,EACjBC,UAAmE,EACnEC,aAAsE;IAEtE,IAAIP,WAAW,CAAC,QAAQ,EAAEI,SAAS,GAAG,EAAEC,UAAU,OAAO,CAAC;IAE1D,kEAAkE;IAClE,wEAAwE;IACxE,0CAA0C;IAE1CL,YAAYJ,cAAcU;IAC1BN,YAAYJ,cAAcW;IAE1B,OAAOP;AACT"}

View File

@@ -0,0 +1,25 @@
export function createSafeZoneAreaStateStore() {
let isActive = false;
const listeners = [];
return {
isActive () {
return isActive;
},
toggleActive (newIsActive) {
if (isActive === newIsActive) {
return;
}
isActive = newIsActive;
listeners.forEach((listener)=>listener(isActive));
},
subscribe (listener) {
listeners.push(listener);
return ()=>{
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
};
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useSafeZoneArea/createSafeZoneAreaStateStore.ts"],"sourcesContent":["export function createSafeZoneAreaStateStore(): {\n isActive: () => boolean;\n toggleActive: (newIsActive: boolean) => void;\n subscribe: (listener: (value: boolean) => void) => () => void;\n} {\n let isActive = false;\n const listeners: ((value: boolean) => void)[] = [];\n\n return {\n isActive() {\n return isActive;\n },\n toggleActive(newIsActive: boolean) {\n if (isActive === newIsActive) {\n return;\n }\n\n isActive = newIsActive;\n listeners.forEach(listener => listener(isActive));\n },\n\n subscribe(listener: (value: boolean) => void) {\n listeners.push(listener);\n\n return () => {\n const index = listeners.indexOf(listener);\n\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n },\n };\n}\n"],"names":["createSafeZoneAreaStateStore","isActive","listeners","toggleActive","newIsActive","forEach","listener","subscribe","push","index","indexOf","splice"],"mappings":"AAAA,OAAO,SAASA;IAKd,IAAIC,WAAW;IACf,MAAMC,YAA0C,EAAE;IAElD,OAAO;QACLD;YACE,OAAOA;QACT;QACAE,cAAaC,WAAoB;YAC/B,IAAIH,aAAaG,aAAa;gBAC5B;YACF;YAEAH,WAAWG;YACXF,UAAUG,OAAO,CAACC,CAAAA,WAAYA,SAASL;QACzC;QAEAM,WAAUD,QAAkC;YAC1CJ,UAAUM,IAAI,CAACF;YAEf,OAAO;gBACL,MAAMG,QAAQP,UAAUQ,OAAO,CAACJ;gBAEhC,IAAIG,QAAQ,CAAC,GAAG;oBACdP,UAAUS,MAAM,CAACF,OAAO;gBAC1B;YACF;QACF;IACF;AACF"}

View File

@@ -0,0 +1,51 @@
const OFFSET_DISTANCE = 20;
/**
* Measures the distance between two points in a 2D space.
*/ export function measureDistance(a, b) {
return Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2);
}
/**
* Returns a unit vector pointing from point `b` to point `a`.
* If the distance is zero, returns a zero vector.
*/ export function getUnitVector(a, b) {
const distance = measureDistance(a, b);
if (distance === 0) {
return [
0,
0
];
}
return [
(a[0] - b[0]) / distance,
(a[1] - b[1]) / distance
];
}
/**
* Calculates the anchor point for a mouse position within a container defined by its top-left and bottom-right corners.
* The anchor point is calculated as an offset from the center of the container in the direction of the mouse position.
*
* @internal
*/ export function getMouseAnchor(topLeftCorner, bottomRightCorner, mouseCoordinates) {
const containerCenter = [
(topLeftCorner[0] + bottomRightCorner[0]) / 2,
(topLeftCorner[1] + bottomRightCorner[1]) / 2
];
const unitVector = getUnitVector([
mouseCoordinates[0],
mouseCoordinates[1]
], [
containerCenter[0],
containerCenter[1]
]);
const distance = measureDistance([
containerCenter[0],
containerCenter[1]
], [
mouseCoordinates[0],
mouseCoordinates[1]
]);
return [
containerCenter[0] + unitVector[0] * (distance + OFFSET_DISTANCE),
containerCenter[1] + unitVector[1] * (distance + OFFSET_DISTANCE)
];
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useSafeZoneArea/getMouseAnchor.ts"],"sourcesContent":["import type { Point } from './types';\n\nconst OFFSET_DISTANCE = 20;\n\n/**\n * Measures the distance between two points in a 2D space.\n */\nexport function measureDistance(a: Point, b: Point): number {\n return Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2);\n}\n\n/**\n * Returns a unit vector pointing from point `b` to point `a`.\n * If the distance is zero, returns a zero vector.\n */\nexport function getUnitVector(a: Point, b: Point): Point {\n const distance = measureDistance(a, b);\n\n if (distance === 0) {\n return [0, 0];\n }\n\n return [(a[0] - b[0]) / distance, (a[1] - b[1]) / distance];\n}\n\n/**\n * Calculates the anchor point for a mouse position within a container defined by its top-left and bottom-right corners.\n * The anchor point is calculated as an offset from the center of the container in the direction of the mouse position.\n *\n * @internal\n */\nexport function getMouseAnchor(topLeftCorner: Point, bottomRightCorner: Point, mouseCoordinates: Point): Point {\n const containerCenter: Point = [\n (topLeftCorner[0] + bottomRightCorner[0]) / 2,\n (topLeftCorner[1] + bottomRightCorner[1]) / 2,\n ];\n\n const unitVector = getUnitVector(\n [mouseCoordinates[0], mouseCoordinates[1]],\n [containerCenter[0], containerCenter[1]],\n );\n const distance = measureDistance(\n [containerCenter[0], containerCenter[1]],\n [mouseCoordinates[0], mouseCoordinates[1]],\n );\n\n return [\n containerCenter[0] + unitVector[0] * (distance + OFFSET_DISTANCE),\n containerCenter[1] + unitVector[1] * (distance + OFFSET_DISTANCE),\n ];\n}\n"],"names":["OFFSET_DISTANCE","measureDistance","a","b","Math","sqrt","getUnitVector","distance","getMouseAnchor","topLeftCorner","bottomRightCorner","mouseCoordinates","containerCenter","unitVector"],"mappings":"AAEA,MAAMA,kBAAkB;AAExB;;CAEC,GACD,OAAO,SAASC,gBAAgBC,CAAQ,EAAEC,CAAQ;IAChD,OAAOC,KAAKC,IAAI,CAAC,AAACH,CAAAA,CAAC,CAAC,EAAE,GAAGC,CAAC,CAAC,EAAE,AAAD,KAAM,IAAI,AAACD,CAAAA,CAAC,CAAC,EAAE,GAAGC,CAAC,CAAC,EAAE,AAAD,KAAM;AACzD;AAEA;;;CAGC,GACD,OAAO,SAASG,cAAcJ,CAAQ,EAAEC,CAAQ;IAC9C,MAAMI,WAAWN,gBAAgBC,GAAGC;IAEpC,IAAII,aAAa,GAAG;QAClB,OAAO;YAAC;YAAG;SAAE;IACf;IAEA,OAAO;QAAEL,CAAAA,CAAC,CAAC,EAAE,GAAGC,CAAC,CAAC,EAAE,AAAD,IAAKI;QAAWL,CAAAA,CAAC,CAAC,EAAE,GAAGC,CAAC,CAAC,EAAE,AAAD,IAAKI;KAAS;AAC7D;AAEA;;;;;CAKC,GACD,OAAO,SAASC,eAAeC,aAAoB,EAAEC,iBAAwB,EAAEC,gBAAuB;IACpG,MAAMC,kBAAyB;QAC5BH,CAAAA,aAAa,CAAC,EAAE,GAAGC,iBAAiB,CAAC,EAAE,AAAD,IAAK;QAC3CD,CAAAA,aAAa,CAAC,EAAE,GAAGC,iBAAiB,CAAC,EAAE,AAAD,IAAK;KAC7C;IAED,MAAMG,aAAaP,cACjB;QAACK,gBAAgB,CAAC,EAAE;QAAEA,gBAAgB,CAAC,EAAE;KAAC,EAC1C;QAACC,eAAe,CAAC,EAAE;QAAEA,eAAe,CAAC,EAAE;KAAC;IAE1C,MAAML,WAAWN,gBACf;QAACW,eAAe,CAAC,EAAE;QAAEA,eAAe,CAAC,EAAE;KAAC,EACxC;QAACD,gBAAgB,CAAC,EAAE;QAAEA,gBAAgB,CAAC,EAAE;KAAC;IAG5C,OAAO;QACLC,eAAe,CAAC,EAAE,GAAGC,UAAU,CAAC,EAAE,GAAIN,CAAAA,WAAWP,eAAc;QAC/DY,eAAe,CAAC,EAAE,GAAGC,UAAU,CAAC,EAAE,GAAIN,CAAAA,WAAWP,eAAc;KAChE;AACH"}

View File

@@ -0,0 +1,24 @@
/**
* Calculates the corners of a rectangle based on its DOMRect and an offset.
*
* @internal
*/ export function getRectCorners(rect, offset) {
return {
topLeft: [
rect.left - offset[0],
rect.top - offset[1]
],
topRight: [
rect.right - offset[0],
rect.top - offset[1]
],
bottomRight: [
rect.right - offset[0],
rect.bottom - offset[1]
],
bottomLeft: [
rect.left - offset[0],
rect.bottom - offset[1]
]
};
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useSafeZoneArea/getRectCorners.ts"],"sourcesContent":["import type { Point } from './types';\n\n/**\n * Calculates the corners of a rectangle based on its DOMRect and an offset.\n *\n * @internal\n */\nexport function getRectCorners(\n rect: DOMRect,\n offset: Point,\n): Record<'topLeft' | 'topRight' | 'bottomRight' | 'bottomLeft', Point> {\n return {\n topLeft: [rect.left - offset[0], rect.top - offset[1]],\n topRight: [rect.right - offset[0], rect.top - offset[1]],\n bottomRight: [rect.right - offset[0], rect.bottom - offset[1]],\n bottomLeft: [rect.left - offset[0], rect.bottom - offset[1]],\n };\n}\n"],"names":["getRectCorners","rect","offset","topLeft","left","top","topRight","right","bottomRight","bottom","bottomLeft"],"mappings":"AAEA;;;;CAIC,GACD,OAAO,SAASA,eACdC,IAAa,EACbC,MAAa;IAEb,OAAO;QACLC,SAAS;YAACF,KAAKG,IAAI,GAAGF,MAAM,CAAC,EAAE;YAAED,KAAKI,GAAG,GAAGH,MAAM,CAAC,EAAE;SAAC;QACtDI,UAAU;YAACL,KAAKM,KAAK,GAAGL,MAAM,CAAC,EAAE;YAAED,KAAKI,GAAG,GAAGH,MAAM,CAAC,EAAE;SAAC;QACxDM,aAAa;YAACP,KAAKM,KAAK,GAAGL,MAAM,CAAC,EAAE;YAAED,KAAKQ,MAAM,GAAGP,MAAM,CAAC,EAAE;SAAC;QAC9DQ,YAAY;YAACT,KAAKG,IAAI,GAAGF,MAAM,CAAC,EAAE;YAAED,KAAKQ,MAAM,GAAGP,MAAM,CAAC,EAAE;SAAC;IAC9D;AACF"}

View File

@@ -0,0 +1,7 @@
/**
* Calculates the corners of a rectangle based on its DOMRect and an offset.
*
* @internal
*/ export function pointsToSvgPath(points) {
return `M ${points} z`;
}

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useSafeZoneArea/pointsToSvgPath.ts"],"sourcesContent":["import type { Point } from './types';\n\n/**\n * Calculates the corners of a rectangle based on its DOMRect and an offset.\n *\n * @internal\n */\nexport function pointsToSvgPath(points: Point[]): string {\n return `M ${points} z`;\n}\n"],"names":["pointsToSvgPath","points"],"mappings":"AAEA;;;;CAIC,GACD,OAAO,SAASA,gBAAgBC,MAAe;IAC7C,OAAO,CAAC,EAAE,EAAEA,OAAO,EAAE,CAAC;AACxB"}

View File

@@ -0,0 +1,5 @@
/**
* A type representing a point in a 2D space as an array of two numbers (x & y coordinates).
*
* @internal
*/ export { };

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../src/hooks/useSafeZoneArea/types.ts"],"sourcesContent":["/**\n * A type representing a point in a 2D space as an array of two numbers (x & y coordinates).\n *\n * @internal\n */\nexport type Point = [number, number];\n"],"names":[],"mappings":"AAAA;;;;CAIC,GACD,WAAqC"}

View File

@@ -0,0 +1,146 @@
'use client';
import { useAnimationFrame, useEventCallback, useMergedRefs, useTimeout } from '@fluentui/react-utilities';
import * as React from 'react';
import { createSafeZoneAreaStateStore } from './createSafeZoneAreaStateStore';
import { SafeZoneArea } from './SafeZoneArea';
/**
* Time in milliseconds after which the safe zone area will be cleared if no mouse movement is detected.
*
* Only affects the target element, not the safe zone area itself.
*/ const MOUSE_MOVE_TARGET_POLLING_TIMEOUT = 2000;
// ---
export function useSafeZoneArea({ debug = false, disabled = false, onSafeZoneEnter, onSafeZoneMove, onSafeZoneLeave, onSafeZoneTimeout, timeout = 1500 } = {}) {
const [stateStore] = React.useState(createSafeZoneAreaStateStore);
const safeZoneAreaRef = React.useRef(null);
const containerRef = React.useRef(null);
const targetRef = React.useRef(null);
const [setSafeZoneCloseTimeout, clearSafeZoneCloseTimeout] = useTimeout();
const [requestUpdateFrame, clearUpdateFrame] = useAnimationFrame();
const mouseCoordinatesRef = React.useRef({
x: 0,
y: 0
});
const containerListenerRef = React.useMemo(()=>{
if (disabled) {
return ()=>{
// do nothing
};
}
let containerEl = null;
function onContainerMouseEnter() {
clearSafeZoneCloseTimeout();
stateStore.toggleActive(false);
}
return (el)=>{
if (el === null) {
containerEl === null || containerEl === void 0 ? void 0 : containerEl.removeEventListener('mouseenter', onContainerMouseEnter);
}
containerEl = el;
el === null || el === void 0 ? void 0 : el.addEventListener('mouseenter', onContainerMouseEnter);
};
}, [
clearSafeZoneCloseTimeout,
disabled,
stateStore
]);
const targetListenerRef = React.useMemo(()=>{
if (disabled) {
return ()=>{
// do nothing
};
}
let targetEl = null;
function onTargetMouseMove(e) {
mouseCoordinatesRef.current = {
x: e.clientX,
y: e.clientY
};
if (!stateStore.isActive()) {
stateStore.toggleActive(true);
}
setSafeZoneCloseTimeout(()=>{
stateStore.toggleActive(false);
}, MOUSE_MOVE_TARGET_POLLING_TIMEOUT);
}
return (el)=>{
if (el === null) {
clearUpdateFrame();
clearSafeZoneCloseTimeout();
targetEl === null || targetEl === void 0 ? void 0 : targetEl.removeEventListener('mousemove', onTargetMouseMove);
}
targetEl = el;
el === null || el === void 0 ? void 0 : el.addEventListener('mousemove', onTargetMouseMove);
};
}, [
clearUpdateFrame,
clearSafeZoneCloseTimeout,
disabled,
stateStore,
setSafeZoneCloseTimeout
]);
const onSvgMouseEnter = useEventCallback((e)=>{
onSafeZoneEnter === null || onSafeZoneEnter === void 0 ? void 0 : onSafeZoneEnter(e);
setSafeZoneCloseTimeout(()=>{
stateStore.toggleActive(false);
onSafeZoneTimeout === null || onSafeZoneTimeout === void 0 ? void 0 : onSafeZoneTimeout();
}, timeout);
});
const onSvgMouseMove = useEventCallback((e)=>{
setSafeZoneCloseTimeout(()=>{
stateStore.toggleActive(false);
onSafeZoneTimeout === null || onSafeZoneTimeout === void 0 ? void 0 : onSafeZoneTimeout();
}, timeout);
onSafeZoneMove === null || onSafeZoneMove === void 0 ? void 0 : onSafeZoneMove(e);
});
const onSvgMouseLeave = useEventCallback((e)=>{
onSafeZoneLeave === null || onSafeZoneLeave === void 0 ? void 0 : onSafeZoneLeave(e);
});
React.useEffect(()=>{
return stateStore.subscribe((isActive)=>{
if (isActive) {
function updateSVGs() {
const containerEl = containerRef.current;
const targetEl = targetRef.current;
if (containerEl && targetEl) {
var _safeZoneAreaRef_current;
(_safeZoneAreaRef_current = safeZoneAreaRef.current) === null || _safeZoneAreaRef_current === void 0 ? void 0 : _safeZoneAreaRef_current.updateSVG({
containerRect: containerEl.getBoundingClientRect(),
mouseCoordinates: [
mouseCoordinatesRef.current.x,
mouseCoordinatesRef.current.y
],
targetRect: targetEl.getBoundingClientRect()
});
}
requestUpdateFrame(updateSVGs);
}
updateSVGs();
return;
}
clearUpdateFrame();
});
}, [
clearUpdateFrame,
requestUpdateFrame,
stateStore
]);
return {
containerRef: useMergedRefs(containerRef, containerListenerRef),
targetRef: useMergedRefs(targetRef, targetListenerRef),
elementToRender: React.useMemo(()=>disabled ? null : /*#__PURE__*/ React.createElement(SafeZoneArea, {
debug: debug,
onMouseEnter: onSvgMouseEnter,
onMouseMove: onSvgMouseMove,
onMouseLeave: onSvgMouseLeave,
imperativeRef: safeZoneAreaRef,
stateStore: stateStore
}), [
disabled,
debug,
onSvgMouseEnter,
onSvgMouseMove,
onSvgMouseLeave,
stateStore
])
};
}

File diff suppressed because one or more lines are too long