@rogieking/figui3 6.6.2 → 6.6.4
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/components.css +5 -1
- package/dist/components.css +1 -1
- package/dist/fig.css +1 -1
- package/dist/fig.js +17 -17
- package/fig.js +271 -115
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -2884,10 +2884,167 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2884
2884
|
return "top";
|
|
2885
2885
|
}
|
|
2886
2886
|
|
|
2887
|
-
|
|
2887
|
+
lengthToPx(value, fallback = 0) {
|
|
2888
|
+
const styles = getComputedStyle(this);
|
|
2889
|
+
const raw = String(value || "").trim();
|
|
2890
|
+
const n = parseFloat(raw);
|
|
2891
|
+
if (!Number.isFinite(n)) return fallback;
|
|
2892
|
+
if (raw.endsWith("rem")) {
|
|
2893
|
+
return n * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
2894
|
+
}
|
|
2895
|
+
if (raw.endsWith("em")) {
|
|
2896
|
+
return n * parseFloat(styles.fontSize);
|
|
2897
|
+
}
|
|
2898
|
+
return n;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
radiusForSide(side) {
|
|
2902
|
+
const styles = getComputedStyle(this);
|
|
2903
|
+
const toPx = (value) => this.lengthToPx(value, 0);
|
|
2904
|
+
if (side === "top") {
|
|
2905
|
+
return Math.max(
|
|
2906
|
+
toPx(styles.borderTopLeftRadius),
|
|
2907
|
+
toPx(styles.borderTopRightRadius),
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
if (side === "bottom") {
|
|
2911
|
+
return Math.max(
|
|
2912
|
+
toPx(styles.borderBottomLeftRadius),
|
|
2913
|
+
toPx(styles.borderBottomRightRadius),
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
if (side === "left") {
|
|
2917
|
+
return Math.max(
|
|
2918
|
+
toPx(styles.borderTopLeftRadius),
|
|
2919
|
+
toPx(styles.borderBottomLeftRadius),
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2922
|
+
if (side === "right") {
|
|
2923
|
+
return Math.max(
|
|
2924
|
+
toPx(styles.borderTopRightRadius),
|
|
2925
|
+
toPx(styles.borderBottomRightRadius),
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
return 0;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
getBeakEdgeInset(beakSide) {
|
|
2932
|
+
const beakWidth = this.lengthToPx(
|
|
2933
|
+
getComputedStyle(this).getPropertyValue("--fig-popup-beak-width"),
|
|
2934
|
+
16,
|
|
2935
|
+
);
|
|
2936
|
+
return Math.max(10, this.radiusForSide(beakSide) + beakWidth / 2);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
tracksAnchorBeak() {
|
|
2888
2940
|
const variant = this.getAttribute("variant");
|
|
2889
|
-
|
|
2890
|
-
|
|
2941
|
+
return variant === "popover" || variant === "tooltip";
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
getViewportBounds(m) {
|
|
2945
|
+
const vv = window.visualViewport;
|
|
2946
|
+
const width = vv?.width ?? window.innerWidth;
|
|
2947
|
+
const height = vv?.height ?? window.innerHeight;
|
|
2948
|
+
const offsetLeft = vv?.offsetLeft ?? 0;
|
|
2949
|
+
const offsetTop = vv?.offsetTop ?? 0;
|
|
2950
|
+
|
|
2951
|
+
return {
|
|
2952
|
+
minLeft: offsetLeft + m.left,
|
|
2953
|
+
minTop: offsetTop + m.top,
|
|
2954
|
+
maxRight: offsetLeft + width - m.right,
|
|
2955
|
+
maxBottom: offsetTop + height - m.bottom,
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
clampToViewport(coords, popupRect, m) {
|
|
2960
|
+
const bounds = this.getViewportBounds(m);
|
|
2961
|
+
const maxLeft = bounds.maxRight - popupRect.width;
|
|
2962
|
+
const maxTop = bounds.maxBottom - popupRect.height;
|
|
2963
|
+
|
|
2964
|
+
return {
|
|
2965
|
+
left: Math.min(maxLeft, Math.max(bounds.minLeft, coords.left)),
|
|
2966
|
+
top: Math.min(maxTop, Math.max(bounds.minTop, coords.top)),
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
resolveCoordsAtViewport(anchorRect, popupRect, coords, placementSide, m) {
|
|
2971
|
+
let { left, top } = this.clampToViewport(coords, popupRect, m);
|
|
2972
|
+
if (!anchorRect || !this.tracksAnchorBeak()) {
|
|
2973
|
+
return { left, top };
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
const beakSide = this.oppositeSide(placementSide);
|
|
2977
|
+
const bounds = this.getViewportBounds(m);
|
|
2978
|
+
const maxLeft = bounds.maxRight - popupRect.width;
|
|
2979
|
+
const minLeft = bounds.minLeft;
|
|
2980
|
+
const maxTop = bounds.maxBottom - popupRect.height;
|
|
2981
|
+
const minTop = bounds.minTop;
|
|
2982
|
+
|
|
2983
|
+
if (beakSide === "top" || beakSide === "bottom") {
|
|
2984
|
+
const inset = this.getBeakEdgeInset(beakSide);
|
|
2985
|
+
const anchorCenterX = anchorRect.left + anchorRect.width / 2;
|
|
2986
|
+
const beakOffset = anchorCenterX - left;
|
|
2987
|
+
if (beakOffset < inset) {
|
|
2988
|
+
left = anchorCenterX - inset;
|
|
2989
|
+
} else if (beakOffset > popupRect.width - inset) {
|
|
2990
|
+
left = anchorCenterX - (popupRect.width - inset);
|
|
2991
|
+
}
|
|
2992
|
+
left = Math.min(maxLeft, Math.max(minLeft, left));
|
|
2993
|
+
} else if (beakSide === "left" || beakSide === "right") {
|
|
2994
|
+
const inset = this.getBeakEdgeInset(beakSide);
|
|
2995
|
+
const anchorCenterY = anchorRect.top + anchorRect.height / 2;
|
|
2996
|
+
const beakOffset = anchorCenterY - top;
|
|
2997
|
+
if (beakOffset < inset) {
|
|
2998
|
+
top = anchorCenterY - inset;
|
|
2999
|
+
} else if (beakOffset > popupRect.height - inset) {
|
|
3000
|
+
top = anchorCenterY - (popupRect.height - inset);
|
|
3001
|
+
}
|
|
3002
|
+
top = Math.min(maxTop, Math.max(minTop, top));
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
return { left, top };
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
canPointAtAnchor(anchorRect, popupRect, left, top, placementSide) {
|
|
3009
|
+
if (!anchorRect || !this.tracksAnchorBeak()) return true;
|
|
3010
|
+
|
|
3011
|
+
const beakSide = this.oppositeSide(placementSide);
|
|
3012
|
+
const inset = this.getBeakEdgeInset(beakSide);
|
|
3013
|
+
|
|
3014
|
+
if (beakSide === "top" || beakSide === "bottom") {
|
|
3015
|
+
const beakOffset = anchorRect.left + anchorRect.width / 2 - left;
|
|
3016
|
+
return (
|
|
3017
|
+
beakOffset >= inset - 0.5 &&
|
|
3018
|
+
beakOffset <= popupRect.width - inset + 0.5
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
const beakOffset = anchorRect.top + anchorRect.height / 2 - top;
|
|
3023
|
+
return (
|
|
3024
|
+
beakOffset >= inset - 0.5 &&
|
|
3025
|
+
beakOffset <= popupRect.height - inset + 0.5
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
updatePointerVisibility(anchorRect, popupRect, left, top, placementSide) {
|
|
3030
|
+
if (!this.tracksAnchorBeak()) return;
|
|
3031
|
+
if (this.getAttribute("pointer") === "false") return;
|
|
3032
|
+
|
|
3033
|
+
if (
|
|
3034
|
+
anchorRect &&
|
|
3035
|
+
!this.canPointAtAnchor(anchorRect, popupRect, left, top, placementSide)
|
|
3036
|
+
) {
|
|
3037
|
+
this.setAttribute("pointer", "false");
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
if (this.hasAttribute("pointer")) {
|
|
3042
|
+
this.removeAttribute("pointer");
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
updatePopoverBeak(anchorRect, popupRect, left, top, placementSide) {
|
|
3047
|
+
if (!this.tracksAnchorBeak() || !anchorRect) {
|
|
2891
3048
|
this.style.removeProperty("--fig-popup-beak-offset");
|
|
2892
3049
|
this.removeAttribute("data-beak-side");
|
|
2893
3050
|
return;
|
|
@@ -2903,54 +3060,9 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2903
3060
|
measuredRect.width > 0 && measuredRect.height > 0
|
|
2904
3061
|
? measuredRect
|
|
2905
3062
|
: popupRect;
|
|
2906
|
-
// Always use the rendered popup rect so beak alignment matches real final placement.
|
|
2907
3063
|
const resolvedLeft = rect.left;
|
|
2908
3064
|
const resolvedTop = rect.top;
|
|
2909
|
-
const
|
|
2910
|
-
const toPx = (value, fallback = 0) => {
|
|
2911
|
-
const raw = String(value || "").trim();
|
|
2912
|
-
const n = parseFloat(raw);
|
|
2913
|
-
if (!Number.isFinite(n)) return fallback;
|
|
2914
|
-
if (raw.endsWith("rem")) {
|
|
2915
|
-
return n * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
2916
|
-
}
|
|
2917
|
-
if (raw.endsWith("em")) {
|
|
2918
|
-
return n * parseFloat(styles.fontSize);
|
|
2919
|
-
}
|
|
2920
|
-
return n;
|
|
2921
|
-
};
|
|
2922
|
-
const radiusForSide = (side) => {
|
|
2923
|
-
if (side === "top") {
|
|
2924
|
-
return Math.max(
|
|
2925
|
-
toPx(styles.borderTopLeftRadius),
|
|
2926
|
-
toPx(styles.borderTopRightRadius),
|
|
2927
|
-
);
|
|
2928
|
-
}
|
|
2929
|
-
if (side === "bottom") {
|
|
2930
|
-
return Math.max(
|
|
2931
|
-
toPx(styles.borderBottomLeftRadius),
|
|
2932
|
-
toPx(styles.borderBottomRightRadius),
|
|
2933
|
-
);
|
|
2934
|
-
}
|
|
2935
|
-
if (side === "left") {
|
|
2936
|
-
return Math.max(
|
|
2937
|
-
toPx(styles.borderTopLeftRadius),
|
|
2938
|
-
toPx(styles.borderBottomLeftRadius),
|
|
2939
|
-
);
|
|
2940
|
-
}
|
|
2941
|
-
if (side === "right") {
|
|
2942
|
-
return Math.max(
|
|
2943
|
-
toPx(styles.borderTopRightRadius),
|
|
2944
|
-
toPx(styles.borderBottomRightRadius),
|
|
2945
|
-
);
|
|
2946
|
-
}
|
|
2947
|
-
return 0;
|
|
2948
|
-
};
|
|
2949
|
-
const beakWidth = toPx(
|
|
2950
|
-
styles.getPropertyValue("--fig-popup-beak-width"),
|
|
2951
|
-
16,
|
|
2952
|
-
);
|
|
2953
|
-
const edgeInset = Math.max(10, radiusForSide(beakSide) + beakWidth / 2);
|
|
3065
|
+
const edgeInset = this.getBeakEdgeInset(beakSide);
|
|
2954
3066
|
|
|
2955
3067
|
let beakOffset;
|
|
2956
3068
|
if (beakSide === "top" || beakSide === "bottom") {
|
|
@@ -2969,15 +3081,14 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2969
3081
|
}
|
|
2970
3082
|
|
|
2971
3083
|
overflowScore(coords, popupRect, m) {
|
|
2972
|
-
const
|
|
2973
|
-
const vh = window.innerHeight;
|
|
3084
|
+
const bounds = this.getViewportBounds(m);
|
|
2974
3085
|
const right = coords.left + popupRect.width;
|
|
2975
3086
|
const bottom = coords.top + popupRect.height;
|
|
2976
3087
|
|
|
2977
|
-
const overflowLeft = Math.max(0,
|
|
2978
|
-
const overflowTop = Math.max(0,
|
|
2979
|
-
const overflowRight = Math.max(0, right -
|
|
2980
|
-
const overflowBottom = Math.max(0, bottom -
|
|
3088
|
+
const overflowLeft = Math.max(0, bounds.minLeft - coords.left);
|
|
3089
|
+
const overflowTop = Math.max(0, bounds.minTop - coords.top);
|
|
3090
|
+
const overflowRight = Math.max(0, right - bounds.maxRight);
|
|
3091
|
+
const overflowBottom = Math.max(0, bottom - bounds.maxBottom);
|
|
2981
3092
|
|
|
2982
3093
|
return overflowLeft + overflowTop + overflowRight + overflowBottom;
|
|
2983
3094
|
}
|
|
@@ -2986,22 +3097,73 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2986
3097
|
return this.overflowScore(coords, popupRect, m) === 0;
|
|
2987
3098
|
}
|
|
2988
3099
|
|
|
2989
|
-
clamp(coords, popupRect, m) {
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3100
|
+
clamp(coords, popupRect, m, anchorRect = null, placementSide = "top") {
|
|
3101
|
+
if (anchorRect) {
|
|
3102
|
+
return this.resolveCoordsAtViewport(
|
|
3103
|
+
anchorRect,
|
|
3104
|
+
popupRect,
|
|
3105
|
+
coords,
|
|
3106
|
+
placementSide,
|
|
3107
|
+
m,
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
return this.clampToViewport(coords, popupRect, m);
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
placementScore(anchorRect, popupRect, coords, placementSide, m) {
|
|
3114
|
+
const resolved = this.resolveCoordsAtViewport(
|
|
3115
|
+
anchorRect,
|
|
3116
|
+
popupRect,
|
|
3117
|
+
coords,
|
|
3118
|
+
placementSide,
|
|
3119
|
+
m,
|
|
2999
3120
|
);
|
|
3121
|
+
let score = this.overflowScore(resolved, popupRect, m);
|
|
3122
|
+
if (
|
|
3123
|
+
anchorRect &&
|
|
3124
|
+
!this.canPointAtAnchor(
|
|
3125
|
+
anchorRect,
|
|
3126
|
+
popupRect,
|
|
3127
|
+
resolved.left,
|
|
3128
|
+
resolved.top,
|
|
3129
|
+
placementSide,
|
|
3130
|
+
)
|
|
3131
|
+
) {
|
|
3132
|
+
score += 10000;
|
|
3133
|
+
}
|
|
3134
|
+
return { score, resolved };
|
|
3135
|
+
}
|
|
3000
3136
|
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3137
|
+
applyPopupPosition(
|
|
3138
|
+
anchorRect,
|
|
3139
|
+
popupRect,
|
|
3140
|
+
coords,
|
|
3141
|
+
placementSide,
|
|
3142
|
+
m,
|
|
3143
|
+
) {
|
|
3144
|
+
const resolved = this.resolveCoordsAtViewport(
|
|
3145
|
+
anchorRect,
|
|
3146
|
+
popupRect,
|
|
3147
|
+
coords,
|
|
3148
|
+
placementSide,
|
|
3149
|
+
m,
|
|
3150
|
+
);
|
|
3151
|
+
this.style.left = `${resolved.left}px`;
|
|
3152
|
+
this.style.top = `${resolved.top}px`;
|
|
3153
|
+
this.updatePointerVisibility(
|
|
3154
|
+
anchorRect,
|
|
3155
|
+
popupRect,
|
|
3156
|
+
resolved.left,
|
|
3157
|
+
resolved.top,
|
|
3158
|
+
placementSide,
|
|
3159
|
+
);
|
|
3160
|
+
this.updatePopoverBeak(
|
|
3161
|
+
anchorRect,
|
|
3162
|
+
popupRect,
|
|
3163
|
+
resolved.left,
|
|
3164
|
+
resolved.top,
|
|
3165
|
+
placementSide,
|
|
3166
|
+
);
|
|
3005
3167
|
}
|
|
3006
3168
|
|
|
3007
3169
|
positionPopup() {
|
|
@@ -3015,14 +3177,16 @@ class FigPopup extends HTMLDialogElement {
|
|
|
3015
3177
|
|
|
3016
3178
|
if (!anchor) {
|
|
3017
3179
|
this.updatePopoverBeak(null, popupRect, 0, 0, "top");
|
|
3180
|
+
const bounds = this.getViewportBounds(m);
|
|
3018
3181
|
const centered = {
|
|
3019
3182
|
left:
|
|
3020
|
-
|
|
3183
|
+
bounds.minLeft +
|
|
3184
|
+
(bounds.maxRight - bounds.minLeft - popupRect.width) / 2,
|
|
3021
3185
|
top:
|
|
3022
|
-
|
|
3023
|
-
(
|
|
3186
|
+
bounds.minTop +
|
|
3187
|
+
(bounds.maxBottom - bounds.minTop - popupRect.height) / 2,
|
|
3024
3188
|
};
|
|
3025
|
-
const clamped = this.
|
|
3189
|
+
const clamped = this.clampToViewport(centered, popupRect, m);
|
|
3026
3190
|
this.style.left = `${clamped.left}px`;
|
|
3027
3191
|
this.style.top = `${clamped.top}px`;
|
|
3028
3192
|
return;
|
|
@@ -3041,64 +3205,56 @@ class FigPopup extends HTMLDialogElement {
|
|
|
3041
3205
|
for (const { v, h, s } of candidates) {
|
|
3042
3206
|
const coords = this.computeCoords(anchorRect, popupRect, v, h, offset, s);
|
|
3043
3207
|
const placementSide = this.getPlacementSide(v, h, s);
|
|
3208
|
+
const { score, resolved } = this.placementScore(
|
|
3209
|
+
anchorRect,
|
|
3210
|
+
popupRect,
|
|
3211
|
+
coords,
|
|
3212
|
+
placementSide,
|
|
3213
|
+
m,
|
|
3214
|
+
);
|
|
3044
3215
|
|
|
3045
3216
|
if (s) {
|
|
3046
|
-
const
|
|
3217
|
+
const bounds = this.getViewportBounds(m);
|
|
3047
3218
|
const primaryFits =
|
|
3048
3219
|
s === "left" || s === "right"
|
|
3049
|
-
? coords.left >=
|
|
3050
|
-
coords.left + popupRect.width <=
|
|
3051
|
-
: coords.top >=
|
|
3052
|
-
coords.top + popupRect.height <=
|
|
3053
|
-
if (primaryFits) {
|
|
3054
|
-
this.
|
|
3055
|
-
this.style.top = `${clamped.top}px`;
|
|
3056
|
-
this.updatePopoverBeak(
|
|
3220
|
+
? coords.left >= bounds.minLeft &&
|
|
3221
|
+
coords.left + popupRect.width <= bounds.maxRight
|
|
3222
|
+
: coords.top >= bounds.minTop &&
|
|
3223
|
+
coords.top + popupRect.height <= bounds.maxBottom;
|
|
3224
|
+
if (primaryFits && score < 10000) {
|
|
3225
|
+
this.applyPopupPosition(
|
|
3057
3226
|
anchorRect,
|
|
3058
3227
|
popupRect,
|
|
3059
|
-
|
|
3060
|
-
clamped.top,
|
|
3228
|
+
coords,
|
|
3061
3229
|
placementSide,
|
|
3230
|
+
m,
|
|
3062
3231
|
);
|
|
3063
3232
|
return;
|
|
3064
3233
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
placementSide,
|
|
3081
|
-
);
|
|
3082
|
-
return;
|
|
3083
|
-
}
|
|
3084
|
-
const score = this.overflowScore(coords, popupRect, m);
|
|
3085
|
-
if (score < bestScore) {
|
|
3086
|
-
bestScore = score;
|
|
3087
|
-
best = coords;
|
|
3088
|
-
bestSide = placementSide;
|
|
3089
|
-
}
|
|
3234
|
+
} else if (score === 0) {
|
|
3235
|
+
this.applyPopupPosition(
|
|
3236
|
+
anchorRect,
|
|
3237
|
+
popupRect,
|
|
3238
|
+
coords,
|
|
3239
|
+
placementSide,
|
|
3240
|
+
m,
|
|
3241
|
+
);
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
if (score < bestScore) {
|
|
3246
|
+
bestScore = score;
|
|
3247
|
+
best = resolved;
|
|
3248
|
+
bestSide = placementSide;
|
|
3090
3249
|
}
|
|
3091
3250
|
}
|
|
3092
3251
|
|
|
3093
|
-
|
|
3094
|
-
this.style.left = `${clamped.left}px`;
|
|
3095
|
-
this.style.top = `${clamped.top}px`;
|
|
3096
|
-
this.updatePopoverBeak(
|
|
3252
|
+
this.applyPopupPosition(
|
|
3097
3253
|
anchorRect,
|
|
3098
3254
|
popupRect,
|
|
3099
|
-
|
|
3100
|
-
clamped.top,
|
|
3255
|
+
best || { left: 0, top: 0 },
|
|
3101
3256
|
bestSide,
|
|
3257
|
+
m,
|
|
3102
3258
|
);
|
|
3103
3259
|
}
|
|
3104
3260
|
|
package/package.json
CHANGED