@buoy-gg/debug-borders 2.0.2
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 +334 -0
- package/lib/commonjs/debug-borders/components/DebugBordersModal.js +234 -0
- package/lib/commonjs/debug-borders/components/DebugBordersStandaloneOverlay.js +436 -0
- package/lib/commonjs/debug-borders/index.js +51 -0
- package/lib/commonjs/debug-borders/types.js +1 -0
- package/lib/commonjs/debug-borders/utils/DebugBordersManager.js +119 -0
- package/lib/commonjs/debug-borders/utils/ViewTypeMapper.js +264 -0
- package/lib/commonjs/debug-borders/utils/colorGeneration.js +76 -0
- package/lib/commonjs/debug-borders/utils/componentInfo.js +183 -0
- package/lib/commonjs/debug-borders/utils/componentMeasurement.js +111 -0
- package/lib/commonjs/debug-borders/utils/fiberTreeTraversal.js +309 -0
- package/lib/commonjs/debug-borders/utils/labelPositioning.js +202 -0
- package/lib/commonjs/index.js +34 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +178 -0
- package/lib/module/debug-borders/components/DebugBordersModal.js +229 -0
- package/lib/module/debug-borders/components/DebugBordersStandaloneOverlay.js +432 -0
- package/lib/module/debug-borders/index.js +15 -0
- package/lib/module/debug-borders/types.js +1 -0
- package/lib/module/debug-borders/utils/DebugBordersManager.js +119 -0
- package/lib/module/debug-borders/utils/ViewTypeMapper.js +255 -0
- package/lib/module/debug-borders/utils/colorGeneration.js +76 -0
- package/lib/module/debug-borders/utils/componentInfo.js +183 -0
- package/lib/module/debug-borders/utils/componentMeasurement.js +111 -0
- package/lib/module/debug-borders/utils/fiberTreeTraversal.js +309 -0
- package/lib/module/debug-borders/utils/labelPositioning.js +202 -0
- package/lib/module/index.js +7 -0
- package/lib/module/preset.js +166 -0
- package/lib/typescript/debug-borders/components/DebugBordersModal.d.ts +11 -0
- package/lib/typescript/debug-borders/components/DebugBordersModal.d.ts.map +1 -0
- package/lib/typescript/debug-borders/components/DebugBordersStandaloneOverlay.d.ts +15 -0
- package/lib/typescript/debug-borders/components/DebugBordersStandaloneOverlay.d.ts.map +1 -0
- package/lib/typescript/debug-borders/index.d.ts +8 -0
- package/lib/typescript/debug-borders/index.d.ts.map +1 -0
- package/lib/typescript/debug-borders/types.d.ts +45 -0
- package/lib/typescript/debug-borders/types.d.ts.map +1 -0
- package/lib/typescript/debug-borders/utils/ViewTypeMapper.d.ts +66 -0
- package/lib/typescript/debug-borders/utils/ViewTypeMapper.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +3 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +108 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function getReactDevToolsHook() {
|
|
4
|
+
if (typeof global === "undefined") {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
8
|
+
if (!hook) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return hook;
|
|
12
|
+
}
|
|
13
|
+
function getFiberRoots() {
|
|
14
|
+
const hook = getReactDevToolsHook();
|
|
15
|
+
if (!hook) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
if (!hook.getFiberRoots) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const rootsSet = hook.getFiberRoots(1);
|
|
23
|
+
if (!rootsSet) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
return Array.from(rootsSet);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const FiberTags = {
|
|
32
|
+
FunctionComponent: 0,
|
|
33
|
+
ClassComponent: 1,
|
|
34
|
+
IndeterminateComponent: 2,
|
|
35
|
+
HostRoot: 3,
|
|
36
|
+
HostPortal: 4,
|
|
37
|
+
HostComponent: 5,
|
|
38
|
+
HostText: 6,
|
|
39
|
+
Fragment: 7,
|
|
40
|
+
Mode: 8,
|
|
41
|
+
ContextConsumer: 9,
|
|
42
|
+
ContextProvider: 10,
|
|
43
|
+
ForwardRef: 11,
|
|
44
|
+
Profiler: 12,
|
|
45
|
+
SuspenseComponent: 13,
|
|
46
|
+
MemoComponent: 14,
|
|
47
|
+
SimpleMemoComponent: 15,
|
|
48
|
+
LazyComponent: 16,
|
|
49
|
+
IncompleteClassComponent: 17,
|
|
50
|
+
DehydratedFragment: 18,
|
|
51
|
+
SuspenseListComponent: 19,
|
|
52
|
+
ScopeComponent: 21,
|
|
53
|
+
OffscreenComponent: 22,
|
|
54
|
+
LegacyHiddenComponent: 23,
|
|
55
|
+
CacheComponent: 24,
|
|
56
|
+
TracingMarkerComponent: 25
|
|
57
|
+
};
|
|
58
|
+
const DEV_TOOLS_COMPONENT_NAMES = new Set(["FloatingTools", "FloatingDevTools", "FloatingMenu", "DialDevTools", "DevToolsVisibilityProvider", "AppHostProvider", "MinimizedToolsProvider", "MinimizedToolsStack", "GlitchToolButton", "DefaultConfigProvider", "JsModalComponent", "JsModal", "ModalHeader", "DraggableHeader", "DragIndicator", "CornerHandle", "WindowControls", "HighlightUpdatesModal", "HighlightUpdatesOverlay", "HighlightFilterView", "RenderDetailView", "RenderListItem", "RenderListItemInner", "RenderHistoryViewer", "RenderCauseBadge", "DebugBordersStandaloneOverlay", "DebugBordersModal", "StorageBrowser", "StorageModal", "GameUIStorageBrowser", "StorageEventListener", "StorageEventFilterView", "NetworkMonitor", "NetworkModal", "NetworkRequestList", "NetworkRequestDetail", "RouteEventsModal", "RouteEventsList", "EnvSwitcher", "EnvSwitcherModal", "TabSelector", "SectionHeader", "TypePicker", "PatternInput", "PatternChip", "DetectedItemsSection", "DetectedCategoryBadge", "IdentifierBadge", "CategoryBadge", "AppRenderer", "AppOverlay", "ExpandablePopover", "Divider", "UserStatus", "GripVerticalIcon", "LogBox", "LogBoxLog", "LogBoxLogNotification", "LogBoxNotificationContainer", "_LogBoxNotificationContainer", "LogBoxInspector", "LogBoxInspectorContainer", "LogBoxInspectorHeader", "LogBoxInspectorBody", "LogBoxInspectorFooter", "LogBoxInspectorMessageHeader", "LogBoxInspectorStackFrame", "LogBoxInspectorSection", "LogBoxButton", "LogBoxMessage"]);
|
|
59
|
+
const DEV_TOOLS_COMPONENT_PREFIXES = ["JsModal", "HighlightUpdates", "RenderList", "RenderDetail", "DebugBorders", "Storage", "Network", "RouteEvents", "EnvSwitcher", "Floating", "Minimized", "Expandable", "DevTools", "GameUI"];
|
|
60
|
+
const DEV_TOOLS_NATIVE_IDS = new Set(["debug-borders-overlay", "floating-devtools-root", "dial-devtools-root", "jsmodal-root", "highlight-updates-overlay", "__rn_buoy__highlight-modal", "logbox_inspector", "logbox"]);
|
|
61
|
+
const devToolsNodeCache = new Map();
|
|
62
|
+
const CACHE_MAX_SIZE = 500;
|
|
63
|
+
function getNativeTag(stateNode) {
|
|
64
|
+
if (!stateNode) return null;
|
|
65
|
+
if (typeof stateNode._nativeTag === "number") return stateNode._nativeTag;
|
|
66
|
+
if (typeof stateNode.canonical?._nativeTag === "number") {
|
|
67
|
+
return stateNode.canonical._nativeTag;
|
|
68
|
+
}
|
|
69
|
+
if (typeof stateNode.__nativeTag === "number") return stateNode.__nativeTag;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function isDevToolsNativeID(nativeID) {
|
|
73
|
+
if (!nativeID) return false;
|
|
74
|
+
if (DEV_TOOLS_NATIVE_IDS.has(nativeID)) return true;
|
|
75
|
+
const firstChar = nativeID.charCodeAt(0);
|
|
76
|
+
if (firstChar === 95) {
|
|
77
|
+
if (nativeID.startsWith("__highlight_") || nativeID.startsWith("__rn_buoy__")) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (firstChar === 108 && nativeID.startsWith("logbox")) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
function getComponentName(fiber) {
|
|
87
|
+
if (!fiber) return null;
|
|
88
|
+
const type = fiber.type;
|
|
89
|
+
if (type) {
|
|
90
|
+
if (typeof type === "function") {
|
|
91
|
+
return type.displayName || type.name || null;
|
|
92
|
+
}
|
|
93
|
+
if (typeof type === "string") {
|
|
94
|
+
return type;
|
|
95
|
+
}
|
|
96
|
+
if (type.displayName) return type.displayName;
|
|
97
|
+
if (type.name) return type.name;
|
|
98
|
+
}
|
|
99
|
+
const elementType = fiber.elementType;
|
|
100
|
+
if (elementType) {
|
|
101
|
+
if (typeof elementType === "function") {
|
|
102
|
+
return elementType.displayName || elementType.name || null;
|
|
103
|
+
}
|
|
104
|
+
if (elementType.displayName) return elementType.displayName;
|
|
105
|
+
if (elementType.name) return elementType.name;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function isDevToolsComponent(fiber, stateNode) {
|
|
110
|
+
const nativeTag = getNativeTag(stateNode);
|
|
111
|
+
if (nativeTag != null) {
|
|
112
|
+
const cached = devToolsNodeCache.get(nativeTag);
|
|
113
|
+
if (cached !== undefined) {
|
|
114
|
+
return cached;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let result = false;
|
|
118
|
+
const directNativeID = fiber.pendingProps?.nativeID || fiber.memoizedProps?.nativeID;
|
|
119
|
+
if (isDevToolsNativeID(directNativeID)) {
|
|
120
|
+
result = true;
|
|
121
|
+
} else {
|
|
122
|
+
let currentFiber = fiber;
|
|
123
|
+
let depth = 0;
|
|
124
|
+
while (currentFiber && depth < 30) {
|
|
125
|
+
const name = getComponentName(currentFiber);
|
|
126
|
+
if (name) {
|
|
127
|
+
if (DEV_TOOLS_COMPONENT_NAMES.has(name)) {
|
|
128
|
+
result = true;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
for (const prefix of DEV_TOOLS_COMPONENT_PREFIXES) {
|
|
132
|
+
if (name.startsWith(prefix)) {
|
|
133
|
+
result = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (result) break;
|
|
138
|
+
}
|
|
139
|
+
const nativeID = currentFiber.pendingProps?.nativeID || currentFiber.memoizedProps?.nativeID;
|
|
140
|
+
if (isDevToolsNativeID(nativeID)) {
|
|
141
|
+
result = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
currentFiber = currentFiber.return;
|
|
145
|
+
depth++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (result && nativeTag != null) {
|
|
149
|
+
if (devToolsNodeCache.size >= CACHE_MAX_SIZE) {
|
|
150
|
+
const entries = Array.from(devToolsNodeCache.keys());
|
|
151
|
+
for (let i = 0; i < CACHE_MAX_SIZE / 2; i++) {
|
|
152
|
+
devToolsNodeCache.delete(entries[i]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
devToolsNodeCache.set(nativeTag, result);
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
function clearDevToolsCache() {
|
|
160
|
+
devToolsNodeCache.clear();
|
|
161
|
+
}
|
|
162
|
+
function isHiddenOffscreen(fiber) {
|
|
163
|
+
if (fiber.tag !== FiberTags.OffscreenComponent) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return fiber.memoizedState !== null;
|
|
167
|
+
}
|
|
168
|
+
function isInactiveScreen(fiber) {
|
|
169
|
+
const props = fiber.memoizedProps;
|
|
170
|
+
if (!props) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
return props.activityState === 0;
|
|
174
|
+
}
|
|
175
|
+
function getNativeViewClassName(stateNode) {
|
|
176
|
+
if (!stateNode) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (stateNode.canonical?.viewConfig?.uiViewClassName) {
|
|
180
|
+
return stateNode.canonical.viewConfig.uiViewClassName;
|
|
181
|
+
}
|
|
182
|
+
if (stateNode.viewConfig?.uiViewClassName) {
|
|
183
|
+
return stateNode.viewConfig.uiViewClassName;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
function isSVGComponent(stateNode) {
|
|
188
|
+
const viewClassName = getNativeViewClassName(stateNode);
|
|
189
|
+
if (!viewClassName) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return viewClassName.startsWith("RNSVG");
|
|
193
|
+
}
|
|
194
|
+
function traverseFiberTree(fiber, callback, depth = 0, visited = new Set()) {
|
|
195
|
+
if (!fiber) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (visited.has(fiber)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
visited.add(fiber);
|
|
202
|
+
if (depth > 500) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (isHiddenOffscreen(fiber)) {
|
|
206
|
+
if (fiber.sibling) {
|
|
207
|
+
traverseFiberTree(fiber.sibling, callback, depth, visited);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (isInactiveScreen(fiber)) {
|
|
212
|
+
if (fiber.sibling) {
|
|
213
|
+
traverseFiberTree(fiber.sibling, callback, depth, visited);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
callback(fiber, depth);
|
|
218
|
+
if (fiber.child) {
|
|
219
|
+
traverseFiberTree(fiber.child, callback, depth + 1, visited);
|
|
220
|
+
}
|
|
221
|
+
if (fiber.sibling) {
|
|
222
|
+
traverseFiberTree(fiber.sibling, callback, depth, visited);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function getAllHostComponentInstances() {
|
|
226
|
+
const roots = getFiberRoots();
|
|
227
|
+
if (roots.length === 0) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const instances = [];
|
|
231
|
+
roots.forEach((root, rootIndex) => {
|
|
232
|
+
traverseFiberTree(root.current, (fiber, depth) => {
|
|
233
|
+
if (fiber.tag === FiberTags.HostComponent) {
|
|
234
|
+
const publicInstance = fiber.stateNode;
|
|
235
|
+
if (isDevToolsComponent(fiber, publicInstance)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (isSVGComponent(publicInstance)) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (publicInstance) {
|
|
242
|
+
instances.push({
|
|
243
|
+
instance: publicInstance,
|
|
244
|
+
fiber: fiber,
|
|
245
|
+
depth: depth
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
return instances;
|
|
252
|
+
}
|
|
253
|
+
function isReactDevToolsAvailable() {
|
|
254
|
+
const hook = getReactDevToolsHook();
|
|
255
|
+
if (!hook) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (!hook.getFiberRoots) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
function getReactDevToolsDiagnostics() {
|
|
264
|
+
const hook = getReactDevToolsHook();
|
|
265
|
+
if (!hook) {
|
|
266
|
+
return {
|
|
267
|
+
available: false,
|
|
268
|
+
reason: "Hook not found on global object"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const diagnostics = {
|
|
272
|
+
available: true,
|
|
273
|
+
hasAgent: !!hook.reactDevtoolsAgent,
|
|
274
|
+
hasFiberRoots: typeof hook.getFiberRoots === "function",
|
|
275
|
+
rendererCount: 0,
|
|
276
|
+
rootCount: 0,
|
|
277
|
+
hookKeys: Object.keys(hook)
|
|
278
|
+
};
|
|
279
|
+
if (hook.getFiberRoots) {
|
|
280
|
+
try {
|
|
281
|
+
const roots = hook.getFiberRoots(1);
|
|
282
|
+
if (roots) {
|
|
283
|
+
diagnostics.rootCount = roots.size;
|
|
284
|
+
}
|
|
285
|
+
} catch (e) {
|
|
286
|
+
diagnostics.error = e.message;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return diagnostics;
|
|
290
|
+
}
|
|
291
|
+
module.exports = {
|
|
292
|
+
getReactDevToolsHook,
|
|
293
|
+
getFiberRoots,
|
|
294
|
+
traverseFiberTree,
|
|
295
|
+
getAllHostComponentInstances,
|
|
296
|
+
isReactDevToolsAvailable,
|
|
297
|
+
getReactDevToolsDiagnostics,
|
|
298
|
+
isHiddenOffscreen,
|
|
299
|
+
isInactiveScreen,
|
|
300
|
+
isSVGComponent,
|
|
301
|
+
getNativeViewClassName,
|
|
302
|
+
isDevToolsComponent,
|
|
303
|
+
isDevToolsNativeID,
|
|
304
|
+
getComponentName,
|
|
305
|
+
clearDevToolsCache,
|
|
306
|
+
FiberTags,
|
|
307
|
+
DEV_TOOLS_COMPONENT_NAMES,
|
|
308
|
+
DEV_TOOLS_NATIVE_IDS
|
|
309
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label Positioning Utility
|
|
3
|
+
*
|
|
4
|
+
* Resolves overlapping labels by repositioning them.
|
|
5
|
+
* Uses a greedy algorithm that processes labels once and offsets
|
|
6
|
+
* any that would overlap with already-placed labels.
|
|
7
|
+
*
|
|
8
|
+
* Time Complexity: O(n²) worst case, but typically much faster
|
|
9
|
+
* since we only compare against nearby labels and use early exit.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for label positioning
|
|
16
|
+
*/
|
|
17
|
+
const CONFIG = {
|
|
18
|
+
// Minimum spacing between labels (pixels)
|
|
19
|
+
LABEL_SPACING: 0,
|
|
20
|
+
// Maximum vertical offset before giving up (pixels)
|
|
21
|
+
MAX_VERTICAL_OFFSET: 100,
|
|
22
|
+
// Label height estimate (used for offset increments)
|
|
23
|
+
LABEL_HEIGHT: 10,
|
|
24
|
+
// Padding around labels for collision detection
|
|
25
|
+
PADDING: 0
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if two rectangles overlap
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} a - First rectangle {x, y, width, height}
|
|
32
|
+
* @param {Object} b - Second rectangle {x, y, width, height}
|
|
33
|
+
* @returns {boolean} - True if rectangles overlap
|
|
34
|
+
*/
|
|
35
|
+
function rectsOverlap(a, b) {
|
|
36
|
+
const padding = CONFIG.PADDING;
|
|
37
|
+
const aLeft = a.x - padding;
|
|
38
|
+
const aRight = a.x + a.width + padding;
|
|
39
|
+
const aTop = a.y - padding;
|
|
40
|
+
const aBottom = a.y + a.height + padding;
|
|
41
|
+
const bLeft = b.x - padding;
|
|
42
|
+
const bRight = b.x + b.width + padding;
|
|
43
|
+
const bTop = b.y - padding;
|
|
44
|
+
const bBottom = b.y + b.height + padding;
|
|
45
|
+
|
|
46
|
+
// No overlap if one is completely to the side or above/below the other
|
|
47
|
+
return !(aRight < bLeft || aLeft > bRight || aBottom < bTop || aTop > bBottom);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Estimate label width based on text length
|
|
52
|
+
* (Used when actual measurement isn't available)
|
|
53
|
+
*
|
|
54
|
+
* @param {string} text - Label text
|
|
55
|
+
* @returns {number} - Estimated width in pixels
|
|
56
|
+
*/
|
|
57
|
+
function estimateLabelWidth(text) {
|
|
58
|
+
if (!text) return 0;
|
|
59
|
+
// Approximate: 5px per character for 8pt monospace font + padding
|
|
60
|
+
return text.length * 5 + 8;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve overlapping labels by stacking them upward like a menu
|
|
65
|
+
*
|
|
66
|
+
* Strategy: Process labels in order. For each label, check if it overlaps
|
|
67
|
+
* with any already-placed label. If so, stack it above (going upward).
|
|
68
|
+
* Labels are positioned above their boxes, stacking upward with no gaps.
|
|
69
|
+
*
|
|
70
|
+
* @param {Array<Object>} rectangles - Array of rectangle data with componentInfo
|
|
71
|
+
* @returns {Array<Object>} - Same array with added labelOffset property
|
|
72
|
+
*/
|
|
73
|
+
function resolveOverlappingLabels(rectangles) {
|
|
74
|
+
if (!rectangles || rectangles.length === 0) {
|
|
75
|
+
return rectangles;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Filter to only rectangles with valid labels (testID or accessibilityLabel)
|
|
79
|
+
const validRectangles = rectangles.filter(rect => {
|
|
80
|
+
const info = rect.componentInfo;
|
|
81
|
+
return info && (info.testID || info.accessibilityLabel);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Build label rects with estimated dimensions
|
|
85
|
+
// Labels are positioned ABOVE the box (y - labelHeight)
|
|
86
|
+
const labelRects = rectangles.map((rect, index) => {
|
|
87
|
+
const info = rect.componentInfo;
|
|
88
|
+
const hasValidLabel = info && (info.testID || info.accessibilityLabel);
|
|
89
|
+
const labelText = hasValidLabel ? info.primaryLabel : "";
|
|
90
|
+
const labelWidth = estimateLabelWidth(labelText);
|
|
91
|
+
return {
|
|
92
|
+
index,
|
|
93
|
+
hasValidLabel,
|
|
94
|
+
// Label position is above the box
|
|
95
|
+
x: rect.x,
|
|
96
|
+
y: rect.y - CONFIG.LABEL_HEIGHT,
|
|
97
|
+
// Position above the box
|
|
98
|
+
width: labelWidth,
|
|
99
|
+
height: CONFIG.LABEL_HEIGHT,
|
|
100
|
+
// Track the offset we apply (going upward, so negative)
|
|
101
|
+
offsetY: 0
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Sort by position (top-to-bottom, left-to-right) for consistent placement
|
|
106
|
+
const sortedIndices = labelRects.map((_, i) => i).filter(i => labelRects[i].hasValidLabel) // Only process valid labels
|
|
107
|
+
.sort((a, b) => {
|
|
108
|
+
const rectA = rectangles[a];
|
|
109
|
+
const rectB = rectangles[b];
|
|
110
|
+
// Primary sort by Y, secondary by X
|
|
111
|
+
if (Math.abs(rectA.y - rectB.y) > 10) {
|
|
112
|
+
return rectA.y - rectB.y;
|
|
113
|
+
}
|
|
114
|
+
return rectA.x - rectB.x;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Track placed labels for collision detection
|
|
118
|
+
const placedLabels = [];
|
|
119
|
+
|
|
120
|
+
// Process each label in sorted order
|
|
121
|
+
for (const idx of sortedIndices) {
|
|
122
|
+
const label = labelRects[idx];
|
|
123
|
+
|
|
124
|
+
// Skip empty labels
|
|
125
|
+
if (label.width === 0) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let offsetY = 0;
|
|
129
|
+
let hasOverlap = true;
|
|
130
|
+
let attempts = 0;
|
|
131
|
+
const maxAttempts = Math.ceil(CONFIG.MAX_VERTICAL_OFFSET / CONFIG.LABEL_HEIGHT);
|
|
132
|
+
while (hasOverlap && attempts < maxAttempts) {
|
|
133
|
+
// Create test rect with current offset (going upward)
|
|
134
|
+
const testRect = {
|
|
135
|
+
x: label.x,
|
|
136
|
+
y: label.y - offsetY,
|
|
137
|
+
// Subtract to go upward
|
|
138
|
+
width: label.width,
|
|
139
|
+
height: label.height
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Check against all placed labels
|
|
143
|
+
hasOverlap = placedLabels.some(placed => rectsOverlap(testRect, placed));
|
|
144
|
+
if (hasOverlap) {
|
|
145
|
+
// Stack upward with no spacing
|
|
146
|
+
offsetY += CONFIG.LABEL_HEIGHT;
|
|
147
|
+
attempts++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Store the offset
|
|
152
|
+
label.offsetY = offsetY;
|
|
153
|
+
|
|
154
|
+
// Add to placed labels with final position
|
|
155
|
+
placedLabels.push({
|
|
156
|
+
x: label.x,
|
|
157
|
+
y: label.y - offsetY,
|
|
158
|
+
width: label.width,
|
|
159
|
+
height: label.height
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Apply offsets to original rectangles
|
|
164
|
+
return rectangles.map((rect, index) => ({
|
|
165
|
+
...rect,
|
|
166
|
+
labelOffsetY: labelRects[index].offsetY
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Quick check if any labels might overlap (for early exit optimization)
|
|
172
|
+
*
|
|
173
|
+
* @param {Array<Object>} rectangles - Array of rectangle data
|
|
174
|
+
* @returns {boolean} - True if there might be overlaps worth resolving
|
|
175
|
+
*/
|
|
176
|
+
function mightHaveOverlaps(rectangles) {
|
|
177
|
+
if (!rectangles || rectangles.length < 2) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Quick heuristic: check if any boxes are close together
|
|
182
|
+
for (let i = 0; i < Math.min(rectangles.length, 20); i++) {
|
|
183
|
+
for (let j = i + 1; j < Math.min(rectangles.length, 20); j++) {
|
|
184
|
+
const a = rectangles[i];
|
|
185
|
+
const b = rectangles[j];
|
|
186
|
+
|
|
187
|
+
// If boxes are within label height of each other vertically
|
|
188
|
+
// and overlap horizontally, we might have label overlaps
|
|
189
|
+
if (Math.abs(a.y - b.y) < CONFIG.LABEL_HEIGHT * 2 && !(a.x + 100 < b.x || b.x + 100 < a.x)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
module.exports = {
|
|
197
|
+
resolveOverlappingLabels,
|
|
198
|
+
rectsOverlap,
|
|
199
|
+
estimateLabelWidth,
|
|
200
|
+
mightHaveOverlaps,
|
|
201
|
+
CONFIG
|
|
202
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pre-configured debug borders tool for FloatingDevTools
|
|
5
|
+
*
|
|
6
|
+
* This preset provides a zero-config way to add visual layout debugging to your dev tools.
|
|
7
|
+
* Just import and add it to your apps array! Tap the icon to cycle through modes:
|
|
8
|
+
* - Off (gray icon)
|
|
9
|
+
* - Borders only (green icon)
|
|
10
|
+
* - Borders + Labels (cyan icon)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { debugBordersToolPreset } from '@buoy-gg/debug-borders';
|
|
15
|
+
*
|
|
16
|
+
* const installedApps = [
|
|
17
|
+
* debugBordersToolPreset, // That's it!
|
|
18
|
+
* // ...other tools
|
|
19
|
+
* ];
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React, { useState, useEffect } from "react";
|
|
24
|
+
import { Layers } from "@buoy-gg/shared-ui";
|
|
25
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
26
|
+
const DebugBordersManager = require("./debug-borders/utils/DebugBordersManager");
|
|
27
|
+
/**
|
|
28
|
+
* Mode colors for the icon
|
|
29
|
+
* - off: gray (disabled)
|
|
30
|
+
* - borders: green (enabled, borders only)
|
|
31
|
+
* - labels: cyan (enabled, with labels)
|
|
32
|
+
*/
|
|
33
|
+
const MODE_COLORS = {
|
|
34
|
+
off: "#6b7280",
|
|
35
|
+
// Gray
|
|
36
|
+
borders: "#10b981",
|
|
37
|
+
// Green
|
|
38
|
+
labels: "#06b6d4" // Cyan
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Icon component that changes color based on display mode.
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ IMPORTANT - DO NOT MODIFY THIS COMPONENT ⚠️
|
|
45
|
+
* This component MUST use useState and useEffect hooks to subscribe to the manager.
|
|
46
|
+
* It is rendered as a JSX component (<IconComponent />) in FloatingMenu and DialIcon,
|
|
47
|
+
* which allows hooks to work properly.
|
|
48
|
+
*
|
|
49
|
+
* If you remove the hooks or change this to read getMode() directly,
|
|
50
|
+
* the icon color will NOT update when the toggle is pressed.
|
|
51
|
+
*/
|
|
52
|
+
function BordersIcon({
|
|
53
|
+
size
|
|
54
|
+
}) {
|
|
55
|
+
const [mode, setMode] = useState(() => DebugBordersManager.getMode());
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const unsubscribe = DebugBordersManager.subscribe(newMode => {
|
|
58
|
+
setMode(newMode);
|
|
59
|
+
});
|
|
60
|
+
return unsubscribe;
|
|
61
|
+
}, []);
|
|
62
|
+
return /*#__PURE__*/_jsx(Layers, {
|
|
63
|
+
size: size,
|
|
64
|
+
color: MODE_COLORS[mode]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Empty component for toggle-only tools (no modal needed)
|
|
70
|
+
*/
|
|
71
|
+
function EmptyComponent() {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pre-configured debug borders tool for FloatingDevTools.
|
|
77
|
+
* Tap the icon to cycle through modes: Off → Borders → Labels → Off
|
|
78
|
+
*
|
|
79
|
+
* Features:
|
|
80
|
+
* - Visual layout debugging with colored borders
|
|
81
|
+
* - Optional component labels showing testID, nativeID, component name, etc.
|
|
82
|
+
* - Automatic component tracking
|
|
83
|
+
* - Real-time updates every 2 seconds
|
|
84
|
+
* - Icon changes color: gray (off), green (borders), cyan (labels)
|
|
85
|
+
*/
|
|
86
|
+
export const debugBordersToolPreset = {
|
|
87
|
+
id: "debug-borders",
|
|
88
|
+
name: "BORDERS",
|
|
89
|
+
description: "Visual layout debugger - tap to cycle modes",
|
|
90
|
+
slot: "menu",
|
|
91
|
+
icon: BordersIcon,
|
|
92
|
+
component: EmptyComponent,
|
|
93
|
+
props: {},
|
|
94
|
+
launchMode: "toggle-only",
|
|
95
|
+
onPress: () => {
|
|
96
|
+
DebugBordersManager.cycle();
|
|
97
|
+
// Icon updates automatically via subscription in BordersIcon component
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a custom debug borders tool configuration.
|
|
103
|
+
* Use this if you want to override default settings.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* import { createDebugBordersTool } from '@buoy-gg/debug-borders';
|
|
108
|
+
*
|
|
109
|
+
* const myBordersTool = createDebugBordersTool({
|
|
110
|
+
* name: "LAYOUT",
|
|
111
|
+
* offColor: "#9ca3af",
|
|
112
|
+
* bordersColor: "#ec4899",
|
|
113
|
+
* labelsColor: "#8b5cf6",
|
|
114
|
+
* });
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export function createDebugBordersTool(options) {
|
|
118
|
+
const colors = {
|
|
119
|
+
off: options?.offColor || "#6b7280",
|
|
120
|
+
borders: options?.bordersColor || "#10b981",
|
|
121
|
+
labels: options?.labelsColor || "#06b6d4"
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Custom icon component with hooks - rendered as JSX component.
|
|
126
|
+
*
|
|
127
|
+
* ⚠️ IMPORTANT - DO NOT MODIFY THIS COMPONENT ⚠️
|
|
128
|
+
* This component MUST use useState and useEffect hooks to subscribe to the manager.
|
|
129
|
+
* See the comment on BordersIcon above for full explanation.
|
|
130
|
+
*/
|
|
131
|
+
const CustomBordersIcon = ({
|
|
132
|
+
size
|
|
133
|
+
}) => {
|
|
134
|
+
const [mode, setMode] = useState(() => DebugBordersManager.getMode());
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const unsubscribe = DebugBordersManager.subscribe(newMode => {
|
|
137
|
+
setMode(newMode);
|
|
138
|
+
});
|
|
139
|
+
return unsubscribe;
|
|
140
|
+
}, []);
|
|
141
|
+
return /*#__PURE__*/_jsx(Layers, {
|
|
142
|
+
size: size,
|
|
143
|
+
color: colors[mode]
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
id: options?.id || "debug-borders",
|
|
148
|
+
name: options?.name || "BORDERS",
|
|
149
|
+
description: options?.description || "Visual layout debugger - tap to cycle modes",
|
|
150
|
+
slot: "menu",
|
|
151
|
+
icon: CustomBordersIcon,
|
|
152
|
+
component: EmptyComponent,
|
|
153
|
+
props: {},
|
|
154
|
+
launchMode: "toggle-only",
|
|
155
|
+
onPress: () => {
|
|
156
|
+
DebugBordersManager.cycle();
|
|
157
|
+
// Icon updates automatically via subscription
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Export the standalone overlay for manual integration
|
|
164
|
+
* Use this if you want to control debug borders outside of FloatingDevTools
|
|
165
|
+
*/
|
|
166
|
+
export { DebugBordersStandaloneOverlay } from "./debug-borders/components/DebugBordersStandaloneOverlay";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { DebugBordersModalProps } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Modal component for controlling debug borders.
|
|
5
|
+
* This allows developers to toggle visual layout debugging borders on/off.
|
|
6
|
+
*/
|
|
7
|
+
export declare function DebugBordersModal({ visible, onClose }: DebugBordersModalProps & {
|
|
8
|
+
visible: boolean;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}): React.JSX.Element | null;
|
|
11
|
+
//# sourceMappingURL=DebugBordersModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DebugBordersModal.d.ts","sourceRoot":"","sources":["../../../../src/debug-borders/components/DebugBordersModal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAIvD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,sBAAsB,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,4BAiGzH"}
|