@atlassian/chippy 0.0.0-stage → 0.0.1-beta.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 +201 -0
- package/README.md +188 -2
- package/bin/chippy.mjs +134 -0
- package/bin/pack.mjs +184 -0
- package/lib/avalanche.js +2570 -0
- package/lib/avalanche.js.map +7 -0
- package/lib/bootstrap.d.ts +1 -0
- package/lib/bootstrap.js +51 -0
- package/lib/bootstrap.js.map +7 -0
- package/lib/browser.js +15 -0
- package/lib/browser.js.map +7 -0
- package/lib/extension/avalanche.es.js +5396 -0
- package/lib/extension/avalanche.es.js.map +1 -0
- package/lib/extension/chippy-page-bridge.js +1833 -0
- package/lib/extension/inspect-react.js +216 -0
- package/lib/extension/layout-lab.js +1453 -0
- package/lib/extension/panel-dom.js +90 -0
- package/lib/extension/resolve-probes.js +342 -0
- package/lib/mcp-skill/LICENSE +201 -0
- package/lib/mcp-skill/README.md +150 -0
- package/lib/mcp-skill/SKILL.md +330 -0
- package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
- package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
- package/lib/mcp-skill/extension/avalanche.es.js +5396 -0
- package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
- package/lib/mcp-skill/extension/avalanche.iife.js +5397 -0
- package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
- package/lib/mcp-skill/extension/background.js +323 -0
- package/lib/mcp-skill/extension/chippy-page-bridge.js +1833 -0
- package/lib/mcp-skill/extension/content-script.js +167 -0
- package/lib/mcp-skill/extension/devtools-hook-shim.js +68 -0
- package/lib/mcp-skill/extension/early-bootstrap.js +47 -0
- package/lib/mcp-skill/extension/icon-128.png +0 -0
- package/lib/mcp-skill/extension/icon-16.png +0 -0
- package/lib/mcp-skill/extension/icon-32.png +0 -0
- package/lib/mcp-skill/extension/icon-48.png +0 -0
- package/lib/mcp-skill/extension/inspect-react.js +216 -0
- package/lib/mcp-skill/extension/layout-lab.js +1453 -0
- package/lib/mcp-skill/extension/logo.svg +14 -0
- package/lib/mcp-skill/extension/manifest.json +69 -0
- package/lib/mcp-skill/extension/panel-dom.js +90 -0
- package/lib/mcp-skill/extension/panel-settings.js +191 -0
- package/lib/mcp-skill/extension/popup.html +41 -0
- package/lib/mcp-skill/extension/popup.js +80 -0
- package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
- package/lib/mcp-skill/extension/resolve-probes.js +342 -0
- package/lib/mcp-skill/package.json +1 -0
- package/lib/mcp-skill/references/README.md +56 -0
- package/lib/mcp-skill/references/active-stimulus.md +85 -0
- package/lib/mcp-skill/references/avalanche-detector.md +731 -0
- package/lib/mcp-skill/references/bdd-router.md +310 -0
- package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
- package/lib/mcp-skill/references/command-output-formats.md +1075 -0
- package/lib/mcp-skill/references/command-surface.md +183 -0
- package/lib/mcp-skill/references/cynefin.md +99 -0
- package/lib/mcp-skill/references/debug-hook.md +161 -0
- package/lib/mcp-skill/references/fixing-performance.md +490 -0
- package/lib/mcp-skill/references/hook-metrics.md +199 -0
- package/lib/mcp-skill/references/hooks-inspection.md +61 -0
- package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
- package/lib/mcp-skill/references/mcp.md +122 -0
- package/lib/mcp-skill/references/mode-a-injection.md +68 -0
- package/lib/mcp-skill/references/operating-model.md +128 -0
- package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
- package/lib/mcp-skill/references/playwright-probes.md +205 -0
- package/lib/mcp-skill/references/procedures.md +545 -0
- package/lib/mcp-skill/references/recipes.txt +114 -0
- package/lib/mcp-skill/references/stability-render.md +428 -0
- package/lib/mcp-skill/references/test-mode-playbook.md +193 -0
- package/lib/mcp-skill/references/troubleshooting.md +54 -0
- package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
- package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
- package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
- package/lib/mcp-skill/scripts/chippy-helper-server.js +1710 -0
- package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
- package/lib/mcp-skill/scripts/chippy.sh +1218 -0
- package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
- package/lib/mcp-skill/scripts/helper-access.js +144 -0
- package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
- package/lib/mcp-skill/scripts/search-recipes.js +248 -0
- package/lib/mcp-skill/snippets/README.md +89 -0
- package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
- package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
- package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
- package/lib/mcp-skill/snippets/find-prop.js +83 -0
- package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
- package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
- package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
- package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
- package/lib/package.json +1 -0
- package/lib/plugin.js +119 -0
- package/lib/plugin.js.map +7 -0
- package/lib/storybook/avalanche-panel-model.js +79 -0
- package/lib/storybook/brand.jsx +6 -0
- package/lib/storybook/connection-fallback.jsx +47 -0
- package/lib/storybook/constants.js +15 -0
- package/lib/storybook/context-card.jsx +171 -0
- package/lib/storybook/manager.jsx +403 -0
- package/lib/storybook/preset.js +7 -0
- package/lib/storybook/preview.js +263 -0
- package/lib/storybook/probe-annotation-editor.jsx +52 -0
- package/lib/storybook/probe-intent.js +23 -0
- package/lib/storybook/setup.js +93 -0
- package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
- package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
- package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
- package/lib/types/avalanche/detector.d.ts +183 -0
- package/lib/types/avalanche/interaction-capture.d.ts +25 -0
- package/lib/types/avalanche/internals.d.ts +189 -0
- package/lib/types/avalanche/report-serializer.d.ts +283 -0
- package/lib/types/avalanche/reporter.d.ts +610 -0
- package/lib/types/chippy-bridge/fiber-source.d.ts +37 -0
- package/lib/types/chippy-bridge/react-renderer.d.ts +49 -0
- package/lib/types/chippy-bridge/source-map.d.ts +30 -0
- package/lib/types/debug-render/core.d.ts +72 -0
- package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
- package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
- package/lib/types/layout-lab/marker-context.d.ts +94 -0
- package/lib/types/package/avalanche.d.ts +6 -0
- package/lib/types/package/browser.d.ts +10 -0
- package/lib/types/package/plugin.d.ts +15 -0
- package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
- package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
- package/lib/types/plugins/index.d.ts +49 -0
- package/lib/types/plugins/jira/context-provider.d.ts +36 -0
- package/package.json +104 -4
|
@@ -0,0 +1,1453 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
(function installLayoutLab(global) {
|
|
3
|
+
const chippy = global.__CHIPPY = global.__CHIPPY || {};
|
|
4
|
+
// Always reinstall — guards caused stale code to win after extension reload.
|
|
5
|
+
|
|
6
|
+
// p1 = red, p2 = green. No p3.
|
|
7
|
+
const MARKER_IDS = ['1', '2'];
|
|
8
|
+
const CHIPPY_LOGO = "<svg data-chippy-logo aria-hidden=\"true\" width=\"24\" height=\"24\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\" fill=\"none\">\n <defs>\n <linearGradient id=\"chippy-trace-blue\" x1=\"4\" y1=\"25\" x2=\"28\" y2=\"7\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#1558BC\"/><stop offset=\"1\" stop-color=\"#357DE8\"/></linearGradient>\n <linearGradient id=\"chippy-trace-light\" x1=\"4\" y1=\"9\" x2=\"27\" y2=\"9\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#85B8FF\"/><stop offset=\"1\" stop-color=\"#357DE8\"/></linearGradient>\n </defs>\n <style>\n @keyframes chippy-trace-reveal { from { opacity: .45; transform: translateY(-1.5px); } to { opacity: 1; transform: translateY(0); } }\n .chippy-trace-cut { animation: chippy-trace-reveal 650ms ease-out both; }\n @media (prefers-reduced-motion: reduce) { .chippy-trace-cut { animation: none; } }\n </style>\n <path d=\"M4 9 16 1.5 28 9 21 13.4 16 10.3 11 13.4v5.2l5 3.1 5-3.1 7 4.4L16 30.5 4 23Z\" fill=\"url(#chippy-trace-blue)\"/>\n <path class=\"chippy-trace-cut\" d=\"M4 9 16 1.5 28 9 21 13.4 16 10.3 11 13.4Z\" fill=\"url(#chippy-trace-light)\"/>\n <path d=\"m11 18.6 5 3.1 5-3.1 7 4.4-12 7.5-5-3.1Z\" fill=\"#1868DB\"/>\n</svg>";
|
|
9
|
+
const MARKER_COLORS = { '1': '#F87168', '2': '#4BCE97' };
|
|
10
|
+
const MARKER_SIZE = 36;
|
|
11
|
+
const DOCK_GAP = 12;
|
|
12
|
+
const PANEL_WIDTH = 340;
|
|
13
|
+
const PANEL_LAUNCHER_SIZE = 48;
|
|
14
|
+
// How close (px) to dock centre to snap back
|
|
15
|
+
const DOCK_SNAP_RADIUS = 80;
|
|
16
|
+
|
|
17
|
+
function emitLabEvent(reason, detail = {}) {
|
|
18
|
+
global.dispatchEvent(
|
|
19
|
+
new CustomEvent('layout-lab:update', {
|
|
20
|
+
detail: {
|
|
21
|
+
reason,
|
|
22
|
+
...detail,
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── React fiber helpers ──────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
// ─── Fiber-walk shims — implementations live in src/layout-lab/fiber-walk.ts
|
|
31
|
+
// bundled into __AVALANCHE__.layoutLab. Fallbacks are intentionally minimal:
|
|
32
|
+
// if the bundle is missing, the lab degrades gracefully (no probes resolve)
|
|
33
|
+
// rather than carrying a duplicated fiber-walk.
|
|
34
|
+
function getLab() { return (window.__AVALANCHE__ && window.__AVALANCHE__.layoutLab) || null; }
|
|
35
|
+
function getFiberKey(node) { const l = getLab(); return l ? l.getFiberKey(node) : null; }
|
|
36
|
+
function getFiber(node) { const l = getLab(); return l ? l.getFiber(node) : null; }
|
|
37
|
+
function getDisplayName(type) { const l = getLab(); return l ? l.getDisplayName(type) : null; }
|
|
38
|
+
function isUserComponent(fiber) { const l = getLab(); return l ? l.isUserComponent(fiber) : false; }
|
|
39
|
+
function getNearestFiber(domNode) { const l = getLab(); return l ? l.getNearestFiber(domNode) : null; }
|
|
40
|
+
function getNearestComponent(domNode) { const l = getLab(); return l ? l.getNearestComponent(domNode) : null; }
|
|
41
|
+
|
|
42
|
+
function getHookSummary(fiber) { const l = getLab(); return l ? l.getHookSummary(fiber) : { hookCount: 0, hookCountTruncated: false, hookCountMax: 500 }; }
|
|
43
|
+
function getOwnerChain(fiber) { const l = getLab(); return l ? l.getOwnerChain(fiber) : []; }
|
|
44
|
+
function getOwnerFibers(fiber) { const l = getLab(); return l ? l.getOwnerFibers(fiber) : []; }
|
|
45
|
+
function getFiberDomNode(fiber) { const l = getLab(); return l ? l.getFiberDomNode(fiber) : null; }
|
|
46
|
+
function getReturnComponentChain(fiber) { const l = getLab(); return l ? l.getReturnComponentChain(fiber) : []; }
|
|
47
|
+
function getReturnComponentFibers(fiber) { const l = getLab(); return l ? l.getReturnComponentFibers(fiber) : []; }
|
|
48
|
+
function describeSource(source) { const l = getLab(); return l ? l.describeSource(source) : null; }
|
|
49
|
+
function describeFiberTarget(fiber, domNode) { const l = getLab(); return l ? l.describeFiberTarget(fiber, domNode) : null; }
|
|
50
|
+
function describeComponent(fiber) { const l = getLab(); return l ? l.describeComponent(fiber) : null; }
|
|
51
|
+
function describeContext(fiber) {
|
|
52
|
+
const l = getLab();
|
|
53
|
+
const describe = l?.describeContext;
|
|
54
|
+
return typeof describe === 'function' ? describe(fiber) : null;
|
|
55
|
+
}
|
|
56
|
+
function findBoundaryFibers(fiber) { const l = getLab(); return typeof l?.findBoundaryFibers === 'function' ? l.findBoundaryFibers(fiber) : { error: null, suspense: null }; }
|
|
57
|
+
function normalizeProbeAnnotation(value) {
|
|
58
|
+
const l = getLab();
|
|
59
|
+
if (typeof l?.normalizeProbeAnnotation === 'function') return l.normalizeProbeAnnotation(value);
|
|
60
|
+
const intent = ['explain', 'investigate', 'change'].includes(value?.intent) ? value.intent : null;
|
|
61
|
+
return { intent, comment: typeof value?.comment === 'string' ? value.comment.slice(0, 2000) : '' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Overlay helpers ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
// All Chippy UI elements carry data-chippy-ui so they can be filtered out uniformly
|
|
67
|
+
function isOverlay(node) {
|
|
68
|
+
return Boolean(node?.closest?.('[data-chippy-ui]'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hideOverlayWhile(fn) {
|
|
72
|
+
const els = document.querySelectorAll('[data-chippy-ui]');
|
|
73
|
+
els.forEach((el) => { el.style.visibility = 'hidden'; el.style.pointerEvents = 'none'; });
|
|
74
|
+
const result = fn();
|
|
75
|
+
els.forEach((el) => { el.style.visibility = ''; el.style.pointerEvents = ''; });
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ownsProbeDockUi() {
|
|
80
|
+
const ui = chippy.config?.ui || window.__CHIPPY_OWNER__?.ui || null;
|
|
81
|
+
return ui !== 'none';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ownsProbePanelUi() {
|
|
85
|
+
const ui = chippy.config?.ui || window.__CHIPPY_OWNER__?.ui || null;
|
|
86
|
+
return ui !== 'storybook-panel' && ui !== 'none';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function removeInPageUi() {
|
|
90
|
+
// Storybook addon owns visualization, so remove only the in-page panel.
|
|
91
|
+
// Keep its standalone probe dock/markers in the preview frame.
|
|
92
|
+
document.querySelector('[data-layout-lab-panel]')?.remove();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderDockSlots() {
|
|
96
|
+
return MARKER_IDS.map((id) => {
|
|
97
|
+
const color = MARKER_COLORS[id] || '#A9ABAF';
|
|
98
|
+
return `<div style="
|
|
99
|
+
width:${MARKER_SIZE}px;height:${MARKER_SIZE}px;border-radius:50%;
|
|
100
|
+
border:2px dashed ${color};opacity:0.35;
|
|
101
|
+
display:flex;align-items:center;justify-content:center;
|
|
102
|
+
color:${color};font-size:12px;font-weight:700;
|
|
103
|
+
">${id}</div>`;
|
|
104
|
+
}).join('');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ensureDock() {
|
|
108
|
+
if (!ownsProbeDockUi()) {
|
|
109
|
+
document.querySelector('[data-layout-lab-dock]')?.remove();
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// In-page panel owners use one unified surface. Their dock is rendered
|
|
114
|
+
// inside the expanded panel, so the collapsed state is genuinely one small
|
|
115
|
+
// launcher instead of a launcher plus a second floating probe tray.
|
|
116
|
+
if (ownsProbePanelUi()) {
|
|
117
|
+
const dock = document.querySelector('[data-layout-lab-panel] [data-layout-lab-dock]');
|
|
118
|
+
document.querySelectorAll('body > [data-layout-lab-dock]').forEach((node) => node.remove());
|
|
119
|
+
return dock;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Storybook renders probe details in the manager frame. It still needs a
|
|
123
|
+
// standalone dock inside the preview frame where the markers can be dragged.
|
|
124
|
+
let dock = document.querySelector('[data-layout-lab-dock]');
|
|
125
|
+
if (!dock) {
|
|
126
|
+
dock = document.createElement('div');
|
|
127
|
+
dock.setAttribute('data-layout-lab-dock', 'true');
|
|
128
|
+
dock.setAttribute('data-chippy-ui', 'true');
|
|
129
|
+
dock.style.cssText = `
|
|
130
|
+
position:fixed;left:50%;bottom:20px;transform:translateX(-50%);
|
|
131
|
+
display:flex;align-items:center;gap:${DOCK_GAP}px;
|
|
132
|
+
padding:10px 18px;border-radius:999px;
|
|
133
|
+
background:rgba(15,23,42,0.88);backdrop-filter:blur(8px);
|
|
134
|
+
box-shadow:0 4px 20px rgba(0,0,0,0.4);
|
|
135
|
+
pointer-events:none;z-index:2147483646;
|
|
136
|
+
`;
|
|
137
|
+
dock.innerHTML = renderDockSlots();
|
|
138
|
+
document.body.appendChild(dock);
|
|
139
|
+
}
|
|
140
|
+
return dock;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const existingPanel = document.querySelector('[data-layout-lab-panel]');
|
|
144
|
+
const panelState = chippy.__layoutLabPanelState || {
|
|
145
|
+
side: existingPanel?.style.left ? 'left' : 'right',
|
|
146
|
+
expanded: existingPanel?.getAttribute('data-chippy-panel-expanded') === 'true',
|
|
147
|
+
settingsExpanded: Boolean(existingPanel?.querySelector('[data-chippy-settings-section]')),
|
|
148
|
+
};
|
|
149
|
+
chippy.__layoutLabPanelState = panelState;
|
|
150
|
+
|
|
151
|
+
function applyPanelStyle(panel) {
|
|
152
|
+
panel.setAttribute('data-chippy-panel-expanded', panelState.expanded ? 'true' : 'false');
|
|
153
|
+
panel.setAttribute('role', 'region');
|
|
154
|
+
panel.setAttribute('aria-label', 'Chippy');
|
|
155
|
+
panel.style.cssText = panelState.expanded ? `
|
|
156
|
+
position:fixed;${panelState.side}:20px;bottom:20px;width:${PANEL_WIDTH}px;
|
|
157
|
+
max-height:calc(100vh - 40px);overflow:auto;box-sizing:border-box;
|
|
158
|
+
pointer-events:auto;padding:14px;border-radius:18px;
|
|
159
|
+
background:#1F1F21;color:#CECFD2;
|
|
160
|
+
font:12px/1.5 'Atlassian Sans',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;box-shadow:0 16px 40px rgba(0,0,0,0.35);
|
|
161
|
+
z-index:2147483647;
|
|
162
|
+
` : `
|
|
163
|
+
position:fixed;${panelState.side}:20px;bottom:20px;
|
|
164
|
+
width:${PANEL_LAUNCHER_SIZE}px;height:${PANEL_LAUNCHER_SIZE}px;
|
|
165
|
+
overflow:visible;pointer-events:auto;padding:0;border-radius:50%;
|
|
166
|
+
background:transparent;color:#CECFD2;
|
|
167
|
+
font:12px/1.5 'Atlassian Sans',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;box-shadow:none;
|
|
168
|
+
z-index:2147483647;
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ensurePanel() {
|
|
173
|
+
if (!ownsProbePanelUi()) {
|
|
174
|
+
document.querySelector('[data-layout-lab-panel]')?.remove();
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
let panel = document.querySelector('[data-layout-lab-panel]');
|
|
178
|
+
if (!panel) {
|
|
179
|
+
panel = document.createElement('div');
|
|
180
|
+
panel.setAttribute('data-layout-lab-panel', 'true');
|
|
181
|
+
panel.setAttribute('data-chippy-ui', 'true');
|
|
182
|
+
applyPanelStyle(panel);
|
|
183
|
+
document.body.appendChild(panel);
|
|
184
|
+
}
|
|
185
|
+
return panel;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function syncDockedMarkers() {
|
|
189
|
+
const dock = ensureDock();
|
|
190
|
+
MARKER_IDS.forEach((id, index) => {
|
|
191
|
+
const marker = document.querySelector(`[data-layout-lab-marker="${id}"]`);
|
|
192
|
+
if (!marker || !marker.__docked) return;
|
|
193
|
+
if (!dock) {
|
|
194
|
+
marker.style.display = 'none';
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const slot = getDockSlotPos(index);
|
|
198
|
+
if (!slot) {
|
|
199
|
+
marker.style.display = 'none';
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
marker.style.display = '';
|
|
203
|
+
setMarkerViewportPos(marker, slot.x, slot.y);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function setPanelExpanded(expanded) {
|
|
208
|
+
panelState.expanded = Boolean(expanded);
|
|
209
|
+
if (!panelState.expanded) panelState.settingsExpanded = false;
|
|
210
|
+
updatePanel();
|
|
211
|
+
emitLabEvent(panelState.expanded ? 'panel-expanded' : 'panel-collapsed', {
|
|
212
|
+
panelExpanded: panelState.expanded,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function togglePanelSide() {
|
|
217
|
+
panelState.side = panelState.side === 'right' ? 'left' : 'right';
|
|
218
|
+
const panel = document.querySelector('[data-layout-lab-panel]');
|
|
219
|
+
if (panel) {
|
|
220
|
+
panel.style.left = panelState.side === 'left' ? '20px' : '';
|
|
221
|
+
panel.style.right = panelState.side === 'right' ? '20px' : '';
|
|
222
|
+
}
|
|
223
|
+
syncDockedMarkers();
|
|
224
|
+
emitLabEvent('toggle-panel-side', { panelSide: panelState.side });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Probe resolution ─────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
function probePoint(x, y) {
|
|
230
|
+
return hideOverlayWhile(() => {
|
|
231
|
+
const elements = document.elementsFromPoint(x, y);
|
|
232
|
+
const results = [];
|
|
233
|
+
|
|
234
|
+
for (const el of elements) {
|
|
235
|
+
if (isOverlay(el)) continue;
|
|
236
|
+
const exactFiber = getNearestFiber(el);
|
|
237
|
+
const componentFiber = getNearestComponent(el);
|
|
238
|
+
results.push({
|
|
239
|
+
exact: describeFiberTarget(exactFiber, el),
|
|
240
|
+
component: describeComponent(componentFiber),
|
|
241
|
+
_el: el,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const best = results.find((result) => result.component || result.exact) || results[0] || null;
|
|
246
|
+
// Strip internal _el before returning
|
|
247
|
+
return {
|
|
248
|
+
best: best ? { exact: best.exact, component: best.component } : null,
|
|
249
|
+
all: results.slice(0, 6).map(({ exact, component }) => ({ exact, component })),
|
|
250
|
+
_bestEl: best?._el || null,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Element highlight ────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
// One highlight element per marker — a very faint tinted overlay on the target element.
|
|
258
|
+
const _highlights = {};
|
|
259
|
+
|
|
260
|
+
// Per-marker scroll listener cleanup — keyed by marker id
|
|
261
|
+
const _scrollListeners = {};
|
|
262
|
+
|
|
263
|
+
function attachScrollListeners(id, targetEl, color) {
|
|
264
|
+
// Remove any previous listeners for this marker
|
|
265
|
+
detachScrollListeners(id);
|
|
266
|
+
|
|
267
|
+
if (!targetEl) return;
|
|
268
|
+
|
|
269
|
+
const listeners = [];
|
|
270
|
+
let current = targetEl.parentNode;
|
|
271
|
+
|
|
272
|
+
// Walk up to document.body only — page scroll is handled automatically
|
|
273
|
+
// because the highlight is position:absolute in document coordinates.
|
|
274
|
+
// Only internal scrollable containers need explicit listeners.
|
|
275
|
+
while (current && current !== document.body && current !== document) {
|
|
276
|
+
const node = current;
|
|
277
|
+
const cb = () => {
|
|
278
|
+
const marker = document.querySelector(`[data-layout-lab-marker="${id}"]`);
|
|
279
|
+
if (!marker || marker.__docked || marker.__dragging) return;
|
|
280
|
+
const currentTarget = marker.__syncTargetEl || marker.__probe?._targetEl || marker.__baseEl;
|
|
281
|
+
if (!currentTarget) return;
|
|
282
|
+
syncMarkerToTarget(marker, currentTarget);
|
|
283
|
+
updateHighlight(id, currentTarget, color);
|
|
284
|
+
updateParentBorder();
|
|
285
|
+
};
|
|
286
|
+
node.addEventListener('scroll', cb, { passive: true });
|
|
287
|
+
listeners.push({ node, cb });
|
|
288
|
+
current = current.parentNode;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
_scrollListeners[id] = listeners;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function detachScrollListeners(id) {
|
|
295
|
+
const listeners = _scrollListeners[id];
|
|
296
|
+
if (!listeners) return;
|
|
297
|
+
listeners.forEach(({ node, cb }) => {
|
|
298
|
+
try { node.removeEventListener('scroll', cb); } catch {}
|
|
299
|
+
});
|
|
300
|
+
delete _scrollListeners[id];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function getHighlightEl(id) {
|
|
304
|
+
if (!_highlights[id]) {
|
|
305
|
+
const el = document.createElement('div');
|
|
306
|
+
el.setAttribute('data-layout-lab-highlight', id);
|
|
307
|
+
el.setAttribute('data-chippy-ui', 'true');
|
|
308
|
+
el.style.cssText = `
|
|
309
|
+
position:absolute;pointer-events:none;z-index:2147483646;
|
|
310
|
+
border-radius:3px;transition:opacity 80ms ease;
|
|
311
|
+
display:none;
|
|
312
|
+
`;
|
|
313
|
+
document.documentElement.appendChild(el);
|
|
314
|
+
_highlights[id] = el;
|
|
315
|
+
}
|
|
316
|
+
return _highlights[id];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function updateHighlight(id, targetEl, color) {
|
|
320
|
+
const hl = getHighlightEl(id);
|
|
321
|
+
if (!targetEl) { hl.style.display = 'none'; return; }
|
|
322
|
+
const r = targetEl.getBoundingClientRect();
|
|
323
|
+
if (!r || r.width === 0) { hl.style.display = 'none'; return; }
|
|
324
|
+
// Convert viewport rect to document-absolute coords
|
|
325
|
+
const scrollX = window.scrollX || document.documentElement.scrollLeft;
|
|
326
|
+
const scrollY = window.scrollY || document.documentElement.scrollTop;
|
|
327
|
+
const hex = color.replace('#', '');
|
|
328
|
+
const rb = parseInt(hex.slice(0,2),16), gb = parseInt(hex.slice(2,4),16), bb = parseInt(hex.slice(4,6),16);
|
|
329
|
+
hl.style.cssText = `
|
|
330
|
+
position:absolute;pointer-events:none;z-index:2147483646;
|
|
331
|
+
border-radius:3px;transition:opacity 80ms ease;
|
|
332
|
+
left:${r.left + scrollX}px;top:${r.top + scrollY}px;
|
|
333
|
+
width:${r.width}px;height:${r.height}px;
|
|
334
|
+
background:rgba(${rb},${gb},${bb},0.08);
|
|
335
|
+
outline:1.5px solid rgba(${rb},${gb},${bb},0.28);
|
|
336
|
+
display:block;
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function clearHighlight(id) {
|
|
341
|
+
const hl = _highlights[id];
|
|
342
|
+
if (hl) hl.style.display = 'none';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ─── Boundary highlight (on-page location of a collateral boundary) ─────────
|
|
346
|
+
|
|
347
|
+
// Boundary fibers (Suspense, error boundaries, OwnerMark) are often composite
|
|
348
|
+
// fibers with no direct DOM node. Find the nearest host DOM element: first walk
|
|
349
|
+
// DOWN the child/sibling subtree to the first hostable element (the boundary's
|
|
350
|
+
// rendered content), then fall back to walking UP the return chain.
|
|
351
|
+
function nearestHostElement(fiber) {
|
|
352
|
+
if (!fiber) return null;
|
|
353
|
+
const seen = new Set();
|
|
354
|
+
const queue = [fiber];
|
|
355
|
+
while (queue.length) {
|
|
356
|
+
const f = queue.shift();
|
|
357
|
+
if (!f || seen.has(f)) continue;
|
|
358
|
+
seen.add(f);
|
|
359
|
+
if (f.stateNode instanceof Element) return f.stateNode;
|
|
360
|
+
if (f.child) queue.push(f.child);
|
|
361
|
+
if (f.sibling && f !== fiber) queue.push(f.sibling);
|
|
362
|
+
if (seen.size > 400) break;
|
|
363
|
+
}
|
|
364
|
+
let cur = fiber.return;
|
|
365
|
+
let hops = 0;
|
|
366
|
+
while (cur && hops < 400) {
|
|
367
|
+
if (cur.stateNode instanceof Element) return cur.stateNode;
|
|
368
|
+
cur = cur.return;
|
|
369
|
+
hops += 1;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const BOUNDARY_HIGHLIGHT_COLORS = { error: '#ae2e24', suspense: '#BF63F3' };
|
|
375
|
+
const BOUNDARY_HIGHLIGHT_ID = '__boundary';
|
|
376
|
+
|
|
377
|
+
function providerKindAccent(kind) {
|
|
378
|
+
const providers = getLab()?.getContextProviders?.() || [];
|
|
379
|
+
for (const provider of providers) {
|
|
380
|
+
const match = (provider.kinds || []).find((entry) => entry.kind === kind);
|
|
381
|
+
if (match?.accent) return match.accent;
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Highlight a collateral boundary's DOM subtree on the page so the user can see
|
|
387
|
+
// WHERE it is, and scroll it into view. Returns the located element (or null).
|
|
388
|
+
function highlightBoundary(markerId, kind) {
|
|
389
|
+
const markerEl = document.querySelector(`[data-layout-lab-marker="${markerId}"]`);
|
|
390
|
+
const fibers = findBoundaryFibers(markerEl?.__probe?._bestFiber || null);
|
|
391
|
+
const el = nearestHostElement(fibers[kind]);
|
|
392
|
+
if (!el) { clearHighlight(BOUNDARY_HIGHLIGHT_ID); return null; }
|
|
393
|
+
updateHighlight(BOUNDARY_HIGHLIGHT_ID, el, BOUNDARY_HIGHLIGHT_COLORS[kind] || providerKindAccent(kind) || '#CECFD2');
|
|
394
|
+
// keep the overlay tracking scroll/resize while it is shown
|
|
395
|
+
try { el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' }); } catch (_) {}
|
|
396
|
+
return el;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Shared-parent border ─────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
let _parentBorderEl = null;
|
|
402
|
+
|
|
403
|
+
function getParentBorderEl() {
|
|
404
|
+
if (!_parentBorderEl) {
|
|
405
|
+
_parentBorderEl = document.createElement('div');
|
|
406
|
+
_parentBorderEl.setAttribute('data-layout-lab-parent-border', 'true');
|
|
407
|
+
_parentBorderEl.setAttribute('data-chippy-ui', 'true');
|
|
408
|
+
_parentBorderEl.style.cssText = `
|
|
409
|
+
position:absolute;pointer-events:none;z-index:2147483645;
|
|
410
|
+
border:2px dashed rgba(148,163,184,0.55);border-radius:4px;
|
|
411
|
+
display:none;
|
|
412
|
+
`;
|
|
413
|
+
document.documentElement.appendChild(_parentBorderEl);
|
|
414
|
+
}
|
|
415
|
+
return _parentBorderEl;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function findNearestSharedParentEl(fiberA, fiberB) {
|
|
419
|
+
if (!fiberA || !fiberB) return null;
|
|
420
|
+
// Walk return chain of A, collect stateNodes
|
|
421
|
+
const setA = new Set();
|
|
422
|
+
let cur = fiberA.return;
|
|
423
|
+
while (cur) {
|
|
424
|
+
if (cur.stateNode && cur.stateNode instanceof Element) setA.add(cur.stateNode);
|
|
425
|
+
cur = cur.return;
|
|
426
|
+
}
|
|
427
|
+
// Walk return chain of B, first match wins
|
|
428
|
+
cur = fiberB.return;
|
|
429
|
+
while (cur) {
|
|
430
|
+
if (cur.stateNode && cur.stateNode instanceof Element && setA.has(cur.stateNode)) {
|
|
431
|
+
return cur.stateNode;
|
|
432
|
+
}
|
|
433
|
+
cur = cur.return;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
function getCommonParentInspection() {
|
|
440
|
+
const m1 = document.querySelector('[data-layout-lab-marker="1"]');
|
|
441
|
+
const m2 = document.querySelector('[data-layout-lab-marker="2"]');
|
|
442
|
+
if (!m1 || !m2 || m1.__docked || m2.__docked) return null;
|
|
443
|
+
const f1 = m1.__probe?._bestFiber;
|
|
444
|
+
const f2 = m2.__probe?._bestFiber;
|
|
445
|
+
const sharedEl = findNearestSharedParentEl(f1, f2);
|
|
446
|
+
if (!sharedEl) return null;
|
|
447
|
+
return chippy.inspectReact?.inspectNode?.(sharedEl) || null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function updateParentBorder() {
|
|
451
|
+
const pb = getParentBorderEl();
|
|
452
|
+
const m1 = document.querySelector('[data-layout-lab-marker="1"]');
|
|
453
|
+
const m2 = document.querySelector('[data-layout-lab-marker="2"]');
|
|
454
|
+
if (!m1 || !m2 || m1.__docked || m2.__docked) { pb.style.display = 'none'; return; }
|
|
455
|
+
const f1 = m1.__probe?._bestFiber;
|
|
456
|
+
const f2 = m2.__probe?._bestFiber;
|
|
457
|
+
const sharedEl = findNearestSharedParentEl(f1, f2);
|
|
458
|
+
if (!sharedEl) { pb.style.display = 'none'; return; }
|
|
459
|
+
const r = sharedEl.getBoundingClientRect();
|
|
460
|
+
if (!r || r.width === 0) { pb.style.display = 'none'; return; }
|
|
461
|
+
const scrollX = window.scrollX || document.documentElement.scrollLeft;
|
|
462
|
+
const scrollY = window.scrollY || document.documentElement.scrollTop;
|
|
463
|
+
pb.style.cssText = `
|
|
464
|
+
position:absolute;pointer-events:none;z-index:2147483645;
|
|
465
|
+
border:2px dashed rgba(148,163,184,0.55);border-radius:4px;
|
|
466
|
+
left:${r.left + scrollX - 3}px;top:${r.top + scrollY - 3}px;
|
|
467
|
+
width:${r.width + 6}px;height:${r.height + 6}px;
|
|
468
|
+
display:block;
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ─── Marker lifecycle ─────────────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
function scrollOffset() {
|
|
475
|
+
return {
|
|
476
|
+
x: window.scrollX || document.documentElement.scrollLeft,
|
|
477
|
+
y: window.scrollY || document.documentElement.scrollTop,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function setPos(marker, x, y) {
|
|
482
|
+
// Active probes use document-absolute coordinates; docked probes use viewport coordinates.
|
|
483
|
+
marker.__x = x;
|
|
484
|
+
marker.__y = y;
|
|
485
|
+
marker.style.left = x + 'px';
|
|
486
|
+
marker.style.top = y + 'px';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function setMarkerViewportPos(marker, x, y) {
|
|
490
|
+
marker.__x = x;
|
|
491
|
+
marker.__y = y;
|
|
492
|
+
marker.style.left = x + 'px';
|
|
493
|
+
marker.style.top = y + 'px';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function storeMarkerAnchor(marker, targetEl) {
|
|
497
|
+
if (!marker || !targetEl) return;
|
|
498
|
+
const mr = marker.getBoundingClientRect();
|
|
499
|
+
const tr = targetEl.getBoundingClientRect();
|
|
500
|
+
// Probe tip is marker left/bottom.
|
|
501
|
+
marker.__anchorDx = mr.left - tr.left;
|
|
502
|
+
marker.__anchorDy = mr.bottom - tr.top;
|
|
503
|
+
marker.__syncTargetEl = targetEl;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function syncMarkerToTarget(marker, targetEl) {
|
|
507
|
+
if (!marker || !targetEl) return;
|
|
508
|
+
const tr = targetEl.getBoundingClientRect();
|
|
509
|
+
const { x: sx, y: sy } = scrollOffset();
|
|
510
|
+
const tipX = tr.left + (marker.__anchorDx || 0);
|
|
511
|
+
const tipY = tr.top + (marker.__anchorDy || 0);
|
|
512
|
+
setPos(marker, tipX + sx, tipY + sy - MARKER_SIZE);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function getDockSlotPos(index) {
|
|
516
|
+
// Dock is position:fixed, so docked markers use viewport coordinates too.
|
|
517
|
+
const dock = ensureDock();
|
|
518
|
+
if (!dock) return null;
|
|
519
|
+
if (ownsProbePanelUi()) {
|
|
520
|
+
const id = MARKER_IDS[index];
|
|
521
|
+
const slot = dock.querySelector(`[data-chippy-probe-dock-slot="${id}"]`);
|
|
522
|
+
if (!slot) return null;
|
|
523
|
+
const slotRect = slot.getBoundingClientRect();
|
|
524
|
+
return {
|
|
525
|
+
x: slotRect.left + (slotRect.width - MARKER_SIZE) / 2,
|
|
526
|
+
y: slotRect.top + (slotRect.height - MARKER_SIZE) / 2,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const dockRect = dock.getBoundingClientRect();
|
|
530
|
+
const totalWidth = MARKER_IDS.length * MARKER_SIZE + (MARKER_IDS.length - 1) * DOCK_GAP;
|
|
531
|
+
const startX = dockRect.left + (dockRect.width - totalWidth) / 2;
|
|
532
|
+
return {
|
|
533
|
+
x: startX + index * (MARKER_SIZE + DOCK_GAP),
|
|
534
|
+
y: dockRect.top + (dockRect.height - MARKER_SIZE) / 2,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function snapToDock(marker) {
|
|
539
|
+
const id = marker.getAttribute('data-layout-lab-marker');
|
|
540
|
+
const index = MARKER_IDS.indexOf(id);
|
|
541
|
+
marker.__docked = true;
|
|
542
|
+
marker.__probe = null;
|
|
543
|
+
marker.__baseEl = null;
|
|
544
|
+
marker.__levelOffset = 0;
|
|
545
|
+
detachScrollListeners(id);
|
|
546
|
+
// applyMarkerStyle rewrites cssText — call setPos after
|
|
547
|
+
applyMarkerStyle(marker);
|
|
548
|
+
const slot = getDockSlotPos(index);
|
|
549
|
+
if (!slot) {
|
|
550
|
+
marker.style.display = 'none';
|
|
551
|
+
clearHighlight(id);
|
|
552
|
+
updateParentBorder();
|
|
553
|
+
updatePanel();
|
|
554
|
+
emitLabEvent('marker-docked', { markerId: id });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
setMarkerViewportPos(marker, slot.x, slot.y);
|
|
558
|
+
clearHighlight(id);
|
|
559
|
+
updateParentBorder();
|
|
560
|
+
updatePanel();
|
|
561
|
+
emitLabEvent('marker-docked', { markerId: id });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function isNearDock(clientX, clientY) {
|
|
565
|
+
const dock = document.querySelector('[data-layout-lab-dock]');
|
|
566
|
+
if (!dock) return false;
|
|
567
|
+
const slots = Array.from(dock.querySelectorAll('[data-chippy-probe-dock-slot]'));
|
|
568
|
+
if (slots.length > 0) {
|
|
569
|
+
return slots.some((slot) => {
|
|
570
|
+
const r = slot.getBoundingClientRect();
|
|
571
|
+
return Math.hypot(clientX - (r.left + r.width / 2), clientY - (r.top + r.height / 2)) < DOCK_SNAP_RADIUS;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
const r = dock.getBoundingClientRect();
|
|
575
|
+
const cx = r.left + r.width / 2;
|
|
576
|
+
const cy = r.top + r.height / 2;
|
|
577
|
+
return Math.hypot(clientX - cx, clientY - cy) < DOCK_SNAP_RADIUS;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function applyMarkerStyle(marker) {
|
|
581
|
+
const id = marker.getAttribute('data-layout-lab-marker');
|
|
582
|
+
const color = MARKER_COLORS[id] || '#A9ABAF';
|
|
583
|
+
const docked = marker.__docked;
|
|
584
|
+
const S = MARKER_SIZE;
|
|
585
|
+
marker.style.cssText = `
|
|
586
|
+
position:${docked ? 'fixed' : 'absolute'};width:${S}px;height:${S}px;
|
|
587
|
+
border:0;border-radius:${docked ? '50%' : '50% 50% 50% 0'};
|
|
588
|
+
background:${docked ? 'rgba(71,85,105,0.55)' : color};
|
|
589
|
+
color:#fff;font-weight:700;font-size:14px;
|
|
590
|
+
box-shadow:${docked ? 'none' : '0 4px 16px rgba(0,0,0,0.30)'};
|
|
591
|
+
pointer-events:auto;cursor:grab;
|
|
592
|
+
touch-action:none;user-select:none;
|
|
593
|
+
transition:border-radius 150ms ease, background 150ms ease, box-shadow 150ms ease;
|
|
594
|
+
z-index:2147483647;opacity:${docked ? 0.55 : 1};
|
|
595
|
+
display:flex;align-items:center;justify-content:center;
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function getMarkerState(id) {
|
|
600
|
+
const marker = document.querySelector(`[data-layout-lab-marker="${id}"]`);
|
|
601
|
+
if (!marker) return null;
|
|
602
|
+
const annotation = normalizeProbeAnnotation({ intent: marker.__intent, comment: marker.__comment });
|
|
603
|
+
return {
|
|
604
|
+
id,
|
|
605
|
+
docked: marker.__docked ?? true,
|
|
606
|
+
position: { x: marker.__x ?? null, y: marker.__y ?? null },
|
|
607
|
+
...annotation,
|
|
608
|
+
probe: marker.__probe || null,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function setMarkerAnnotation(id, patch = {}) {
|
|
613
|
+
const marker = document.querySelector(`[data-layout-lab-marker="${id}"]`);
|
|
614
|
+
if (!marker) return null;
|
|
615
|
+
const current = normalizeProbeAnnotation({ intent: marker.__intent, comment: marker.__comment });
|
|
616
|
+
const next = normalizeProbeAnnotation({
|
|
617
|
+
intent: patch.intent === undefined ? current.intent : patch.intent,
|
|
618
|
+
comment: patch.comment === undefined ? current.comment : patch.comment,
|
|
619
|
+
});
|
|
620
|
+
marker.__intent = next.intent;
|
|
621
|
+
marker.__comment = next.comment;
|
|
622
|
+
emitLabEvent('marker-annotation-change', { markerId: id, ...next });
|
|
623
|
+
return { id, ...next };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function getAllMarkerStates() {
|
|
627
|
+
return MARKER_IDS.reduce((acc, id) => {
|
|
628
|
+
const state = getMarkerState(id);
|
|
629
|
+
if (state) acc[id] = state;
|
|
630
|
+
return acc;
|
|
631
|
+
}, {});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Walk DOM parentElement chain by N steps (for level walking — works on any site, React or not)
|
|
635
|
+
function getDomElAtLevel(baseEl, offset) {
|
|
636
|
+
if (!baseEl || offset === 0) return baseEl;
|
|
637
|
+
if (offset > 0) {
|
|
638
|
+
// Go up — skip elements that have zero size (wrappers with no visual box)
|
|
639
|
+
let cur = baseEl.parentElement;
|
|
640
|
+
let steps = offset;
|
|
641
|
+
while (cur && steps > 0) {
|
|
642
|
+
const r = cur.getBoundingClientRect();
|
|
643
|
+
if (r.width > 0 && r.height > 0) steps--;
|
|
644
|
+
if (steps > 0) cur = cur.parentElement;
|
|
645
|
+
}
|
|
646
|
+
return cur || baseEl;
|
|
647
|
+
}
|
|
648
|
+
// Go down — first visible child
|
|
649
|
+
let cur = baseEl.firstElementChild;
|
|
650
|
+
let steps = -offset;
|
|
651
|
+
while (cur && steps > 0) {
|
|
652
|
+
const r = cur.getBoundingClientRect();
|
|
653
|
+
if (r.width > 0 && r.height > 0) steps--;
|
|
654
|
+
if (steps > 0) cur = cur.firstElementChild || cur.nextElementSibling;
|
|
655
|
+
}
|
|
656
|
+
return cur || baseEl;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function refreshMarker(marker, reason = 'refresh-marker') {
|
|
660
|
+
const id = marker.getAttribute('data-layout-lab-marker');
|
|
661
|
+
const color = MARKER_COLORS[id] || '#A9ABAF';
|
|
662
|
+
const rect = marker.getBoundingClientRect();
|
|
663
|
+
// Probe from the sharp corner (bottom-left tip), not the centre
|
|
664
|
+
const cx = rect.left;
|
|
665
|
+
const cy = rect.bottom;
|
|
666
|
+
|
|
667
|
+
const levelOffset = marker.__levelOffset || 0;
|
|
668
|
+
const fiberOffset = marker.__fiberOffset || 0;
|
|
669
|
+
|
|
670
|
+
// On level-walk, reuse the stored base element — don't re-probe
|
|
671
|
+
// On fiber-walk (up/down), target fiber from return chain instead of DOM walk
|
|
672
|
+
// On normal drag/drop, always re-probe and store fresh base element
|
|
673
|
+
let rawProbe = marker.__probe;
|
|
674
|
+
let baseEl = marker.__baseEl || null;
|
|
675
|
+
|
|
676
|
+
if (reason === 'marker-level-up' || reason === 'marker-level-down') {
|
|
677
|
+
// DOM level walk — reuse existing base element
|
|
678
|
+
if (!baseEl && rawProbe?._bestEl) {
|
|
679
|
+
baseEl = rawProbe._bestEl;
|
|
680
|
+
marker.__baseEl = baseEl;
|
|
681
|
+
}
|
|
682
|
+
} else if (reason === 'marker-fiber-up' || reason === 'marker-fiber-down') {
|
|
683
|
+
// Fiber parent walk — reuse existing probe, pick fiber from parents array
|
|
684
|
+
if (!baseEl && rawProbe?._bestEl) {
|
|
685
|
+
baseEl = rawProbe._bestEl;
|
|
686
|
+
marker.__baseEl = baseEl;
|
|
687
|
+
}
|
|
688
|
+
} else if (reason === 'marker-scroll') {
|
|
689
|
+
// Scroll syncing is handled by moving the marker to the tracked target, not by re-probing.
|
|
690
|
+
} else {
|
|
691
|
+
// Fresh probe from current position
|
|
692
|
+
rawProbe = probePoint(cx, cy);
|
|
693
|
+
baseEl = rawProbe?._bestEl || null;
|
|
694
|
+
marker.__baseEl = baseEl;
|
|
695
|
+
// Reset both offsets on fresh probe
|
|
696
|
+
marker.__levelOffset = 0;
|
|
697
|
+
marker.__fiberOffset = 0;
|
|
698
|
+
marker.__fiberReturnChain = null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Walk DOM to target element at current DOM level offset
|
|
702
|
+
const targetEl = getDomElAtLevel(baseEl, levelOffset);
|
|
703
|
+
|
|
704
|
+
// Determine best fiber: fiber-walk overrides DOM-walk fiber
|
|
705
|
+
const baseFiber = targetEl ? getNearestComponent(targetEl) : null;
|
|
706
|
+
let bestFiber;
|
|
707
|
+
if (fiberOffset > 0) {
|
|
708
|
+
// Walk fiber return chain using actual fiber references (not description objects)
|
|
709
|
+
if (!marker.__fiberReturnChain || reason !== 'marker-fiber-up' && reason !== 'marker-fiber-down') {
|
|
710
|
+
marker.__fiberReturnChain = baseFiber ? getReturnComponentFibers(baseFiber) : [];
|
|
711
|
+
}
|
|
712
|
+
const chain = marker.__fiberReturnChain;
|
|
713
|
+
const clampedOffset = Math.min(fiberOffset, chain.length);
|
|
714
|
+
marker.__fiberOffset = clampedOffset; // clamp so panel stays in sync
|
|
715
|
+
bestFiber = chain[clampedOffset - 1] || baseFiber;
|
|
716
|
+
} else {
|
|
717
|
+
marker.__fiberReturnChain = null; // reset chain on return to base
|
|
718
|
+
bestFiber = baseFiber;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Store full fiber parent chain for up/down navigation
|
|
722
|
+
marker.__fiberParents = bestFiber ? getReturnComponentChain(bestFiber) : [];
|
|
723
|
+
// Store actual owner fiber references for owner-click navigation
|
|
724
|
+
marker.__ownerFibers = bestFiber ? getOwnerFibers(bestFiber) : [];
|
|
725
|
+
|
|
726
|
+
// Determine highlight target: when fiber-walking, find the actual DOM node rendered by that fiber
|
|
727
|
+
const highlightEl = fiberOffset > 0
|
|
728
|
+
? (getFiberDomNode(bestFiber) || targetEl)
|
|
729
|
+
: targetEl;
|
|
730
|
+
|
|
731
|
+
marker.__probe = rawProbe
|
|
732
|
+
? { ...rawProbe, _bestFiber: bestFiber, _targetEl: targetEl, best: { exact: describeFiberTarget(getNearestFiber(targetEl), targetEl), component: describeComponent(bestFiber) } }
|
|
733
|
+
: null;
|
|
734
|
+
|
|
735
|
+
updateHighlight(id, highlightEl, color);
|
|
736
|
+
storeMarkerAnchor(marker, highlightEl);
|
|
737
|
+
updateParentBorder();
|
|
738
|
+
updatePanel();
|
|
739
|
+
// Reattach scroll listeners whenever the target element changes
|
|
740
|
+
attachScrollListeners(id, highlightEl, color);
|
|
741
|
+
emitLabEvent(reason, { markerId: id, marker: getMarkerState(id) });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Key handler attached during drag for level walking (left/right = DOM) and fiber walking (up/down = fiber parents)
|
|
745
|
+
function makeDragKeyHandler(marker) {
|
|
746
|
+
return function onKey(event) {
|
|
747
|
+
if (!marker.__dragging) return;
|
|
748
|
+
if (event.key === 'ArrowLeft') {
|
|
749
|
+
event.preventDefault();
|
|
750
|
+
marker.__levelOffset = (marker.__levelOffset || 0) + 1; // go up DOM
|
|
751
|
+
refreshMarker(marker, 'marker-level-up');
|
|
752
|
+
} else if (event.key === 'ArrowRight') {
|
|
753
|
+
event.preventDefault();
|
|
754
|
+
marker.__levelOffset = Math.max(0, (marker.__levelOffset || 0) - 1); // go down DOM
|
|
755
|
+
refreshMarker(marker, 'marker-level-down');
|
|
756
|
+
} else if (event.key === 'ArrowUp') {
|
|
757
|
+
event.preventDefault();
|
|
758
|
+
marker.__fiberOffset = (marker.__fiberOffset || 0) + 1; // go up fiber parents
|
|
759
|
+
refreshMarker(marker, 'marker-fiber-up');
|
|
760
|
+
} else if (event.key === 'ArrowDown') {
|
|
761
|
+
event.preventDefault();
|
|
762
|
+
marker.__fiberOffset = Math.max(0, (marker.__fiberOffset || 0) - 1); // go down fiber parents
|
|
763
|
+
refreshMarker(marker, 'marker-fiber-down');
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function makeMarkerDraggable(marker) {
|
|
769
|
+
let keyHandler = null;
|
|
770
|
+
|
|
771
|
+
marker.addEventListener('pointerdown', (event) => {
|
|
772
|
+
event.preventDefault();
|
|
773
|
+
marker.__dragging = true;
|
|
774
|
+
marker.__levelOffset = marker.__levelOffset || 0;
|
|
775
|
+
marker.__fiberOffset = marker.__fiberOffset || 0;
|
|
776
|
+
marker.__startX = event.clientX;
|
|
777
|
+
marker.__startY = event.clientY;
|
|
778
|
+
marker.setPointerCapture(event.pointerId);
|
|
779
|
+
// Undock if was docked — applyMarkerStyle rewrites cssText, so capture offsets AFTER
|
|
780
|
+
if (marker.__docked) {
|
|
781
|
+
const rect = marker.getBoundingClientRect();
|
|
782
|
+
marker.__docked = false;
|
|
783
|
+
applyMarkerStyle(marker);
|
|
784
|
+
const { x: sx, y: sy } = scrollOffset();
|
|
785
|
+
setPos(marker, rect.left + sx, rect.top + sy);
|
|
786
|
+
}
|
|
787
|
+
// Capture grab offset relative to current rendered position
|
|
788
|
+
marker.__ox = event.clientX - marker.getBoundingClientRect().left;
|
|
789
|
+
marker.__oy = event.clientY - marker.getBoundingClientRect().top;
|
|
790
|
+
marker.style.cursor = 'grabbing';
|
|
791
|
+
marker.style.transform = 'scale(1.08)';
|
|
792
|
+
// Attach key handler for level walking
|
|
793
|
+
keyHandler = makeDragKeyHandler(marker);
|
|
794
|
+
document.addEventListener('keydown', keyHandler);
|
|
795
|
+
emitLabEvent('marker-drag-start', { markerId: marker.getAttribute('data-layout-lab-marker') });
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
marker.addEventListener('pointermove', (event) => {
|
|
799
|
+
if (!marker.__dragging) return;
|
|
800
|
+
const { x: sx, y: sy } = scrollOffset();
|
|
801
|
+
setPos(marker, event.clientX - marker.__ox + sx, event.clientY - marker.__oy + sy);
|
|
802
|
+
refreshMarker(marker, 'marker-dragging');
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
marker.addEventListener('pointerup', (event) => {
|
|
806
|
+
if (!marker.__dragging) return;
|
|
807
|
+
marker.__dragging = false;
|
|
808
|
+
marker.releasePointerCapture(event.pointerId);
|
|
809
|
+
marker.style.transform = 'scale(1)';
|
|
810
|
+
if (keyHandler) { document.removeEventListener('keydown', keyHandler); keyHandler = null; }
|
|
811
|
+
|
|
812
|
+
const dropX = event.clientX;
|
|
813
|
+
const dropY = event.clientY;
|
|
814
|
+
|
|
815
|
+
if (isNearDock(dropX, dropY)) {
|
|
816
|
+
snapToDock(marker);
|
|
817
|
+
} else {
|
|
818
|
+
// applyMarkerStyle rewrites cssText — must call setPos AFTER it
|
|
819
|
+
applyMarkerStyle(marker);
|
|
820
|
+
const { x: sx, y: sy } = scrollOffset();
|
|
821
|
+
setPos(marker, dropX - marker.__ox + sx, dropY - marker.__oy + sy);
|
|
822
|
+
// No re-probe on mouse up — the last drag position already has the probe computed
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function createMarker(id, index) {
|
|
828
|
+
// Markers go on <html> as position:absolute — tracks with document scroll naturally
|
|
829
|
+
const marker = document.createElement('button');
|
|
830
|
+
marker.type = 'button';
|
|
831
|
+
marker.textContent = id;
|
|
832
|
+
marker.setAttribute('data-layout-lab-marker', id);
|
|
833
|
+
marker.setAttribute('data-chippy-ui', 'true');
|
|
834
|
+
marker.__docked = true;
|
|
835
|
+
marker.__levelOffset = 0;
|
|
836
|
+
marker.__intent = null;
|
|
837
|
+
marker.__comment = '';
|
|
838
|
+
applyMarkerStyle(marker);
|
|
839
|
+
document.documentElement.appendChild(marker);
|
|
840
|
+
makeMarkerDraggable(marker);
|
|
841
|
+
|
|
842
|
+
const dockSlot = getDockSlotPos(index);
|
|
843
|
+
marker.__docked = true;
|
|
844
|
+
applyMarkerStyle(marker);
|
|
845
|
+
marker.style.display = dockSlot ? '' : 'none';
|
|
846
|
+
if (dockSlot) setMarkerViewportPos(marker, dockSlot.x, dockSlot.y);
|
|
847
|
+
return marker;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ─── Panel ────────────────────────────────────────────────────────────────
|
|
851
|
+
|
|
852
|
+
const PROBE_LABELS = { '1': 'P1', '2': 'P2' };
|
|
853
|
+
const PROBE_INTENT_LABELS = {
|
|
854
|
+
explain: 'Explain',
|
|
855
|
+
investigate: 'Investigate',
|
|
856
|
+
change: 'Change',
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
function contextUiEnabled() {
|
|
860
|
+
return chippy.config?.contextUiEnabled !== false;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function renderContextField(label, value) {
|
|
864
|
+
if (value == null || value === '') return '';
|
|
865
|
+
return `<div style="display:grid;grid-template-columns:58px 1fr;gap:5px;align-items:baseline;">
|
|
866
|
+
<span style="color:#A9ABAF;font-size:9px;text-transform:uppercase;letter-spacing:.04em;">${escapeAttr(label)}</span>
|
|
867
|
+
<span style="color:#CECFD2;font-family:monospace;font-size:10px;overflow-wrap:anywhere;">${escapeAttr(value)}</span>
|
|
868
|
+
</div>`;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function renderBoundaryDistance(distance) {
|
|
872
|
+
if (distance == null) return '';
|
|
873
|
+
const hops = distance === 1 ? '1 hop' : `${distance} hops`;
|
|
874
|
+
return `<span data-chippy-boundary-distance style="margin-left:auto;padding:1px 5px;border-radius:999px;background:rgba(148,163,184,.18);color:#CECFD2;font-size:9px;font-weight:600;">${escapeAttr(hops)}</span>`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const EMPTY_DISPLAY = { kinds: [], titles: {}, fields: {}, showError: false };
|
|
878
|
+
|
|
879
|
+
function contextDisplay(context) {
|
|
880
|
+
return context.display || EMPTY_DISPLAY;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function boundaryAccent(kind) {
|
|
884
|
+
if (kind.accent && /^#[0-9a-f]{3,8}$/i.test(kind.accent)) return kind.accent;
|
|
885
|
+
return '#5E6C84';
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function renderContext(context, markerId) {
|
|
889
|
+
if (!contextUiEnabled() || !context) return '';
|
|
890
|
+
const boundaries = context.boundaries || {};
|
|
891
|
+
const display = contextDisplay(context);
|
|
892
|
+
const suspense = boundaries.suspense || null;
|
|
893
|
+
|
|
894
|
+
// Provider boundaries (for example a team/package ownership marker). These
|
|
895
|
+
// are NOT the React render owner, which is the separate "jump to owner"
|
|
896
|
+
// affordance. Each is a SHORT clickable lozenge (title + distance only), so
|
|
897
|
+
// Chippy can select the boundary fiber as the next inspection target.
|
|
898
|
+
const providerHtml = (display.kinds || []).map((kind) => {
|
|
899
|
+
const ref = boundaries[kind.kind];
|
|
900
|
+
if (!ref) return '';
|
|
901
|
+
const accent = boundaryAccent(kind);
|
|
902
|
+
const title = display.titles?.[kind.kind] || ref.componentName || kind.label;
|
|
903
|
+
return `
|
|
904
|
+
<div data-chippy-boundary-marker="${markerId}" data-chippy-boundary="${escapeAttr(kind.kind)}" role="button" tabindex="0"
|
|
905
|
+
style="display:flex;align-items:center;gap:6px;padding:6px 8px;border-radius:7px;background:${accent}22;border:1px solid ${accent}44;cursor:pointer;">
|
|
906
|
+
<span style="padding:1px 5px;border-radius:999px;background:${accent};color:#fff;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;">${escapeAttr(kind.label)}</span>
|
|
907
|
+
<strong style="color:#CECFD2;font-size:10px;overflow-wrap:anywhere;">${escapeAttr(title)}</strong>
|
|
908
|
+
${renderBoundaryDistance(ref.distance)}
|
|
909
|
+
</div>`;
|
|
910
|
+
}).join('');
|
|
911
|
+
|
|
912
|
+
// Error boundary: providers choose the operational fields to show (for
|
|
913
|
+
// example team and a Sentry id); the full boundary payload is still sent
|
|
914
|
+
// to the backend on click.
|
|
915
|
+
const errorBoundaryRef = boundaries.error;
|
|
916
|
+
const errorTitle = display.titles?.error || '';
|
|
917
|
+
const errorFields = (display.fields?.error || []).map((f) => renderContextField(f.label, f.value)).join('');
|
|
918
|
+
const boundaryHtml = errorBoundaryRef || display.showError ? `
|
|
919
|
+
<div data-chippy-boundary-marker="${markerId}" data-chippy-boundary="error" role="button" tabindex="0"
|
|
920
|
+
style="display:grid;gap:3px;padding:7px 8px;border-radius:7px;background:rgba(174,46,36,.12);border:1px solid rgba(255,116,102,.22);cursor:pointer;">
|
|
921
|
+
<div style="display:flex;align-items:center;gap:6px;margin-bottom:1px;">
|
|
922
|
+
<span style="padding:1px 5px;border-radius:999px;background:#ae2e24;color:#fff;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;">error boundary</span>
|
|
923
|
+
${errorTitle ? `<strong style="color:#FF9C8F;font-size:10px;">${escapeAttr(errorTitle)}</strong>` : ''}
|
|
924
|
+
${renderBoundaryDistance(errorBoundaryRef?.distance)}
|
|
925
|
+
</div>
|
|
926
|
+
${errorFields}
|
|
927
|
+
</div>` : '';
|
|
928
|
+
|
|
929
|
+
const suspenseHtml = suspense ? `
|
|
930
|
+
<div data-chippy-boundary-marker="${markerId}" data-chippy-boundary="suspense" role="button" tabindex="0"
|
|
931
|
+
style="display:grid;gap:3px;padding:7px 8px;border-radius:7px;background:rgba(124,58,237,.12);border:1px solid rgba(167,139,250,.22);cursor:pointer;">
|
|
932
|
+
<div style="display:flex;align-items:center;gap:6px;margin-bottom:1px;">
|
|
933
|
+
<span style="padding:1px 5px;border-radius:999px;background:#BF63F3;color:#fff;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;">suspense boundary</span>
|
|
934
|
+
<strong style="color:#D8A0F7;font-size:10px;">${escapeAttr(suspense.componentName || 'Suspense')}</strong>
|
|
935
|
+
${renderBoundaryDistance(suspense.distance)}
|
|
936
|
+
</div>
|
|
937
|
+
</div>` : '';
|
|
938
|
+
|
|
939
|
+
if (!providerHtml && !boundaryHtml && !suspenseHtml) return '';
|
|
940
|
+
return `<div data-chippy-context="${markerId}" style="display:grid;gap:5px;margin-top:2px;">${providerHtml}${boundaryHtml}${suspenseHtml}</div>`;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function renderProbeRow(id) {
|
|
944
|
+
const marker = document.querySelector(`[data-layout-lab-marker="${id}"]`);
|
|
945
|
+
const color = MARKER_COLORS[id] || '#A9ABAF';
|
|
946
|
+
const docked = marker?.__docked ?? true;
|
|
947
|
+
const levelOffset = marker?.__levelOffset || 0;
|
|
948
|
+
|
|
949
|
+
if (docked) {
|
|
950
|
+
return `
|
|
951
|
+
<div data-chippy-key="probe-row-${id}" data-chippy-probe-row="${id}" style="min-height:50px;padding:7px 12px 7px 8px;border-radius:10px;background:rgba(30,41,59,0.55);
|
|
952
|
+
display:grid;grid-template-columns:${MARKER_SIZE}px 1fr;align-items:center;gap:10px;
|
|
953
|
+
border:1px solid rgba(148,163,184,.10);">
|
|
954
|
+
<span data-chippy-probe-dock-slot="${id}" aria-hidden="true" style="
|
|
955
|
+
width:${MARKER_SIZE}px;height:${MARKER_SIZE}px;border-radius:50%;box-sizing:border-box;
|
|
956
|
+
border:2px dashed ${color};opacity:.25;display:block;
|
|
957
|
+
"></span>
|
|
958
|
+
<span style="font-size:11px;color:#A9ABAF;"><strong style="color:${color};font-style:normal;">${PROBE_LABELS[id]}</strong><span aria-hidden="true"> — </span>drag to source</span>
|
|
959
|
+
</div>
|
|
960
|
+
`;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const probe = marker?.__probe;
|
|
964
|
+
const exact = probe?.best?.exact;
|
|
965
|
+
const component = probe?.best?.component;
|
|
966
|
+
const domTag = exact?.domTag || '—';
|
|
967
|
+
const domText = (exact?.domText || '').slice(0, 32);
|
|
968
|
+
const exactType = exact?.fiberType || domTag;
|
|
969
|
+
const exactSource = exact?.source || null;
|
|
970
|
+
const exactSourceStr = exactSource
|
|
971
|
+
? `${exactSource.fileName?.split('/').slice(-3).join('/')}:${exactSource.lineNumber}`
|
|
972
|
+
: 'no source (production)';
|
|
973
|
+
const compName = component?.name || '—';
|
|
974
|
+
const owners = component?.owners || [];
|
|
975
|
+
const compSource = component?.source;
|
|
976
|
+
const ownerSource = owners.find((o) => o.source)?.source;
|
|
977
|
+
const markerContext = describeContext(marker?.__probe?._bestFiber || null);
|
|
978
|
+
const componentSource = compSource || ownerSource || null;
|
|
979
|
+
const componentSourceStr = componentSource
|
|
980
|
+
? `${componentSource.fileName?.split('/').slice(-3).join('/')}:${componentSource.lineNumber}`
|
|
981
|
+
: 'no source';
|
|
982
|
+
const props = component?.props || {};
|
|
983
|
+
const propsStr = Object.entries(props)
|
|
984
|
+
.filter(([k]) => !['children', 'style', 'className'].includes(k))
|
|
985
|
+
.slice(0, 4)
|
|
986
|
+
.map(([k, v]) => {
|
|
987
|
+
let s;
|
|
988
|
+
if (typeof v === 'object' && v !== null) {
|
|
989
|
+
if (v?.id) {
|
|
990
|
+
s = String(v.id).slice(0, 16);
|
|
991
|
+
} else {
|
|
992
|
+
try { s = JSON.stringify(v).slice(0, 16); } catch { s = '[obj]'; }
|
|
993
|
+
}
|
|
994
|
+
} else {
|
|
995
|
+
s = String(v).slice(0, 16);
|
|
996
|
+
}
|
|
997
|
+
return `<span style="color:#A9ABAF;">${k}</span><span style="color:#A9ABAF;">=</span><span style="color:#CECFD2;">${s}</span>`;
|
|
998
|
+
}).join(' ') || '<span style="color:#A9ABAF;">—</span>';
|
|
999
|
+
|
|
1000
|
+
const markerEl = document.querySelector(`[data-layout-lab-marker="${id}"]`);
|
|
1001
|
+
const fiberOffset = markerEl?.__fiberOffset || 0;
|
|
1002
|
+
const levelHint = levelOffset > 0
|
|
1003
|
+
? `<span style="color:#A9ABAF;font-size:10px;margin-left:4px;">DOM↑${levelOffset}</span>`
|
|
1004
|
+
: '';
|
|
1005
|
+
const fiberHint = fiberOffset > 0
|
|
1006
|
+
? `<span style="color:#D8A0F7;font-size:10px;margin-left:4px;">fiber↑${fiberOffset}</span>`
|
|
1007
|
+
: '';
|
|
1008
|
+
|
|
1009
|
+
// Clickable owner breadcrumbs — each item is a button that navigates to that owner
|
|
1010
|
+
const ownerButtons = owners.slice(0, 5).map((o, i) =>
|
|
1011
|
+
`<button data-chippy-owner-btn="${id}" data-owner-index="${i}" style="
|
|
1012
|
+
border:0;background:none;padding:0 3px;cursor:pointer;pointer-events:auto;
|
|
1013
|
+
color:#D8A0F7;font-size:10px;font-family:inherit;text-decoration:underline dotted;
|
|
1014
|
+
text-decoration-color:rgba(196,181,253,0.4);
|
|
1015
|
+
">${o.name}</button>`
|
|
1016
|
+
).join(`<span style="color:#A9ABAF;"> › </span>`);
|
|
1017
|
+
const intent = marker?.__intent ?? null;
|
|
1018
|
+
const comment = marker?.__comment || '';
|
|
1019
|
+
const intentOptions = [
|
|
1020
|
+
'<option value="">Choose intent</option>',
|
|
1021
|
+
...Object.entries(PROBE_INTENT_LABELS).map(([value, label]) =>
|
|
1022
|
+
`<option value="${value}"${intent === value ? ' selected' : ''}>${label}</option>`
|
|
1023
|
+
),
|
|
1024
|
+
].join('');
|
|
1025
|
+
|
|
1026
|
+
return `
|
|
1027
|
+
<div data-chippy-key="probe-row-${id}" data-chippy-probe-row="${id}" style="border-radius:10px;overflow:hidden;border-left:3px solid ${color};">
|
|
1028
|
+
<div style="min-height:44px;padding:4px 10px 4px 6px;background:rgba(30,41,59,0.85);display:flex;align-items:center;gap:7px;">
|
|
1029
|
+
<span data-chippy-probe-dock-slot="${id}" aria-hidden="true" style="
|
|
1030
|
+
width:${MARKER_SIZE}px;height:${MARKER_SIZE}px;border-radius:50%;box-sizing:border-box;
|
|
1031
|
+
border:2px dashed ${color};opacity:.25;display:inline-block;flex-shrink:0;
|
|
1032
|
+
"></span>
|
|
1033
|
+
<strong style="font-size:12px;color:${color};flex-shrink:0;">${PROBE_LABELS[id]}</strong>
|
|
1034
|
+
${levelHint}${fiberHint}
|
|
1035
|
+
<span style="color:#A9ABAF;font-family:monospace;font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><${domTag}>${domText ? ' ' + domText : ''}</span>
|
|
1036
|
+
</div>
|
|
1037
|
+
<div style="padding:8px 10px;background:rgba(15,23,42,0.7);display:grid;gap:5px;">
|
|
1038
|
+
<div style="display:grid;grid-template-columns:52px 1fr;gap:4px;align-items:baseline;">
|
|
1039
|
+
<span style="color:#A9ABAF;font-size:10px;text-transform:uppercase;letter-spacing:.04em;">element</span>
|
|
1040
|
+
<span style="color:#FD9891;font-weight:600;font-family:monospace;font-size:11px;">${exactType}</span>
|
|
1041
|
+
</div>
|
|
1042
|
+
<div style="display:grid;grid-template-columns:52px 1fr;gap:4px;align-items:baseline;">
|
|
1043
|
+
<span style="color:#A9ABAF;font-size:10px;text-transform:uppercase;letter-spacing:.04em;">source</span>
|
|
1044
|
+
<span style="color:${exactSource ? '#F5CD47' : '#7D818A'};font-family:monospace;font-size:10px;">${exactSourceStr}</span>
|
|
1045
|
+
</div>
|
|
1046
|
+
<div style="height:1px;background:rgba(255,255,255,0.06);margin:2px 0;"></div>
|
|
1047
|
+
<div style="display:grid;grid-template-columns:52px 1fr;gap:4px;align-items:baseline;">
|
|
1048
|
+
<span style="color:#A9ABAF;font-size:10px;text-transform:uppercase;letter-spacing:.04em;">component</span>
|
|
1049
|
+
<span style="color:#8FB8F6;font-weight:600;font-size:11px;">${compName}</span>
|
|
1050
|
+
</div>
|
|
1051
|
+
<div style="display:grid;grid-template-columns:52px 1fr;gap:4px;align-items:baseline;">
|
|
1052
|
+
<span style="color:#A9ABAF;font-size:10px;text-transform:uppercase;letter-spacing:.04em;">file</span>
|
|
1053
|
+
<span style="color:${componentSource ? '#7EE2B8' : '#7D818A'};font-family:monospace;font-size:10px;">${componentSourceStr}</span>
|
|
1054
|
+
</div>
|
|
1055
|
+
${owners.length > 0 ? `
|
|
1056
|
+
<div style="display:grid;grid-template-columns:52px 1fr;gap:4px;align-items:baseline;">
|
|
1057
|
+
<span style="color:#A9ABAF;font-size:10px;text-transform:uppercase;letter-spacing:.04em;">owners</span>
|
|
1058
|
+
<span style="line-height:1.8;">${ownerButtons || '<span style="color:#A9ABAF;">—</span>'}</span>
|
|
1059
|
+
</div>` : ''}
|
|
1060
|
+
${renderContext(markerContext, id)}
|
|
1061
|
+
${Object.keys(props).filter(k => !['children','style','className'].includes(k)).length > 0 ? `
|
|
1062
|
+
<div style="display:grid;grid-template-columns:52px 1fr;gap:4px;align-items:baseline;">
|
|
1063
|
+
<span style="color:#A9ABAF;font-size:10px;text-transform:uppercase;letter-spacing:.04em;">props</span>
|
|
1064
|
+
<span style="font-family:monospace;font-size:10px;line-height:1.6;">${propsStr}</span>
|
|
1065
|
+
</div>` : ''}
|
|
1066
|
+
<div style="height:1px;background:rgba(255,255,255,0.06);margin:3px 0;"></div>
|
|
1067
|
+
<div data-chippy-key="probe-annotation-${id}" data-chippy-probe-annotation="${id}" style="display:grid;gap:5px;">
|
|
1068
|
+
<select data-chippy-key="probe-intent-${id}" data-chippy-probe-intent="${id}" aria-label="${PROBE_LABELS[id]} intent" style="
|
|
1069
|
+
width:100%;border:1px solid rgba(148,163,184,.25);border-radius:6px;padding:5px 7px;
|
|
1070
|
+
background:#1F1F21;color:#CECFD2;font:11px 'Atlassian Sans',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;pointer-events:auto;
|
|
1071
|
+
">${intentOptions}</select>
|
|
1072
|
+
<textarea data-chippy-key="probe-comment-${id}" data-chippy-probe-comment="${id}" aria-label="${PROBE_LABELS[id]} comment" maxlength="2000"
|
|
1073
|
+
placeholder="What should Chippy do with this probe?" style="
|
|
1074
|
+
box-sizing:border-box;width:100%;min-height:54px;resize:vertical;
|
|
1075
|
+
border:1px solid rgba(148,163,184,.25);border-radius:6px;padding:6px 7px;
|
|
1076
|
+
background:#1F1F21;color:#CECFD2;font:11px/1.4 'Atlassian Sans',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;pointer-events:auto;
|
|
1077
|
+
">${escapeAttr(comment)}</textarea>
|
|
1078
|
+
</div>
|
|
1079
|
+
</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
`;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const CONN_STYLES = {
|
|
1085
|
+
connected: { color: '#4BCE97', label: '● connected' },
|
|
1086
|
+
reconnecting: { color: '#F5CD47', label: '● reconnecting' },
|
|
1087
|
+
failed: { color: '#F87168', label: '● helper unavailable' },
|
|
1088
|
+
unknown: { color: '#A9ABAF', label: '○ unknown' },
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
function getConnState() {
|
|
1092
|
+
return chippy.connState || 'unknown';
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function escapeAttr(value) {
|
|
1096
|
+
return String(value ?? '')
|
|
1097
|
+
.replace(/&/g, '&')
|
|
1098
|
+
.replace(/"/g, '"')
|
|
1099
|
+
.replace(/</g, '<')
|
|
1100
|
+
.replace(/>/g, '>');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function getConnTitle(state) {
|
|
1104
|
+
const helperBaseUrl = chippy.config?.helperBaseUrl;
|
|
1105
|
+
if (state === 'connected' && helperBaseUrl) return `Connected to ${helperBaseUrl}`;
|
|
1106
|
+
if (state === 'reconnecting' && helperBaseUrl) return `Reconnecting to ${helperBaseUrl}`;
|
|
1107
|
+
if (state === 'failed' && helperBaseUrl) return `Helper unavailable at ${helperBaseUrl}`;
|
|
1108
|
+
return CONN_STYLES[state]?.label || CONN_STYLES.unknown.label;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function renderConnPill() {
|
|
1112
|
+
const s = getConnState();
|
|
1113
|
+
const style = CONN_STYLES[s] || CONN_STYLES.unknown;
|
|
1114
|
+
const title = escapeAttr(getConnTitle(s));
|
|
1115
|
+
return `<span data-chippy-key="connection-pill" data-chippy-conn-pill title="${title}" style="
|
|
1116
|
+
font-size:10px;color:${style.color};font-family:monospace;letter-spacing:.04em;
|
|
1117
|
+
">${style.label}</span>`;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function isExtensionIntegration() {
|
|
1121
|
+
const integration = chippy.config?.integration || window.__CHIPPY_OWNER__?.integration || null;
|
|
1122
|
+
return integration?.kind === 'extension';
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function renderSettingsSection() {
|
|
1126
|
+
if (!isExtensionIntegration() || !panelState.settingsExpanded) return '';
|
|
1127
|
+
return `
|
|
1128
|
+
<div data-chippy-key="settings-section" data-chippy-settings-section style="
|
|
1129
|
+
padding:10px;border-radius:12px;background:rgba(30,41,59,.55);
|
|
1130
|
+
border:1px solid rgba(148,163,184,.12);
|
|
1131
|
+
">
|
|
1132
|
+
<div data-chippy-key="settings-slot" data-chippy-settings-slot style="min-height:176px;"></div>
|
|
1133
|
+
</div>
|
|
1134
|
+
`;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function renderExpandedPanel(sideLabel) {
|
|
1138
|
+
const settingsButton = isExtensionIntegration() ? `
|
|
1139
|
+
<button type="button" data-chippy-key="settings-toggle" data-chippy-settings-toggle aria-label="${panelState.settingsExpanded ? 'Hide' : 'Show'} Chippy settings"
|
|
1140
|
+
aria-expanded="${panelState.settingsExpanded ? 'true' : 'false'}" style="
|
|
1141
|
+
border:0;background:${panelState.settingsExpanded ? 'rgba(59,130,246,.22)' : 'rgba(255,255,255,.08)'};
|
|
1142
|
+
color:${panelState.settingsExpanded ? '#B8D6FF' : '#A9ABAF'};border-radius:7px;padding:4px 7px;
|
|
1143
|
+
cursor:pointer;font-size:12px;line-height:1;pointer-events:auto;
|
|
1144
|
+
" title="Settings">⚙</button>
|
|
1145
|
+
` : '';
|
|
1146
|
+
|
|
1147
|
+
return `
|
|
1148
|
+
<div data-chippy-key="expanded-panel" data-chippy-expanded-panel style="display:grid;gap:10px;">
|
|
1149
|
+
<div data-chippy-key="panel-header" style="display:flex;justify-content:space-between;align-items:center;padding-bottom:7px;border-bottom:1px solid rgba(255,255,255,0.07);gap:8px;">
|
|
1150
|
+
<div style="display:flex;align-items:center;gap:7px;min-width:0;">
|
|
1151
|
+
${CHIPPY_LOGO}
|
|
1152
|
+
<div style="font-weight:650;font-size:14px;color:#CECFD2;">Chippy</div>
|
|
1153
|
+
${renderConnPill()}
|
|
1154
|
+
</div>
|
|
1155
|
+
<div style="display:flex;align-items:center;gap:6px;">
|
|
1156
|
+
${settingsButton}
|
|
1157
|
+
<button type="button" data-chippy-key="toggle-side" data-layout-lab-toggle-side aria-label="Move Chippy panel to the ${panelState.side === 'right' ? 'left' : 'right'}" style="
|
|
1158
|
+
border:0;background:rgba(255,255,255,0.08);color:#A9ABAF;
|
|
1159
|
+
border-radius:7px;padding:4px 7px;cursor:pointer;font-size:11px;line-height:1;
|
|
1160
|
+
pointer-events:auto;
|
|
1161
|
+
">${sideLabel}</button>
|
|
1162
|
+
<button type="button" data-chippy-key="collapse-panel" data-chippy-panel-collapse aria-label="Collapse Chippy" style="
|
|
1163
|
+
border:0;background:rgba(255,255,255,0.08);color:#A9ABAF;
|
|
1164
|
+
border-radius:7px;padding:4px 7px;cursor:pointer;font-size:14px;line-height:1;
|
|
1165
|
+
pointer-events:auto;
|
|
1166
|
+
">−</button>
|
|
1167
|
+
</div>
|
|
1168
|
+
</div>
|
|
1169
|
+
<div data-chippy-key="probe-dock" data-layout-lab-dock="true" data-chippy-ui="true" style="
|
|
1170
|
+
position:relative;display:grid;gap:6px;pointer-events:auto;
|
|
1171
|
+
">${MARKER_IDS.map(renderProbeRow).join('')}</div>
|
|
1172
|
+
<div data-chippy-key="panel-help" style="font-size:10px;color:#A9ABAF;text-align:center;padding-top:1px;">← / → DOM levels · ↑ / ↓ fiber parents · click owner to jump</div>
|
|
1173
|
+
${renderSettingsSection()}
|
|
1174
|
+
</div>
|
|
1175
|
+
`;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function patchPanelContent(panel, html) {
|
|
1179
|
+
if (panel.__chippyRenderedHtml === html) return false;
|
|
1180
|
+
const patchChildren = global.__CHIPPY_PANEL_DOM__?.patchChildren;
|
|
1181
|
+
if (typeof patchChildren === 'function') patchChildren(panel, html);
|
|
1182
|
+
else panel.innerHTML = html; // Minimal degradation for legacy direct-script consumers.
|
|
1183
|
+
panel.__chippyRenderedHtml = html;
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function bindPanelEvent(node, type, listener) {
|
|
1188
|
+
if (!node) return;
|
|
1189
|
+
const slot = `__chippyPanelListener_${type}`;
|
|
1190
|
+
if (node[slot]) node.removeEventListener(type, node[slot]);
|
|
1191
|
+
node[slot] = listener;
|
|
1192
|
+
node.addEventListener(type, listener);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function requestSettingsMount(panel) {
|
|
1196
|
+
if (!panel.querySelector('[data-chippy-settings-slot]')) return;
|
|
1197
|
+
document.dispatchEvent(new Event('chippy-settings-mount-request'));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Subscribe once: re-render panel on conn-state change.
|
|
1201
|
+
// Guard: only update if Chippy UI is already present (i.e. not cleared after disable).
|
|
1202
|
+
if (!chippy.__connStateListenerInstalled) {
|
|
1203
|
+
chippy.__connStateListenerInstalled = true;
|
|
1204
|
+
global.addEventListener('chippy-conn-state', () => {
|
|
1205
|
+
try {
|
|
1206
|
+
if (document.querySelector('[data-layout-lab-panel]')) updatePanel();
|
|
1207
|
+
} catch {}
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function updatePanel() {
|
|
1212
|
+
const panel = ensurePanel();
|
|
1213
|
+
if (!panel) return;
|
|
1214
|
+
applyPanelStyle(panel);
|
|
1215
|
+
|
|
1216
|
+
if (!panelState.expanded) {
|
|
1217
|
+
const style = CONN_STYLES[getConnState()] || CONN_STYLES.unknown;
|
|
1218
|
+
const collapsedHtml = `
|
|
1219
|
+
<button type="button" data-chippy-panel-launcher aria-label="Open Chippy" title="Open Chippy" style="
|
|
1220
|
+
position:relative;width:${PANEL_LAUNCHER_SIZE}px;height:${PANEL_LAUNCHER_SIZE}px;
|
|
1221
|
+
display:flex;align-items:center;justify-content:center;border:1px solid rgba(148,163,184,.22);
|
|
1222
|
+
border-radius:50%;background:#1F1F21;color:#CECFD2;cursor:pointer;
|
|
1223
|
+
box-shadow:0 10px 28px rgba(0,0,0,.32);font-size:20px;line-height:1;pointer-events:auto;
|
|
1224
|
+
">
|
|
1225
|
+
${CHIPPY_LOGO}
|
|
1226
|
+
<span aria-hidden="true" style="position:absolute;right:2px;bottom:3px;width:9px;height:9px;border-radius:50%;background:${style.color};border:2px solid #1F1F21;"></span>
|
|
1227
|
+
</button>
|
|
1228
|
+
`;
|
|
1229
|
+
patchPanelContent(panel, collapsedHtml);
|
|
1230
|
+
bindPanelEvent(
|
|
1231
|
+
panel.querySelector('[data-chippy-panel-launcher]'),
|
|
1232
|
+
'click',
|
|
1233
|
+
() => setPanelExpanded(true),
|
|
1234
|
+
);
|
|
1235
|
+
syncDockedMarkers();
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const sideLabel = panelState.side === 'right' ? '◀' : '▶';
|
|
1240
|
+
const html = renderExpandedPanel(sideLabel);
|
|
1241
|
+
patchPanelContent(panel, html);
|
|
1242
|
+
bindPanelEvent(panel.querySelector('[data-chippy-panel-collapse]'), 'click', () => setPanelExpanded(false));
|
|
1243
|
+
bindPanelEvent(panel.querySelector('[data-chippy-settings-toggle]'), 'click', () => {
|
|
1244
|
+
panelState.settingsExpanded = !panelState.settingsExpanded;
|
|
1245
|
+
updatePanel();
|
|
1246
|
+
emitLabEvent(panelState.settingsExpanded ? 'settings-opened' : 'settings-closed', {
|
|
1247
|
+
settingsExpanded: panelState.settingsExpanded,
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
bindPanelEvent(panel.querySelector('[data-layout-lab-toggle-side]'), 'click', () => {
|
|
1251
|
+
togglePanelSide();
|
|
1252
|
+
updatePanel();
|
|
1253
|
+
});
|
|
1254
|
+
panel.querySelectorAll('[data-chippy-probe-intent]').forEach((intentInput) => {
|
|
1255
|
+
bindPanelEvent(intentInput, 'change', (event) => {
|
|
1256
|
+
event.stopPropagation();
|
|
1257
|
+
const markerId = intentInput.getAttribute('data-chippy-probe-intent');
|
|
1258
|
+
const commentInput = panel.querySelector(`[data-chippy-probe-comment="${markerId}"]`);
|
|
1259
|
+
setMarkerAnnotation(markerId, {
|
|
1260
|
+
intent: intentInput?.value || null,
|
|
1261
|
+
comment: commentInput?.value || '',
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
panel.querySelectorAll('[data-chippy-probe-comment]').forEach((commentInput) => {
|
|
1266
|
+
bindPanelEvent(commentInput, 'keydown', (event) => {
|
|
1267
|
+
if (event.key !== 'Enter' || event.shiftKey || event.isComposing) return;
|
|
1268
|
+
event.preventDefault();
|
|
1269
|
+
event.stopPropagation();
|
|
1270
|
+
const markerId = commentInput.getAttribute('data-chippy-probe-comment');
|
|
1271
|
+
const intentInput = panel.querySelector(`[data-chippy-probe-intent="${markerId}"]`);
|
|
1272
|
+
setMarkerAnnotation(markerId, {
|
|
1273
|
+
intent: intentInput?.value || null,
|
|
1274
|
+
comment: commentInput?.value || '',
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
});
|
|
1278
|
+
// Wire declarative-boundary clicks (owner / error / suspense). These are NOT
|
|
1279
|
+
// the React render owner — they are the boundaries that define the collateral
|
|
1280
|
+
// scope. Clicking reports the boundary + its fiber-hop distance to the Chippy
|
|
1281
|
+
// backend via a lab event, so it can investigate the boundary later.
|
|
1282
|
+
const boundaryEls = panel.querySelectorAll('[data-chippy-boundary]');
|
|
1283
|
+
boundaryEls.forEach((el) => {
|
|
1284
|
+
const activate = (e) => {
|
|
1285
|
+
e.stopPropagation();
|
|
1286
|
+
const kind = el.getAttribute('data-chippy-boundary');
|
|
1287
|
+
const markerId = el.getAttribute('data-chippy-boundary-marker');
|
|
1288
|
+
if (!markerId) return;
|
|
1289
|
+
// Persistent selected feedback: clear siblings, mark this one selected,
|
|
1290
|
+
// so the click is visibly acknowledged and the active boundary is clear.
|
|
1291
|
+
boundaryEls.forEach((other) => {
|
|
1292
|
+
other.removeAttribute('data-chippy-boundary-selected');
|
|
1293
|
+
other.style.outline = '';
|
|
1294
|
+
other.style.outlineOffset = '';
|
|
1295
|
+
});
|
|
1296
|
+
el.setAttribute('data-chippy-boundary-selected', 'true');
|
|
1297
|
+
el.style.outline = '2px solid #CECFD2';
|
|
1298
|
+
el.style.outlineOffset = '1px';
|
|
1299
|
+
// Show WHERE the boundary is: highlight its DOM subtree on the page and
|
|
1300
|
+
// scroll it into view. This is the on-page counterpart to the lozenge feedback.
|
|
1301
|
+
highlightBoundary(markerId, kind);
|
|
1302
|
+
const markerEl = document.querySelector(`[data-layout-lab-marker="${markerId}"]`);
|
|
1303
|
+
const markerContext = describeContext(markerEl?.__probe?._bestFiber || null);
|
|
1304
|
+
const boundary = markerContext?.boundaries?.[kind] || null;
|
|
1305
|
+
emitLabEvent('inspect-boundary', {
|
|
1306
|
+
markerId,
|
|
1307
|
+
boundaryKind: kind,
|
|
1308
|
+
boundary,
|
|
1309
|
+
distance: boundary?.distance ?? null,
|
|
1310
|
+
});
|
|
1311
|
+
};
|
|
1312
|
+
bindPanelEvent(el, 'click', activate);
|
|
1313
|
+
bindPanelEvent(el, 'keydown', (e) => {
|
|
1314
|
+
if (e.key === 'Enter' || e.key === ' ') activate(e);
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
// Wire owner breadcrumb clicks — jump directly to that owner fiber
|
|
1318
|
+
panel.querySelectorAll('[data-chippy-owner-btn]').forEach((btn) => {
|
|
1319
|
+
bindPanelEvent(btn, 'click', (e) => {
|
|
1320
|
+
e.stopPropagation();
|
|
1321
|
+
const markerId = btn.getAttribute('data-chippy-owner-btn');
|
|
1322
|
+
const ownerIndex = parseInt(btn.getAttribute('data-owner-index'), 10);
|
|
1323
|
+
const markerEl = document.querySelector(`[data-layout-lab-marker="${markerId}"]`);
|
|
1324
|
+
if (!markerEl) return;
|
|
1325
|
+
const ownerFibers = markerEl.__ownerFibers;
|
|
1326
|
+
if (!ownerFibers || !ownerFibers[ownerIndex]) return;
|
|
1327
|
+
const targetFiber = ownerFibers[ownerIndex];
|
|
1328
|
+
const color = MARKER_COLORS[markerId] || '#A9ABAF';
|
|
1329
|
+
// Resolve the owner fiber into a full component description and pin it
|
|
1330
|
+
const component = describeComponent(targetFiber);
|
|
1331
|
+
const targetEl = getFiberDomNode(targetFiber) || markerEl.__baseEl;
|
|
1332
|
+
markerEl.__probe = {
|
|
1333
|
+
...(markerEl.__probe || {}),
|
|
1334
|
+
_bestFiber: targetFiber,
|
|
1335
|
+
_targetEl: targetEl,
|
|
1336
|
+
best: {
|
|
1337
|
+
exact: describeFiberTarget(getNearestFiber(targetEl), targetEl),
|
|
1338
|
+
component,
|
|
1339
|
+
},
|
|
1340
|
+
};
|
|
1341
|
+
markerEl.__fiberOffset = 0; // reset fiber walk — we're now pinned to a specific owner
|
|
1342
|
+
markerEl.__fiberParents = getReturnComponentChain(targetFiber);
|
|
1343
|
+
updateHighlight(markerId, targetEl, color);
|
|
1344
|
+
storeMarkerAnchor(markerEl, targetEl);
|
|
1345
|
+
attachScrollListeners(markerId, targetEl, color);
|
|
1346
|
+
updateParentBorder();
|
|
1347
|
+
updatePanel();
|
|
1348
|
+
emitLabEvent('marker-owner-jump', { markerId, ownerIndex });
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1351
|
+
syncDockedMarkers();
|
|
1352
|
+
requestSettingsMount(panel);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
1356
|
+
|
|
1357
|
+
function getMarkerProbe(id) {
|
|
1358
|
+
const marker = document.querySelector(`[data-layout-lab-marker="${id}"]`);
|
|
1359
|
+
if (!marker) return null;
|
|
1360
|
+
const annotation = normalizeProbeAnnotation({ intent: marker.__intent, comment: marker.__comment });
|
|
1361
|
+
return {
|
|
1362
|
+
id,
|
|
1363
|
+
...annotation,
|
|
1364
|
+
probe: marker.__probe || null,
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function exposeDefaultMarkers() {
|
|
1369
|
+
// Clean up any existing markers + highlights + parent border
|
|
1370
|
+
document.querySelectorAll('[data-layout-lab-marker]').forEach((marker) => marker.remove());
|
|
1371
|
+
document.querySelectorAll('[data-layout-lab-highlight]').forEach((el) => el.remove());
|
|
1372
|
+
document.querySelector('[data-layout-lab-parent-border]')?.remove();
|
|
1373
|
+
if (_parentBorderEl) _parentBorderEl = null;
|
|
1374
|
+
Object.keys(_highlights).forEach((k) => delete _highlights[k]);
|
|
1375
|
+
|
|
1376
|
+
ensurePanel();
|
|
1377
|
+
ensureDock();
|
|
1378
|
+
// Markers always start docked. In the unified UI they stay hidden until the
|
|
1379
|
+
// launcher expands; Storybook places them in its standalone preview dock.
|
|
1380
|
+
MARKER_IDS.forEach((id, index) => createMarker(id, index));
|
|
1381
|
+
updatePanel();
|
|
1382
|
+
emitLabEvent('markers-exposed', { markers: getAllMarkerStates() });
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function clearAll() {
|
|
1386
|
+
panelState.expanded = false;
|
|
1387
|
+
panelState.settingsExpanded = false;
|
|
1388
|
+
MARKER_IDS.forEach((id) => detachScrollListeners(id));
|
|
1389
|
+
document.querySelector('[data-layout-lab-dock]')?.remove();
|
|
1390
|
+
document.querySelector('[data-layout-lab-panel]')?.remove();
|
|
1391
|
+
document.querySelectorAll('[data-layout-lab-marker]').forEach((marker) => marker.remove());
|
|
1392
|
+
document.querySelectorAll('[data-layout-lab-highlight]').forEach((el) => el.remove());
|
|
1393
|
+
document.querySelector('[data-layout-lab-parent-border]')?.remove();
|
|
1394
|
+
if (_parentBorderEl) _parentBorderEl = null;
|
|
1395
|
+
Object.keys(_highlights).forEach((k) => delete _highlights[k]);
|
|
1396
|
+
emitLabEvent('markers-cleared');
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// ─── Style inspection ─────────────────────────────────────────────────────
|
|
1400
|
+
// getComputedStyleAt({ probeId, parentLevel?, selector? })
|
|
1401
|
+
// probeId — '1' or '2'
|
|
1402
|
+
// parentLevel — 0 = probe target, 1 = direct parent, N = Nth ancestor (default 0)
|
|
1403
|
+
// selector — optional querySelector within the resolved node
|
|
1404
|
+
// Returns full getComputedStyle output as a plain object.
|
|
1405
|
+
|
|
1406
|
+
function getComputedStyleAt({ probeId, parentLevel = 0, selector } = {}) {
|
|
1407
|
+
const probe = chippy.layoutLab?.getMarkerProbe(String(probeId));
|
|
1408
|
+
const targetEl = probe?.probe?._targetEl || null;
|
|
1409
|
+
if (!targetEl) return { error: `No target element for probe ${probeId}` };
|
|
1410
|
+
|
|
1411
|
+
// Walk up parentLevel ancestors
|
|
1412
|
+
let el = targetEl;
|
|
1413
|
+
for (let i = 0; i < parentLevel; i++) {
|
|
1414
|
+
if (!el.parentElement) return { error: `parentLevel ${parentLevel} exceeds DOM depth (stopped at ${i})` };
|
|
1415
|
+
el = el.parentElement;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Optional scoped querySelector
|
|
1419
|
+
if (selector) {
|
|
1420
|
+
const found = el.querySelector(selector);
|
|
1421
|
+
if (!found) return { error: `selector "${selector}" not found within node at parentLevel ${parentLevel}` };
|
|
1422
|
+
el = found;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const cs = window.getComputedStyle(el);
|
|
1426
|
+
const result = {};
|
|
1427
|
+
for (const prop of cs) result[prop] = cs.getPropertyValue(prop);
|
|
1428
|
+
return result;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
chippy.getComputedStyleAt = getComputedStyleAt;
|
|
1432
|
+
|
|
1433
|
+
chippy.layoutLab = {
|
|
1434
|
+
exposeDefaultMarkers,
|
|
1435
|
+
clearAll,
|
|
1436
|
+
getMarkerProbe,
|
|
1437
|
+
getCommonParentInspection,
|
|
1438
|
+
getHookSummary,
|
|
1439
|
+
getMarkerState,
|
|
1440
|
+
getAllMarkerStates,
|
|
1441
|
+
setMarkerAnnotation,
|
|
1442
|
+
probePoint,
|
|
1443
|
+
updatePanel,
|
|
1444
|
+
removeInPageUi,
|
|
1445
|
+
togglePanelSide,
|
|
1446
|
+
snapToDock: (id) => {
|
|
1447
|
+
const marker = document.querySelector(`[data-layout-lab-marker="${id}"]`);
|
|
1448
|
+
if (marker) snapToDock(marker);
|
|
1449
|
+
},
|
|
1450
|
+
markerIds: [...MARKER_IDS],
|
|
1451
|
+
markerColors: { ...MARKER_COLORS },
|
|
1452
|
+
};
|
|
1453
|
+
})(window);
|