angular-grab 0.1.0 → 0.1.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 +215 -0
- package/examples/angular-19-app/.editorconfig +17 -0
- package/examples/angular-19-app/.vscode/extensions.json +4 -0
- package/examples/angular-19-app/.vscode/launch.json +20 -0
- package/examples/angular-19-app/.vscode/mcp.json +9 -0
- package/examples/angular-19-app/.vscode/tasks.json +42 -0
- package/examples/angular-19-app/README.md +59 -0
- package/examples/angular-19-app/angular.json +74 -0
- package/examples/angular-19-app/package.json +44 -0
- package/examples/angular-19-app/public/favicon.ico +0 -0
- package/examples/angular-19-app/src/app/app.config.ts +13 -0
- package/examples/angular-19-app/src/app/app.css +37 -0
- package/examples/angular-19-app/src/app/app.html +25 -0
- package/examples/angular-19-app/src/app/app.routes.ts +3 -0
- package/examples/angular-19-app/src/app/app.spec.ts +23 -0
- package/examples/angular-19-app/src/app/app.ts +12 -0
- package/examples/angular-19-app/src/app/button/button.component.ts +25 -0
- package/examples/angular-19-app/src/app/card/card.component.ts +33 -0
- package/examples/angular-19-app/src/app/header/header.component.ts +31 -0
- package/examples/angular-19-app/src/app/popover/popover.component.ts +133 -0
- package/examples/angular-19-app/src/index.html +13 -0
- package/examples/angular-19-app/src/main.ts +6 -0
- package/examples/angular-19-app/src/styles.css +1 -0
- package/examples/angular-19-app/tsconfig.app.json +15 -0
- package/examples/angular-19-app/tsconfig.json +33 -0
- package/examples/angular-19-app/tsconfig.spec.json +15 -0
- package/package.json +14 -111
- package/packages/angular-grab/package.json +96 -0
- package/packages/angular-grab/src/angular/__tests__/context-builder.test.ts +216 -0
- package/packages/angular-grab/src/angular/angular-grab.service.ts +62 -0
- package/packages/angular-grab/src/angular/index.ts +13 -0
- package/packages/angular-grab/src/angular/provide-angular-grab.ts +22 -0
- package/packages/angular-grab/src/angular/resolvers/component-resolver.ts +71 -0
- package/packages/angular-grab/src/angular/resolvers/context-builder.ts +86 -0
- package/packages/angular-grab/src/angular/resolvers/ng-utils.ts +14 -0
- package/packages/angular-grab/src/angular/resolvers/source-resolver.ts +61 -0
- package/packages/angular-grab/src/builder/__tests__/builder.test.ts +72 -0
- package/packages/angular-grab/src/builder/builders/application/index.ts +13 -0
- package/packages/angular-grab/src/builder/builders/dev-server/index.ts +9 -0
- package/packages/angular-grab/src/builder/index.ts +3 -0
- package/packages/angular-grab/src/cli/__tests__/cli.test.ts +239 -0
- package/packages/angular-grab/src/cli/commands/init.ts +106 -0
- package/packages/angular-grab/src/cli/index.ts +15 -0
- package/packages/angular-grab/src/cli/utils/detect-project.ts +78 -0
- package/packages/angular-grab/src/cli/utils/modify-angular-json.ts +42 -0
- package/packages/angular-grab/src/cli/utils/modify-app-config.ts +42 -0
- package/packages/angular-grab/src/core/__tests__/generate-snippet.test.ts +149 -0
- package/packages/angular-grab/src/core/__tests__/plugin-registry.test.ts +286 -0
- package/packages/angular-grab/src/core/__tests__/store.test.ts +118 -0
- package/packages/angular-grab/src/core/__tests__/utils.test.ts +85 -0
- package/packages/angular-grab/src/core/clipboard/copy.ts +104 -0
- package/packages/angular-grab/src/core/clipboard/generate-snippet.ts +38 -0
- package/packages/angular-grab/src/core/constants.ts +10 -0
- package/packages/angular-grab/src/core/grab.ts +596 -0
- package/packages/angular-grab/src/core/index.global.ts +13 -0
- package/packages/angular-grab/src/core/index.ts +19 -0
- package/packages/angular-grab/src/core/keyboard/keyboard-handler.ts +163 -0
- package/packages/angular-grab/src/core/overlay/crosshair.ts +107 -0
- package/packages/angular-grab/src/core/overlay/freeze-overlay.ts +239 -0
- package/packages/angular-grab/src/core/overlay/overlay-renderer.ts +180 -0
- package/packages/angular-grab/src/core/overlay/select-feedback.ts +108 -0
- package/packages/angular-grab/src/core/overlay/toast.ts +175 -0
- package/packages/angular-grab/src/core/picker/element-picker.ts +114 -0
- package/packages/angular-grab/src/core/plugins/plugin-registry.ts +83 -0
- package/packages/angular-grab/src/core/store.ts +52 -0
- package/packages/angular-grab/src/core/toolbar/actions-menu.ts +178 -0
- package/packages/angular-grab/src/core/toolbar/comment-popover.ts +235 -0
- package/packages/angular-grab/src/core/toolbar/copy-actions.ts +98 -0
- package/packages/angular-grab/src/core/toolbar/history-popover.ts +245 -0
- package/packages/angular-grab/src/core/toolbar/theme-manager.ts +188 -0
- package/packages/angular-grab/src/core/toolbar/toolbar-icons.ts +29 -0
- package/packages/angular-grab/src/core/toolbar/toolbar-renderer.ts +239 -0
- package/packages/angular-grab/src/core/types.ts +139 -0
- package/packages/angular-grab/src/core/utils.ts +16 -0
- package/packages/angular-grab/src/esbuild-plugin/__tests__/transform.test.ts +174 -0
- package/packages/angular-grab/src/esbuild-plugin/index.ts +3 -0
- package/packages/angular-grab/src/esbuild-plugin/plugin.ts +29 -0
- package/packages/angular-grab/src/esbuild-plugin/scan.ts +105 -0
- package/packages/angular-grab/src/esbuild-plugin/transform.ts +152 -0
- package/packages/angular-grab/src/vite-plugin/__tests__/plugin.test.ts +84 -0
- package/packages/angular-grab/src/vite-plugin/index.ts +19 -0
- package/packages/angular-grab/src/webpack-plugin/__tests__/plugin.test.ts +72 -0
- package/packages/angular-grab/src/webpack-plugin/index.ts +2 -0
- package/packages/angular-grab/src/webpack-plugin/loader.ts +15 -0
- package/packages/angular-grab/src/webpack-plugin/plugin.ts +20 -0
- package/packages/angular-grab/tsconfig.json +15 -0
- package/packages/angular-grab/tsup.config.ts +119 -0
- package/pnpm-workspace.yaml +3 -0
- package/turbo.json +21 -0
- package/dist/angular/index.d.ts +0 -151
- package/dist/angular/index.js +0 -2811
- package/dist/angular/index.js.map +0 -1
- package/dist/builder/builders/application/index.js +0 -143
- package/dist/builder/builders/application/index.js.map +0 -1
- package/dist/builder/builders/dev-server/index.js +0 -139
- package/dist/builder/builders/dev-server/index.js.map +0 -1
- package/dist/builder/index.js +0 -2
- package/dist/builder/index.js.map +0 -1
- package/dist/builder/package.json +0 -1
- package/dist/cli/index.js +0 -223
- package/dist/cli/index.js.map +0 -1
- package/dist/core/index.cjs +0 -2589
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -139
- package/dist/core/index.d.ts +0 -139
- package/dist/core/index.global.js +0 -542
- package/dist/core/index.js +0 -2560
- package/dist/core/index.js.map +0 -1
- package/dist/esbuild-plugin/index.cjs +0 -239
- package/dist/esbuild-plugin/index.cjs.map +0 -1
- package/dist/esbuild-plugin/index.d.cts +0 -26
- package/dist/esbuild-plugin/index.d.ts +0 -26
- package/dist/esbuild-plugin/index.js +0 -200
- package/dist/esbuild-plugin/index.js.map +0 -1
- package/dist/vite-plugin/index.d.ts +0 -7
- package/dist/vite-plugin/index.js +0 -128
- package/dist/vite-plugin/index.js.map +0 -1
- package/dist/webpack-plugin/index.cjs +0 -54
- package/dist/webpack-plugin/index.cjs.map +0 -1
- package/dist/webpack-plugin/index.d.cts +0 -5
- package/dist/webpack-plugin/index.d.ts +0 -5
- package/dist/webpack-plugin/index.js +0 -23
- package/dist/webpack-plugin/index.js.map +0 -1
- package/dist/webpack-plugin/loader.cjs +0 -155
- package/dist/webpack-plugin/loader.cjs.map +0 -1
- package/dist/webpack-plugin/loader.d.cts +0 -3
- package/dist/webpack-plugin/loader.d.ts +0 -3
- package/dist/webpack-plugin/loader.js +0 -122
- package/dist/webpack-plugin/loader.js.map +0 -1
- /package/{builders.json → packages/angular-grab/builders.json} +0 -0
- /package/{dist → packages/angular-grab/src}/builder/builders/application/schema.json +0 -0
- /package/{dist → packages/angular-grab/src}/builder/builders/dev-server/schema.json +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export interface ParsedKey {
|
|
2
|
+
key: string;
|
|
3
|
+
meta: boolean;
|
|
4
|
+
ctrl: boolean;
|
|
5
|
+
shift: boolean;
|
|
6
|
+
alt: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface KeyboardHandler {
|
|
10
|
+
start(): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface KeyboardHandlerDeps {
|
|
16
|
+
getActivationKey: () => string;
|
|
17
|
+
getActivationMode: () => 'hold' | 'toggle';
|
|
18
|
+
getKeyHoldDuration: () => number;
|
|
19
|
+
getEnableInInputs: () => boolean;
|
|
20
|
+
onActivate: () => void;
|
|
21
|
+
onDeactivate: () => void;
|
|
22
|
+
isActive: () => boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isMac(): boolean {
|
|
26
|
+
if (typeof navigator === 'undefined') return false;
|
|
27
|
+
const uaData = (navigator as any).userAgentData;
|
|
28
|
+
if (uaData?.platform) return /mac/i.test(uaData.platform);
|
|
29
|
+
return /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseKeyCombo(combo: string): ParsedKey {
|
|
33
|
+
const parts = combo.split('+').map((s) => s.trim());
|
|
34
|
+
const result: ParsedKey = {
|
|
35
|
+
key: '',
|
|
36
|
+
meta: false,
|
|
37
|
+
ctrl: false,
|
|
38
|
+
shift: false,
|
|
39
|
+
alt: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
const lower = part.toLowerCase();
|
|
44
|
+
if (lower === 'meta' || lower === 'cmd' || lower === 'command') {
|
|
45
|
+
result.meta = true;
|
|
46
|
+
} else if (lower === 'ctrl' || lower === 'control') {
|
|
47
|
+
result.ctrl = true;
|
|
48
|
+
} else if (lower === 'shift') {
|
|
49
|
+
result.shift = true;
|
|
50
|
+
} else if (lower === 'alt' || lower === 'option') {
|
|
51
|
+
result.alt = true;
|
|
52
|
+
} else {
|
|
53
|
+
result.key = lower;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function matchesCombo(e: KeyboardEvent, parsed: ParsedKey): boolean {
|
|
61
|
+
if (parsed.meta && !e.metaKey) return false;
|
|
62
|
+
if (parsed.ctrl && !e.ctrlKey) return false;
|
|
63
|
+
if (parsed.shift && !e.shiftKey) return false;
|
|
64
|
+
if (parsed.alt && !e.altKey) return false;
|
|
65
|
+
|
|
66
|
+
return e.key.toLowerCase() === parsed.key;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isInputElement(el: EventTarget | null): boolean {
|
|
70
|
+
if (!el || !(el instanceof HTMLElement)) return false;
|
|
71
|
+
const tag = el.tagName;
|
|
72
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createKeyboardHandler(deps: KeyboardHandlerDeps): KeyboardHandler {
|
|
76
|
+
let holdTimer: ReturnType<typeof setTimeout> | null = null;
|
|
77
|
+
let holdActivated = false;
|
|
78
|
+
let listening = false;
|
|
79
|
+
|
|
80
|
+
function handleKeyDown(e: KeyboardEvent): void {
|
|
81
|
+
if (!deps.getEnableInInputs() && isInputElement(e.target)) return;
|
|
82
|
+
|
|
83
|
+
const parsed = parseKeyCombo(deps.getActivationKey());
|
|
84
|
+
if (!matchesCombo(e, parsed)) return;
|
|
85
|
+
|
|
86
|
+
const mode = deps.getActivationMode();
|
|
87
|
+
const holdDuration = deps.getKeyHoldDuration();
|
|
88
|
+
|
|
89
|
+
if (mode === 'hold') {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
|
|
92
|
+
if (holdActivated) return;
|
|
93
|
+
|
|
94
|
+
if (holdDuration > 0) {
|
|
95
|
+
if (holdTimer) return;
|
|
96
|
+
holdTimer = setTimeout(() => {
|
|
97
|
+
holdActivated = true;
|
|
98
|
+
deps.onActivate();
|
|
99
|
+
}, holdDuration);
|
|
100
|
+
} else {
|
|
101
|
+
holdActivated = true;
|
|
102
|
+
deps.onActivate();
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// toggle mode
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleKeyUp(e: KeyboardEvent): void {
|
|
111
|
+
const parsed = parseKeyCombo(deps.getActivationKey());
|
|
112
|
+
|
|
113
|
+
// For key-up we check if the released key matches the main key
|
|
114
|
+
if (e.key.toLowerCase() !== parsed.key) return;
|
|
115
|
+
|
|
116
|
+
const mode = deps.getActivationMode();
|
|
117
|
+
|
|
118
|
+
if (mode === 'hold') {
|
|
119
|
+
if (holdTimer) {
|
|
120
|
+
clearTimeout(holdTimer);
|
|
121
|
+
holdTimer = null;
|
|
122
|
+
}
|
|
123
|
+
if (holdActivated) {
|
|
124
|
+
holdActivated = false;
|
|
125
|
+
deps.onDeactivate();
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// toggle mode: toggle on key-up so we don't double-fire
|
|
129
|
+
if (deps.isActive()) {
|
|
130
|
+
deps.onDeactivate();
|
|
131
|
+
} else {
|
|
132
|
+
deps.onActivate();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
start(): void {
|
|
139
|
+
if (listening) return;
|
|
140
|
+
listening = true;
|
|
141
|
+
document.addEventListener('keydown', handleKeyDown, true);
|
|
142
|
+
document.addEventListener('keyup', handleKeyUp, true);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
stop(): void {
|
|
146
|
+
if (!listening) return;
|
|
147
|
+
listening = false;
|
|
148
|
+
|
|
149
|
+
if (holdTimer) {
|
|
150
|
+
clearTimeout(holdTimer);
|
|
151
|
+
holdTimer = null;
|
|
152
|
+
}
|
|
153
|
+
holdActivated = false;
|
|
154
|
+
|
|
155
|
+
document.removeEventListener('keydown', handleKeyDown, true);
|
|
156
|
+
document.removeEventListener('keyup', handleKeyUp, true);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
dispose(): void {
|
|
160
|
+
this.stop();
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Z_INDEX_CROSSHAIR } from '../constants';
|
|
2
|
+
|
|
3
|
+
const CROSSHAIR_STYLE_ID = '__ag-crosshair-styles__';
|
|
4
|
+
const H_LINE_ID = '__ag-crosshair-h__';
|
|
5
|
+
const V_LINE_ID = '__ag-crosshair-v__';
|
|
6
|
+
|
|
7
|
+
export interface Crosshair {
|
|
8
|
+
activate(): void;
|
|
9
|
+
deactivate(): void;
|
|
10
|
+
isCrosshairElement(el: Element): boolean;
|
|
11
|
+
dispose(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createCrosshair(): Crosshair {
|
|
15
|
+
let hLine: HTMLDivElement | null = null;
|
|
16
|
+
let vLine: HTMLDivElement | null = null;
|
|
17
|
+
let listening = false;
|
|
18
|
+
|
|
19
|
+
function injectStyles(): void {
|
|
20
|
+
if (document.getElementById(CROSSHAIR_STYLE_ID)) return;
|
|
21
|
+
|
|
22
|
+
const style = document.createElement('style');
|
|
23
|
+
style.id = CROSSHAIR_STYLE_ID;
|
|
24
|
+
style.textContent = `
|
|
25
|
+
.ag-crosshair-line {
|
|
26
|
+
position: fixed;
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
z-index: ${Z_INDEX_CROSSHAIR};
|
|
29
|
+
background: var(--ag-accent, #3b82f6);
|
|
30
|
+
opacity: 0.25;
|
|
31
|
+
transition: none;
|
|
32
|
+
}
|
|
33
|
+
#${H_LINE_ID} {
|
|
34
|
+
left: 0;
|
|
35
|
+
right: 0;
|
|
36
|
+
height: 1px;
|
|
37
|
+
}
|
|
38
|
+
#${V_LINE_ID} {
|
|
39
|
+
top: 0;
|
|
40
|
+
bottom: 0;
|
|
41
|
+
width: 1px;
|
|
42
|
+
}
|
|
43
|
+
body.ag-crosshair-active {
|
|
44
|
+
cursor: crosshair !important;
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
document.head.appendChild(style);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureElements(): void {
|
|
51
|
+
if (!hLine) {
|
|
52
|
+
injectStyles();
|
|
53
|
+
hLine = document.createElement('div');
|
|
54
|
+
hLine.id = H_LINE_ID;
|
|
55
|
+
hLine.className = 'ag-crosshair-line';
|
|
56
|
+
document.body.appendChild(hLine);
|
|
57
|
+
}
|
|
58
|
+
if (!vLine) {
|
|
59
|
+
vLine = document.createElement('div');
|
|
60
|
+
vLine.id = V_LINE_ID;
|
|
61
|
+
vLine.className = 'ag-crosshair-line';
|
|
62
|
+
document.body.appendChild(vLine);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleMouseMove(e: MouseEvent): void {
|
|
67
|
+
if (hLine) {
|
|
68
|
+
hLine.style.top = `${e.clientY}px`;
|
|
69
|
+
}
|
|
70
|
+
if (vLine) {
|
|
71
|
+
vLine.style.left = `${e.clientX}px`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
activate(): void {
|
|
77
|
+
if (listening) return;
|
|
78
|
+
listening = true;
|
|
79
|
+
ensureElements();
|
|
80
|
+
document.body.classList.add('ag-crosshair-active');
|
|
81
|
+
document.addEventListener('mousemove', handleMouseMove, true);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
deactivate(): void {
|
|
85
|
+
if (!listening) return;
|
|
86
|
+
listening = false;
|
|
87
|
+
document.body.classList.remove('ag-crosshair-active');
|
|
88
|
+
document.removeEventListener('mousemove', handleMouseMove, true);
|
|
89
|
+
if (hLine) hLine.style.top = '-10px';
|
|
90
|
+
if (vLine) vLine.style.left = '-10px';
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
isCrosshairElement(el: Element): boolean {
|
|
94
|
+
return el === hLine || el === vLine
|
|
95
|
+
|| el.id === H_LINE_ID || el.id === V_LINE_ID;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
dispose(): void {
|
|
99
|
+
this.deactivate();
|
|
100
|
+
hLine?.remove();
|
|
101
|
+
vLine?.remove();
|
|
102
|
+
document.getElementById(CROSSHAIR_STYLE_ID)?.remove();
|
|
103
|
+
hLine = null;
|
|
104
|
+
vLine = null;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { Z_INDEX_FREEZE } from '../constants';
|
|
2
|
+
|
|
3
|
+
const FREEZE_ID = '__ag-freeze-overlay__';
|
|
4
|
+
const FREEZE_STYLE_ID = '__ag-freeze-styles__';
|
|
5
|
+
const HOVER_STYLE_ID = '__ag-freeze-hover-styles__';
|
|
6
|
+
const ANIM_STYLE_ID = '__ag-freeze-anim-styles__';
|
|
7
|
+
const HOVER_ATTR = 'data-ag-hover';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Events to block during freeze to prevent hover state changes.
|
|
11
|
+
* Adapted from react-grab's freeze-pseudo-states.ts
|
|
12
|
+
*/
|
|
13
|
+
const MOUSE_EVENTS_TO_BLOCK = [
|
|
14
|
+
'mouseenter', 'mouseleave', 'mouseover', 'mouseout',
|
|
15
|
+
'pointerenter', 'pointerleave', 'pointerover', 'pointerout',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
const FOCUS_EVENTS_TO_BLOCK = ['focus', 'blur', 'focusin', 'focusout'] as const;
|
|
19
|
+
|
|
20
|
+
export interface FreezeOverlay {
|
|
21
|
+
show(hoveredElement?: Element | null): void;
|
|
22
|
+
hide(): void;
|
|
23
|
+
isVisible(): boolean;
|
|
24
|
+
isFreezeElement(el: Element): boolean;
|
|
25
|
+
getElement(): HTMLDivElement | null;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createFreezeOverlay(): FreezeOverlay {
|
|
30
|
+
let overlay: HTMLDivElement | null = null;
|
|
31
|
+
let visible = false;
|
|
32
|
+
let hoverStyleEl: HTMLStyleElement | null = null;
|
|
33
|
+
let animStyleEl: HTMLStyleElement | null = null;
|
|
34
|
+
let markedElements: Element[] = [];
|
|
35
|
+
|
|
36
|
+
function injectStyles(): void {
|
|
37
|
+
if (document.getElementById(FREEZE_STYLE_ID)) return;
|
|
38
|
+
|
|
39
|
+
const style = document.createElement('style');
|
|
40
|
+
style.id = FREEZE_STYLE_ID;
|
|
41
|
+
style.textContent = `
|
|
42
|
+
#${FREEZE_ID} {
|
|
43
|
+
position: fixed;
|
|
44
|
+
top: 0;
|
|
45
|
+
left: 0;
|
|
46
|
+
width: 100vw;
|
|
47
|
+
height: 100vh;
|
|
48
|
+
z-index: ${Z_INDEX_FREEZE};
|
|
49
|
+
pointer-events: auto;
|
|
50
|
+
background: transparent;
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
document.head.appendChild(style);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ensureOverlay(): HTMLDivElement {
|
|
57
|
+
if (overlay) return overlay;
|
|
58
|
+
|
|
59
|
+
injectStyles();
|
|
60
|
+
overlay = document.createElement('div');
|
|
61
|
+
overlay.id = FREEZE_ID;
|
|
62
|
+
overlay.style.display = 'none';
|
|
63
|
+
document.body.appendChild(overlay);
|
|
64
|
+
return overlay;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Event blocking (from react-grab's freeze-pseudo-states.ts) ---
|
|
68
|
+
|
|
69
|
+
const stopEvent = (e: Event): void => {
|
|
70
|
+
e.stopImmediatePropagation();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const preventFocusChange = (e: Event): void => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
e.stopImmediatePropagation();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function blockEvents(): void {
|
|
79
|
+
for (const type of MOUSE_EVENTS_TO_BLOCK) {
|
|
80
|
+
document.addEventListener(type, stopEvent, true);
|
|
81
|
+
}
|
|
82
|
+
for (const type of FOCUS_EVENTS_TO_BLOCK) {
|
|
83
|
+
document.addEventListener(type, preventFocusChange, true);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function unblockEvents(): void {
|
|
88
|
+
for (const type of MOUSE_EVENTS_TO_BLOCK) {
|
|
89
|
+
document.removeEventListener(type, stopEvent, true);
|
|
90
|
+
}
|
|
91
|
+
for (const type of FOCUS_EVENTS_TO_BLOCK) {
|
|
92
|
+
document.removeEventListener(type, preventFocusChange, true);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Hover preservation via CSS rule cloning ---
|
|
97
|
+
|
|
98
|
+
/** Mark the hovered element and all ancestors with [data-ag-hover]. */
|
|
99
|
+
function markHoverChain(element: Element): void {
|
|
100
|
+
let current: Element | null = element;
|
|
101
|
+
while (current && current !== document.documentElement) {
|
|
102
|
+
current.setAttribute(HOVER_ATTR, '');
|
|
103
|
+
markedElements.push(current);
|
|
104
|
+
current = current.parentElement;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function clearHoverMarks(): void {
|
|
109
|
+
for (const el of markedElements) {
|
|
110
|
+
el.removeAttribute(HOVER_ATTR);
|
|
111
|
+
}
|
|
112
|
+
markedElements = [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Walk all stylesheets and clone :hover rules as [data-ag-hover] rules.
|
|
117
|
+
* This preserves hover-dependent visibility of child elements
|
|
118
|
+
* (e.g. `.trigger:hover .tooltip { display: block }`) which
|
|
119
|
+
* computed-style snapshotting alone cannot handle.
|
|
120
|
+
*/
|
|
121
|
+
function injectHoverRules(): void {
|
|
122
|
+
if (hoverStyleEl) return;
|
|
123
|
+
|
|
124
|
+
const cloned: string[] = [];
|
|
125
|
+
|
|
126
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
127
|
+
let rules: CSSRuleList;
|
|
128
|
+
try {
|
|
129
|
+
rules = sheet.cssRules;
|
|
130
|
+
} catch {
|
|
131
|
+
continue; // cross-origin stylesheet
|
|
132
|
+
}
|
|
133
|
+
collectHoverRules(rules, cloned);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (cloned.length === 0) return;
|
|
137
|
+
|
|
138
|
+
hoverStyleEl = document.createElement('style');
|
|
139
|
+
hoverStyleEl.id = HOVER_STYLE_ID;
|
|
140
|
+
hoverStyleEl.textContent = cloned.join('\n');
|
|
141
|
+
document.head.appendChild(hoverStyleEl);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectHoverRules(rules: CSSRuleList, out: string[]): void {
|
|
145
|
+
for (const rule of Array.from(rules)) {
|
|
146
|
+
if (rule instanceof CSSStyleRule) {
|
|
147
|
+
if (rule.selectorText.includes(':hover')) {
|
|
148
|
+
const newSelector = rule.selectorText.replace(/:hover/g, `[${HOVER_ATTR}]`);
|
|
149
|
+
out.push(`${newSelector} { ${rule.style.cssText} }`);
|
|
150
|
+
}
|
|
151
|
+
} else if (rule instanceof CSSMediaRule) {
|
|
152
|
+
const inner: string[] = [];
|
|
153
|
+
collectHoverRules(rule.cssRules, inner);
|
|
154
|
+
if (inner.length > 0) {
|
|
155
|
+
out.push(`@media ${rule.conditionText} { ${inner.join('\n')} }`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function removeHoverRules(): void {
|
|
162
|
+
hoverStyleEl?.remove();
|
|
163
|
+
hoverStyleEl = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Animation freezing (from react-grab's freeze-animations.ts) ---
|
|
167
|
+
|
|
168
|
+
function freezeAnimations(): void {
|
|
169
|
+
if (animStyleEl) return;
|
|
170
|
+
|
|
171
|
+
animStyleEl = document.createElement('style');
|
|
172
|
+
animStyleEl.id = ANIM_STYLE_ID;
|
|
173
|
+
animStyleEl.textContent = `
|
|
174
|
+
*, *::before, *::after {
|
|
175
|
+
animation-play-state: paused !important;
|
|
176
|
+
transition: none !important;
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
document.head.appendChild(animStyleEl);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function unfreezeAnimations(): void {
|
|
183
|
+
animStyleEl?.remove();
|
|
184
|
+
animStyleEl = null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
show(hoveredElement?: Element | null): void {
|
|
189
|
+
// 1. Block mouse/focus events to prevent hover state changes
|
|
190
|
+
blockEvents();
|
|
191
|
+
|
|
192
|
+
// 2. Preserve hover state via CSS rule cloning BEFORE overlay steals hover
|
|
193
|
+
if (hoveredElement) {
|
|
194
|
+
markHoverChain(hoveredElement);
|
|
195
|
+
injectHoverRules();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 3. Freeze animations so nothing moves while page is frozen
|
|
199
|
+
freezeAnimations();
|
|
200
|
+
|
|
201
|
+
// 4. Show overlay to block clicks/scrolls
|
|
202
|
+
const el = ensureOverlay();
|
|
203
|
+
el.style.display = 'block';
|
|
204
|
+
visible = true;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
hide(): void {
|
|
208
|
+
if (overlay) overlay.style.display = 'none';
|
|
209
|
+
visible = false;
|
|
210
|
+
clearHoverMarks();
|
|
211
|
+
removeHoverRules();
|
|
212
|
+
unfreezeAnimations();
|
|
213
|
+
unblockEvents();
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
isVisible(): boolean {
|
|
217
|
+
return visible;
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
isFreezeElement(el: Element): boolean {
|
|
221
|
+
return el === overlay || el.id === FREEZE_ID;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
getElement(): HTMLDivElement | null {
|
|
225
|
+
return overlay;
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
dispose(): void {
|
|
229
|
+
clearHoverMarks();
|
|
230
|
+
removeHoverRules();
|
|
231
|
+
unfreezeAnimations();
|
|
232
|
+
unblockEvents();
|
|
233
|
+
overlay?.remove();
|
|
234
|
+
document.getElementById(FREEZE_STYLE_ID)?.remove();
|
|
235
|
+
overlay = null;
|
|
236
|
+
visible = false;
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Z_INDEX_OVERLAY, Z_INDEX_LABEL } from '../constants';
|
|
2
|
+
|
|
3
|
+
const OVERLAY_ID = '__ag-overlay__';
|
|
4
|
+
const LABEL_ID = '__ag-label__';
|
|
5
|
+
const STYLE_ID = '__ag-styles__';
|
|
6
|
+
|
|
7
|
+
export interface OverlayRenderer {
|
|
8
|
+
show(element: Element, componentName: string | null, sourcePath?: string | null, cssClasses?: string[]): void;
|
|
9
|
+
hide(): void;
|
|
10
|
+
isOverlayElement(el: Element): boolean;
|
|
11
|
+
dispose(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createOverlayRenderer(): OverlayRenderer {
|
|
15
|
+
let overlay: HTMLDivElement | null = null;
|
|
16
|
+
let label: HTMLDivElement | null = null;
|
|
17
|
+
let rafId: number | null = null;
|
|
18
|
+
let currentElement: Element | null = null;
|
|
19
|
+
let currentComponentName: string | null = null;
|
|
20
|
+
let currentSourcePath: string | null = null;
|
|
21
|
+
let currentCssClasses: string[] = [];
|
|
22
|
+
|
|
23
|
+
function injectStyles(): void {
|
|
24
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
25
|
+
|
|
26
|
+
const style = document.createElement('style');
|
|
27
|
+
style.id = STYLE_ID;
|
|
28
|
+
style.textContent = `
|
|
29
|
+
#${OVERLAY_ID} {
|
|
30
|
+
position: fixed;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
z-index: ${Z_INDEX_OVERLAY};
|
|
33
|
+
border: 2px solid var(--ag-overlay-border, #3b82f6);
|
|
34
|
+
background: var(--ag-overlay-bg, rgba(59, 130, 246, 0.1));
|
|
35
|
+
transition: top 0.05s ease, left 0.05s ease, width 0.05s ease, height 0.05s ease;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
}
|
|
38
|
+
#${LABEL_ID} {
|
|
39
|
+
position: fixed;
|
|
40
|
+
pointer-events: none;
|
|
41
|
+
z-index: ${Z_INDEX_LABEL};
|
|
42
|
+
background: var(--ag-label-bg, #3b82f6);
|
|
43
|
+
color: var(--ag-label-text, #fff);
|
|
44
|
+
font: 11px/1.4 monospace;
|
|
45
|
+
padding: 2px 6px;
|
|
46
|
+
border-radius: 3px;
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
max-width: 100vw;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
text-overflow: ellipsis;
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
document.head.appendChild(style);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ensureElements(): void {
|
|
58
|
+
if (!overlay) {
|
|
59
|
+
injectStyles();
|
|
60
|
+
overlay = document.createElement('div');
|
|
61
|
+
overlay.id = OVERLAY_ID;
|
|
62
|
+
document.body.appendChild(overlay);
|
|
63
|
+
}
|
|
64
|
+
if (!label) {
|
|
65
|
+
label = document.createElement('div');
|
|
66
|
+
label.id = LABEL_ID;
|
|
67
|
+
document.body.appendChild(label);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function positionOverlay(): void {
|
|
72
|
+
if (!currentElement || !overlay || !label) return;
|
|
73
|
+
|
|
74
|
+
const rect = currentElement.getBoundingClientRect();
|
|
75
|
+
|
|
76
|
+
// Hide overlay if element is detached or has zero dimensions
|
|
77
|
+
if (rect.width === 0 && rect.height === 0 && !currentElement.isConnected) {
|
|
78
|
+
overlay.style.display = 'none';
|
|
79
|
+
label.style.display = 'none';
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
overlay.style.top = `${rect.top}px`;
|
|
84
|
+
overlay.style.left = `${rect.left}px`;
|
|
85
|
+
overlay.style.width = `${rect.width}px`;
|
|
86
|
+
overlay.style.height = `${rect.height}px`;
|
|
87
|
+
overlay.style.display = 'block';
|
|
88
|
+
|
|
89
|
+
const tag = currentElement.tagName.toLowerCase();
|
|
90
|
+
let labelText = `<${tag}>`;
|
|
91
|
+
if (currentCssClasses.length > 0) {
|
|
92
|
+
labelText += ` .${currentCssClasses.join('.')}`;
|
|
93
|
+
}
|
|
94
|
+
if (currentComponentName) {
|
|
95
|
+
labelText += ` in ${currentComponentName}`;
|
|
96
|
+
}
|
|
97
|
+
if (currentSourcePath) {
|
|
98
|
+
labelText += ` \u2014 ${currentSourcePath}`;
|
|
99
|
+
}
|
|
100
|
+
label.textContent = labelText;
|
|
101
|
+
|
|
102
|
+
// Position label above the element, or below if no room
|
|
103
|
+
const labelHeight = 20;
|
|
104
|
+
const gap = 4;
|
|
105
|
+
let labelTop = rect.top - labelHeight - gap;
|
|
106
|
+
if (labelTop < 0) {
|
|
107
|
+
labelTop = rect.bottom + gap;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Clamp label horizontally to viewport
|
|
111
|
+
let labelLeft = rect.left;
|
|
112
|
+
label.style.top = `${labelTop}px`;
|
|
113
|
+
label.style.left = `${labelLeft}px`;
|
|
114
|
+
label.style.display = 'block';
|
|
115
|
+
|
|
116
|
+
// After rendering, check if it overflows right edge
|
|
117
|
+
const labelRect = label.getBoundingClientRect();
|
|
118
|
+
const viewportWidth = document.documentElement.clientWidth;
|
|
119
|
+
if (labelRect.right > viewportWidth) {
|
|
120
|
+
labelLeft = Math.max(0, viewportWidth - labelRect.width);
|
|
121
|
+
label.style.left = `${labelLeft}px`;
|
|
122
|
+
}
|
|
123
|
+
if (labelLeft < 0) {
|
|
124
|
+
label.style.left = '0px';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function trackPosition(): void {
|
|
129
|
+
positionOverlay();
|
|
130
|
+
rafId = requestAnimationFrame(trackPosition);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function stopTracking(): void {
|
|
134
|
+
if (rafId !== null) {
|
|
135
|
+
cancelAnimationFrame(rafId);
|
|
136
|
+
rafId = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
show(element: Element, componentName: string | null, sourcePath?: string | null, cssClasses?: string[]): void {
|
|
142
|
+
ensureElements();
|
|
143
|
+
currentElement = element;
|
|
144
|
+
currentComponentName = componentName;
|
|
145
|
+
currentSourcePath = sourcePath ?? null;
|
|
146
|
+
currentCssClasses = cssClasses ?? [];
|
|
147
|
+
stopTracking();
|
|
148
|
+
trackPosition();
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
hide(): void {
|
|
152
|
+
stopTracking();
|
|
153
|
+
currentElement = null;
|
|
154
|
+
currentComponentName = null;
|
|
155
|
+
currentSourcePath = null;
|
|
156
|
+
currentCssClasses = [];
|
|
157
|
+
|
|
158
|
+
if (overlay) overlay.style.display = 'none';
|
|
159
|
+
if (label) label.style.display = 'none';
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
isOverlayElement(el: Element): boolean {
|
|
163
|
+
return el === overlay || el === label || el.id === OVERLAY_ID || el.id === LABEL_ID;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
dispose(): void {
|
|
167
|
+
stopTracking();
|
|
168
|
+
currentElement = null;
|
|
169
|
+
currentComponentName = null;
|
|
170
|
+
currentSourcePath = null;
|
|
171
|
+
currentCssClasses = [];
|
|
172
|
+
|
|
173
|
+
overlay?.remove();
|
|
174
|
+
label?.remove();
|
|
175
|
+
document.getElementById(STYLE_ID)?.remove();
|
|
176
|
+
overlay = null;
|
|
177
|
+
label = null;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|