@apollo-design/position 0.0.0-stage → 0.1.9

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/README.md CHANGED
@@ -1,3 +1,83 @@
1
- # Temporary Holding Version
1
+ > 规范范围:本文件保留实现说明与历史对照证据;上游同构、旧阶段与历史 Gate 要求不再生效。现行规则见 [项目规范](../../AGENTS.md),公共行为以 Apollo 组件 API 文档与测试契约为准。
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # @apollo-design/position
4
+
5
+ > **层**:L1 | **风险**:high
6
+ >
7
+ > 本文档声明本包的**职责边界**。边界之外的事情不做 —— 如果发现需要做,说明架构需要调整,
8
+ > 应当先修改 `ARCHITECTURE.md` 并记录 ADR。
9
+
10
+ ---
11
+
12
+ ## 职责
13
+
14
+ 浮层定位几何。替代 @rc-component/trigger 与 rc-tooltip 的定位部分。**只做几何与尺寸测量**,触发时机与生命周期在 @apollo-design/overlay。
15
+
16
+ ## 替代的 Ant Design 依赖
17
+
18
+ - `@rc-component/trigger(定位部分)`
19
+ - `@rc-component/tooltip(定位部分)`
20
+
21
+ ## 公开 API
22
+
23
+ - placements:12 个方位(topLeft / top / topRight / bottomLeft / ...)
24
+ - 对齐计算:getAlignResult / getAlignOffset / getArrowOffset
25
+ - 边界处理:flip(翻转)/ shift(收缩)/ 滚动容器跟随(含祖先滚动链)
26
+ - 坐标解析:getPopupContainer / getDocument / 可视区与滚动区计算
27
+ - usePosition —— 把上述能力接到响应式目标与浮层元素上
28
+
29
+ ## 明确不做(边界)
30
+
31
+ - ❌ 不实现触发时机与显隐延迟(那是 @apollo-design/overlay)
32
+ - ❌ 不实现挂载与 z-index(那是 @apollo-design/portal)
33
+ - ❌ 不实现任何视觉样式与 DOM 结构(只输出坐标数字)
34
+
35
+ ## 必须遵守的契约
36
+
37
+ - ★ 定位结果必须与 antd 像素级一致(这是 AR1,本项目最大风险点)
38
+ - 纯几何部分必须可用无 DOM 的单测验证(坐标进 → 坐标出),只有尺寸测量需要 jsdom
39
+ - 必须支持翻转(flip)与自适应(shift)
40
+ - 箭头位置在 12 个方位下都正确
41
+
42
+ ## 依赖
43
+
44
+ ### 运行时依赖
45
+
46
+ | `@apollo-design/utils` | `workspace:*` |
47
+
48
+ ### peer 依赖
49
+
50
+ | `vue` | `catalog:` |
51
+
52
+ ### 构建期 / 测试依赖(devDependencies,**不会**进入用户的依赖树)
53
+
54
+ (无)
55
+
56
+ ## 依赖约束(ARCHITECTURE.md §3.1)
57
+
58
+ - **R1 单向**:只能依赖同层或更低层
59
+ - **R2 无环**:不得与其他包循环依赖
60
+ - **R3 地基纯净**(仅 L0):不得包含任何组件视觉语义
61
+ - **R4 引擎无视觉**(仅 L2):不得定义颜色/圆角/阴影,不得产出 CSS
62
+ - **R6 显式声明**:跨包导入必须在本文件的 `dependencies` 中声明(`.npmrc` 已设 `hoist=false`)
63
+ - **R7 零 Ant Design 运行时依赖**:发布包的 `dependencies` 不得出现任何 `@ant-design/*`。
64
+ Ant Design 生态包只允许出现在三处 —— ① 构建期数据源(`registry/tools/gen-*.mjs`)
65
+ ② 测试 Oracle(`*.oracle.test.ts`)③ `devDependencies`。由 `registry:validate` 的 **E19** 强制。
66
+
67
+ 本包的依赖已通过 `registry/tools/scaffold-packages.mjs` 的分层校验。
68
+
69
+ ## ⚠️ 必须先做 PoC
70
+
71
+ AR1 —— 必须先做 PoC:topLeft/topRight/center/bottomLeft/bottomRight + 翻转 + 箭头,与 antd 参考截图逐像素比对
72
+
73
+ 在 PoC 通过之前,不得开始依赖本包的组件开发。
74
+
75
+ ## 测试
76
+
77
+ ```bash
78
+ pnpm --filter @apollo-design/position test
79
+ pnpm --filter @apollo-design/position lint
80
+ ```
81
+
82
+ 测试要求见 [`TESTING.md`](../../TESTING.md)。
83
+ 覆盖率下限为 语句 95% / 分支 90% / 函数 95%(L1 档位,见 `vitest.config.ts` 的 `coverage.thresholds`)。
package/dist/align.mjs ADDED
@@ -0,0 +1,187 @@
1
+ import { getIntersectionArea } from './area.mjs';
2
+ import { getNumberOffset } from './offset.mjs';
3
+ import { splitPoints, getAlignPoint, flatPoint, reversePoint } from './point.mjs';
4
+
5
+ function supportAdjust(value) {
6
+ if (typeof value === "boolean") return value;
7
+ return value !== void 0 && value >= 0;
8
+ }
9
+ function topLeft(rect) {
10
+ return getAlignPoint(rect, ["t", "l"]);
11
+ }
12
+ function bottomRight(rect) {
13
+ return getAlignPoint(rect, ["b", "r"]);
14
+ }
15
+ function alignPopup(ctx, align, flip = {}) {
16
+ const { target, popup } = ctx;
17
+ const scaleX = ctx.scaleX ?? 1;
18
+ const scaleY = ctx.scaleY ?? 1;
19
+ const overflow = align.overflow ?? {};
20
+ const [targetOffsetX, targetOffsetY] = getNumberOffset(target, align.targetOffset);
21
+ const targetRect = {
22
+ x: target.x - targetOffsetX,
23
+ y: target.y - targetOffsetY,
24
+ width: target.width,
25
+ height: target.height
26
+ };
27
+ let [popupOffsetX, popupOffsetY] = getNumberOffset(popup, align.offset);
28
+ const htmlRegion = align.htmlRegion ?? "visible";
29
+ const isVisibleFirst = htmlRegion === "visibleFirst";
30
+ const compareArea = htmlRegion === "visible" ? ctx.visible : ctx.scroll;
31
+ const adjustArea = htmlRegion === "scroll" ? ctx.scroll : ctx.visible;
32
+ const recommendArea = ctx.visible;
33
+ const [popupPoint, targetPoint] = align.points ?? [];
34
+ const targetPoints = splitPoints(targetPoint ?? "");
35
+ const popupPoints = splitPoints(popupPoint ?? "");
36
+ const targetAlignPoint = getAlignPoint(targetRect, targetPoints);
37
+ const popupAlignPoint = getAlignPoint(popup, popupPoints);
38
+ let nextPoints = [popupPoints, targetPoints];
39
+ let nextOffsetX = targetAlignPoint.x - popupAlignPoint.x + popupOffsetX;
40
+ let nextOffsetY = targetAlignPoint.y - popupAlignPoint.y + popupOffsetY;
41
+ const originIntersectionArea = getIntersectionArea(popup, nextOffsetX, nextOffsetY, compareArea);
42
+ const originRecommendArea = getIntersectionArea(popup, nextOffsetX, nextOffsetY, recommendArea);
43
+ const nextFlip = { ...flip };
44
+ const targetTL = topLeft(targetRect);
45
+ const targetBR = bottomRight(targetRect);
46
+ const popupTL = topLeft(popup);
47
+ const popupBR = bottomRight(popup);
48
+ let nextPopupY = popup.y + nextOffsetY;
49
+ let nextPopupBottom = nextPopupY + popup.height;
50
+ let nextPopupX = popup.x + nextOffsetX;
51
+ let nextPopupRight = nextPopupX + popup.width;
52
+ const tryFlip = (axis, trigger, memoryKey, compute, apply, negateOffset) => {
53
+ if (!trigger) return;
54
+ const candidate = compute();
55
+ const newArea = axis === 0 ? getIntersectionArea(popup, nextOffsetX, candidate, compareArea) : getIntersectionArea(popup, candidate, nextOffsetY, compareArea);
56
+ const newRecommend = axis === 0 ? getIntersectionArea(popup, nextOffsetX, candidate, recommendArea) : getIntersectionArea(popup, candidate, nextOffsetY, recommendArea);
57
+ const accepted = newArea > originIntersectionArea || newArea === originIntersectionArea && (!isVisibleFirst || newRecommend >= originRecommendArea);
58
+ nextFlip[memoryKey] = accepted;
59
+ if (!accepted) return;
60
+ apply(candidate);
61
+ negateOffset();
62
+ nextPoints = [reversePoint(nextPoints[0], axis), reversePoint(nextPoints[1], axis)];
63
+ };
64
+ const needAdjustY = supportAdjust(overflow.adjustY);
65
+ const sameTB = popupPoints[0] === targetPoints[0];
66
+ tryFlip(
67
+ 0,
68
+ needAdjustY && popupPoints[0] === "t" && (nextPopupBottom > adjustArea.bottom || !!flip.bt),
69
+ "bt",
70
+ () => sameTB ? nextOffsetY - (popup.height - targetRect.height) : targetTL.y - popupBR.y - popupOffsetY,
71
+ (v) => {
72
+ nextOffsetY = v;
73
+ },
74
+ () => {
75
+ popupOffsetY = -popupOffsetY;
76
+ }
77
+ );
78
+ tryFlip(
79
+ 0,
80
+ needAdjustY && popupPoints[0] === "b" && (nextPopupY < adjustArea.top || !!flip.tb),
81
+ "tb",
82
+ () => sameTB ? nextOffsetY + (popup.height - targetRect.height) : targetBR.y - popupTL.y - popupOffsetY,
83
+ (v) => {
84
+ nextOffsetY = v;
85
+ },
86
+ () => {
87
+ popupOffsetY = -popupOffsetY;
88
+ }
89
+ );
90
+ const needAdjustX = supportAdjust(overflow.adjustX);
91
+ const sameLR = popupPoints[1] === targetPoints[1];
92
+ tryFlip(
93
+ 1,
94
+ needAdjustX && popupPoints[1] === "l" && (nextPopupRight > adjustArea.right || !!flip.rl),
95
+ "rl",
96
+ () => sameLR ? nextOffsetX - (popup.width - targetRect.width) : targetTL.x - popupBR.x - popupOffsetX,
97
+ (v) => {
98
+ nextOffsetX = v;
99
+ },
100
+ () => {
101
+ popupOffsetX = -popupOffsetX;
102
+ }
103
+ );
104
+ tryFlip(
105
+ 1,
106
+ needAdjustX && popupPoints[1] === "r" && (nextPopupX < adjustArea.left || !!flip.lr),
107
+ "lr",
108
+ () => sameLR ? nextOffsetX + (popup.width - targetRect.width) : targetBR.x - popupTL.x - popupOffsetX,
109
+ (v) => {
110
+ nextOffsetX = v;
111
+ },
112
+ () => {
113
+ popupOffsetX = -popupOffsetX;
114
+ }
115
+ );
116
+ nextPopupY = popup.y + nextOffsetY;
117
+ nextPopupBottom = nextPopupY + popup.height;
118
+ nextPopupX = popup.x + nextOffsetX;
119
+ nextPopupRight = nextPopupX + popup.width;
120
+ const numShiftX = overflow.shiftX === true ? 0 : overflow.shiftX;
121
+ if (typeof numShiftX === "number") {
122
+ if (nextPopupX < recommendArea.left) {
123
+ nextOffsetX -= nextPopupX - recommendArea.left - popupOffsetX;
124
+ if (targetRect.x + targetRect.width < recommendArea.left + numShiftX) {
125
+ nextOffsetX += targetRect.x - recommendArea.left + targetRect.width - numShiftX;
126
+ }
127
+ }
128
+ if (nextPopupRight > recommendArea.right) {
129
+ nextOffsetX -= nextPopupRight - recommendArea.right - popupOffsetX;
130
+ if (targetRect.x > recommendArea.right - numShiftX) {
131
+ nextOffsetX += targetRect.x - recommendArea.right + numShiftX;
132
+ }
133
+ }
134
+ }
135
+ const numShiftY = overflow.shiftY === true ? 0 : overflow.shiftY;
136
+ if (typeof numShiftY === "number") {
137
+ if (nextPopupY < recommendArea.top) {
138
+ nextOffsetY -= nextPopupY - recommendArea.top - popupOffsetY;
139
+ if (targetRect.y + targetRect.height < recommendArea.top + numShiftY) {
140
+ nextOffsetY += targetRect.y - recommendArea.top + targetRect.height - numShiftY;
141
+ }
142
+ }
143
+ if (nextPopupBottom > recommendArea.bottom) {
144
+ nextOffsetY -= nextPopupBottom - recommendArea.bottom - popupOffsetY;
145
+ if (targetRect.y > recommendArea.bottom - numShiftY) {
146
+ nextOffsetY += targetRect.y - recommendArea.bottom + numShiftY;
147
+ }
148
+ }
149
+ }
150
+ const popupLeft = popup.x + nextOffsetX;
151
+ const popupRight = popupLeft + popup.width;
152
+ const popupTop = popup.y + nextOffsetY;
153
+ const popupBottom = popupTop + popup.height;
154
+ const targetLeft = targetRect.x;
155
+ const targetRight = targetLeft + targetRect.width;
156
+ const targetTop = targetRect.y;
157
+ const targetBottom = targetTop + targetRect.height;
158
+ const xCenter = (Math.max(popupLeft, targetLeft) + Math.min(popupRight, targetRight)) / 2;
159
+ const yCenter = (Math.max(popupTop, targetTop) + Math.min(popupBottom, targetBottom)) / 2;
160
+ const nextArrowX = xCenter - popupLeft;
161
+ const nextArrowY = yCenter - popupTop;
162
+ let finalOffsetX = nextOffsetX;
163
+ let finalOffsetY = nextOffsetY;
164
+ if (scaleX === 1) finalOffsetX = Math.floor(finalOffsetX);
165
+ if (scaleY === 1) finalOffsetY = Math.floor(finalOffsetY);
166
+ return {
167
+ offsetX: finalOffsetX / scaleX,
168
+ offsetY: finalOffsetY / scaleY,
169
+ arrowX: nextArrowX / scaleX,
170
+ arrowY: nextArrowY / scaleY,
171
+ points: [flatPoint(nextPoints[0]), flatPoint(nextPoints[1])],
172
+ flip: nextFlip,
173
+ overflow
174
+ };
175
+ }
176
+ function toAlignResult(outcome) {
177
+ return {
178
+ offsetX: outcome.offsetX,
179
+ offsetY: outcome.offsetY,
180
+ arrowX: outcome.arrowX,
181
+ arrowY: outcome.arrowY,
182
+ points: outcome.points,
183
+ flip: outcome.flip
184
+ };
185
+ }
186
+
187
+ export { alignPopup, toAlignResult };
package/dist/area.mjs ADDED
@@ -0,0 +1,29 @@
1
+ function getIntersectionArea(rect, offsetX, offsetY, area) {
2
+ const left = rect.x + offsetX;
3
+ const top = rect.y + offsetY;
4
+ const right = left + rect.width;
5
+ const bottom = top + rect.height;
6
+ const width = Math.min(right, area.right) - Math.max(left, area.left);
7
+ const height = Math.min(bottom, area.bottom) - Math.max(top, area.top);
8
+ return Math.max(0, width) * Math.max(0, height);
9
+ }
10
+ function clipArea(initArea, clips) {
11
+ const visibleArea = { ...initArea };
12
+ for (const clip of clips) {
13
+ visibleArea.left = Math.max(visibleArea.left, clip.left);
14
+ visibleArea.top = Math.max(visibleArea.top, clip.top);
15
+ visibleArea.right = Math.min(visibleArea.right, clip.right);
16
+ visibleArea.bottom = Math.min(visibleArea.bottom, clip.bottom);
17
+ }
18
+ return visibleArea;
19
+ }
20
+ function rectToArea(rect) {
21
+ return {
22
+ left: rect.x,
23
+ top: rect.y,
24
+ right: rect.x + rect.width,
25
+ bottom: rect.y + rect.height
26
+ };
27
+ }
28
+
29
+ export { clipArea, getIntersectionArea, rectToArea };