vite-plugin-visual-selector 0.1.0

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 ADDED
@@ -0,0 +1,795 @@
1
+ # vite-plugin-visual-selector
2
+
3
+ 一个面向 **React / JSX / TSX** 的 Vite 插件,用来实现类似 Base44 可视化编辑器中的“源码同源元素联动高亮”能力。
4
+
5
+ 它提供两部分能力:
6
+
7
+ 1. **编译时注入**:在 JSX 编译阶段给原生 DOM 元素注入 `data-source-location`
8
+ 2. **运行时桥接**:在预览页 / iframe 中启用 `setupSelectionBridge()`,实现 hover、高亮、同源元素批量选中、`postMessage` 通知父页面
9
+
10
+ ---
11
+
12
+ ## 一、它解决什么问题
13
+
14
+ 在可视化编辑器里,用户点到页面上的某个元素时,编辑器通常需要知道:
15
+
16
+ - 这个 DOM 节点来自哪一段源码
17
+ - 页面上还有哪些元素与它来自同一个 JSX 表达式
18
+ - 如何把这些“同源元素”一起高亮出来
19
+ - 如何把选中结果同步给父级编辑器 UI
20
+
21
+ 这个插件就是为这个目标设计的。
22
+
23
+ 典型效果:
24
+
25
+ ```tsx
26
+ {items.map((item) => (
27
+ <div className="cell">{item.label}</div>
28
+ ))}
29
+ ```
30
+
31
+ 如果这几个 `div` 都来自同一行 `.map()` JSX,那么点击其中一个时,所有同样带有相同 `data-source-location` 的元素都会一起被高亮。
32
+
33
+ ---
34
+
35
+ ## 二、能力概览
36
+
37
+ ### 编译时
38
+
39
+ - 只处理 `.jsx` / `.tsx`
40
+ - 只给**原生 DOM 标签**注入属性,例如:`div`、`span`、`button`
41
+ - 不给 React 组件注入,例如:`<Card />`
42
+ - 默认属性名:`data-source-location`
43
+ - 默认值格式:`相对路径:行:列`
44
+
45
+ 示例:
46
+
47
+ ```html
48
+ <div data-source-location="src/App.tsx:12:6">...</div>
49
+ ```
50
+
51
+ ### 运行时
52
+
53
+ - hover 时高亮同 source-location 的元素
54
+ - click 时选中同 source-location 的元素
55
+ - 给每个匹配元素绘制 overlay
56
+ - 向父页面发送 `postMessage`
57
+ - 支持销毁和清空当前高亮状态
58
+
59
+ ---
60
+
61
+ ## 三、安装
62
+
63
+ ```bash
64
+ npm install vite-plugin-visual-selector
65
+ ```
66
+
67
+ 如果你要跑本仓库 demo / 本地开发:
68
+
69
+ ```bash
70
+ npm install
71
+ ```
72
+
73
+ ---
74
+
75
+ ## 四、最基本的使用方式
76
+
77
+ ### 1)在 `vite.config.ts` 中注册插件
78
+
79
+ ```ts
80
+ import { defineConfig } from 'vite'
81
+ import react from '@vitejs/plugin-react'
82
+ import { visualSelectorPlugin } from 'vite-plugin-visual-selector'
83
+
84
+ export default defineConfig({
85
+ plugins: [
86
+ react(),
87
+ visualSelectorPlugin(),
88
+ ],
89
+ })
90
+ ```
91
+
92
+ 这是最小接入方式。
93
+
94
+ 注册后,插件会在 Vite transform 阶段扫描 JSX / TSX 模块,并给原生 DOM 节点自动插入 `data-source-location`。
95
+
96
+ ---
97
+
98
+ ## 五、进阶配置
99
+
100
+ 插件入口:
101
+
102
+ ```ts
103
+ import { visualSelectorPlugin } from 'vite-plugin-visual-selector'
104
+ ```
105
+
106
+ 类型定义:
107
+
108
+ ```ts
109
+ interface VisualSelectorPluginOptions {
110
+ include?: RegExp[]
111
+ exclude?: RegExp[]
112
+ attributeName?: string
113
+ root?: string
114
+ }
115
+ ```
116
+
117
+ ### 配置示例
118
+
119
+ ```ts
120
+ import { defineConfig } from 'vite'
121
+ import react from '@vitejs/plugin-react'
122
+ import { visualSelectorPlugin } from 'vite-plugin-visual-selector'
123
+
124
+ export default defineConfig({
125
+ plugins: [
126
+ react(),
127
+ visualSelectorPlugin({
128
+ include: [/\.[jt]sx$/],
129
+ exclude: [/node_modules/, /\.test\./],
130
+ attributeName: 'data-source-location',
131
+ root: process.cwd(),
132
+ }),
133
+ ],
134
+ })
135
+ ```
136
+
137
+ ### 配置项说明
138
+
139
+ #### `include?: RegExp[]`
140
+
141
+ 控制哪些文件会进入 transform。
142
+
143
+ 默认值:
144
+
145
+ ```ts
146
+ [/\.[jt]sx$/]
147
+ ```
148
+
149
+ 也就是默认只处理 JSX / TSX 文件。
150
+
151
+ #### `exclude?: RegExp[]`
152
+
153
+ 控制哪些文件会被跳过。
154
+
155
+ 默认值:
156
+
157
+ ```ts
158
+ [/node_modules/]
159
+ ```
160
+
161
+ #### `attributeName?: string`
162
+
163
+ 自定义注入属性名。
164
+
165
+ 默认值:
166
+
167
+ ```ts
168
+ 'data-source-location'
169
+ ```
170
+
171
+ 如果你的编辑器协议想用别的名字,可以改。
172
+
173
+ #### `root?: string`
174
+
175
+ 用于生成相对路径。
176
+
177
+ 例如源码绝对路径是:
178
+
179
+ ```txt
180
+ /Users/me/project/src/App.tsx
181
+ ```
182
+
183
+ 如果 `root = /Users/me/project`,最终注入值会变成:
184
+
185
+ ```txt
186
+ src/App.tsx:12:6
187
+ ```
188
+
189
+ ---
190
+
191
+ ## 六、运行时接入方式
192
+
193
+ 仅仅注册 Vite 插件还不够。
194
+ 如果你想获得 Base44 那种“点击元素时,同源元素一起高亮”的能力,还需要在预览页中显式启用 runtime bridge。
195
+
196
+ 运行时入口:
197
+
198
+ ```ts
199
+ import { setupSelectionBridge } from 'vite-plugin-visual-selector/runtime'
200
+ ```
201
+
202
+ ### 最小接入示例
203
+
204
+ ```ts
205
+ import { setupSelectionBridge } from 'vite-plugin-visual-selector/runtime'
206
+
207
+ setupSelectionBridge({
208
+ parentWindow: window.parent,
209
+ enableHover: true,
210
+ enableClickSelection: true,
211
+ })
212
+ ```
213
+
214
+ 这个调用通常应该放在:
215
+
216
+ - 预览页入口
217
+ - iframe 内部页面入口
218
+ - 或专门的 preview bootstrap 文件里
219
+
220
+ ---
221
+
222
+ ## 七、runtime API 详细说明
223
+
224
+ 类型定义:
225
+
226
+ ```ts
227
+ interface SelectionBridgeOptions {
228
+ attributeName?: string
229
+ targetWindow?: Window
230
+ parentWindow?: Pick<Window, 'postMessage'>
231
+ targetOrigin?: string
232
+ enableHover?: boolean
233
+ enableClickSelection?: boolean
234
+ }
235
+ ```
236
+
237
+ ### 参数说明
238
+
239
+ #### `attributeName?: string`
240
+
241
+ 要读取的元素标识属性名。
242
+
243
+ 默认值:
244
+
245
+ ```ts
246
+ 'data-source-location'
247
+ ```
248
+
249
+ #### `targetWindow?: Window`
250
+
251
+ bridge 所工作的目标 window。
252
+
253
+ 默认值:
254
+
255
+ ```ts
256
+ window
257
+ ```
258
+
259
+ 通常不需要改,除非你在特殊上下文中手动桥接别的 window。
260
+
261
+ #### `parentWindow?: Pick<Window, 'postMessage'>`
262
+
263
+ 接收消息的父窗口对象。
264
+
265
+ 默认值:
266
+
267
+ ```ts
268
+ window.parent
269
+ ```
270
+
271
+ #### `targetOrigin?: string`
272
+
273
+ `postMessage` 的目标 origin。
274
+
275
+ 默认值:
276
+
277
+ ```ts
278
+ '*'
279
+ ```
280
+
281
+ 生产环境里如果你有固定编辑器域名,建议收紧为明确 origin。
282
+
283
+ #### `enableHover?: boolean`
284
+
285
+ 是否启用 hover 联动高亮。
286
+
287
+ 默认值:
288
+
289
+ ```ts
290
+ true
291
+ ```
292
+
293
+ #### `enableClickSelection?: boolean`
294
+
295
+ 是否启用点击选中。
296
+
297
+ 默认值:
298
+
299
+ ```ts
300
+ true
301
+ ```
302
+
303
+ ---
304
+
305
+ ## 八、`setupSelectionBridge()` 返回什么
306
+
307
+ 当前返回一个控制对象:
308
+
309
+ ```ts
310
+ const bridge = setupSelectionBridge({...})
311
+
312
+ bridge.destroy()
313
+ bridge.clearSelection()
314
+ ```
315
+
316
+ ### `destroy()`
317
+
318
+ 作用:
319
+
320
+ - 移除事件监听器
321
+ - 删除当前所有 overlay
322
+ - 释放当前激活的 bridge
323
+
324
+ 适合场景:
325
+
326
+ - 预览页卸载
327
+ - iframe 切换页面
328
+ - 编辑器销毁
329
+
330
+ ### `clearSelection()`
331
+
332
+ 作用:
333
+
334
+ - 清除 selected overlays
335
+ - 清除 hover overlays
336
+
337
+ 适合场景:
338
+
339
+ - 父编辑器主动取消选中
340
+ - 切换右侧面板时重置视觉状态
341
+ - 执行删除 / 替换元素后重置高亮
342
+
343
+ ---
344
+
345
+ ## 九、父页面会收到什么消息
346
+
347
+ 当前 runtime 会发出类似下面的消息:
348
+
349
+ ```ts
350
+ {
351
+ type: 'element-selected',
352
+ tagName: 'DIV',
353
+ className: 'box',
354
+ selectorId: 'src/App.tsx:12:6',
355
+ dataSourceLocation: 'src/App.tsx:12:6',
356
+ rect: {
357
+ top: 120,
358
+ left: 300,
359
+ right: 348,
360
+ bottom: 168,
361
+ width: 48,
362
+ height: 48,
363
+ },
364
+ }
365
+ ```
366
+
367
+ 其中:
368
+
369
+ - `type`
370
+ - 当前可能值:`element-selected` / `element-hovered`
371
+ - `tagName`
372
+ - 被命中元素的标签名
373
+ - `className`
374
+ - 元素的 className
375
+ - `selectorId`
376
+ - 当前选中的 source 标识
377
+ - `dataSourceLocation`
378
+ - 当前和 `selectorId` 保持一致
379
+ - `rect`
380
+ - 元素在 viewport 中的矩形信息
381
+
382
+ ### 父页面接收示例
383
+
384
+ ```ts
385
+ window.addEventListener('message', (event) => {
386
+ const data = event.data
387
+
388
+ if (data?.type === 'element-selected') {
389
+ console.log('用户选中了元素:', data)
390
+ }
391
+ })
392
+ ```
393
+
394
+ ---
395
+
396
+ ## 十、完整接入示例
397
+
398
+ ### `vite.config.ts`
399
+
400
+ ```ts
401
+ import { defineConfig } from 'vite'
402
+ import react from '@vitejs/plugin-react'
403
+ import { visualSelectorPlugin } from 'vite-plugin-visual-selector'
404
+
405
+ export default defineConfig({
406
+ plugins: [
407
+ react(),
408
+ visualSelectorPlugin({
409
+ root: process.cwd(),
410
+ }),
411
+ ],
412
+ })
413
+ ```
414
+
415
+ ### `src/preview-bridge.ts`
416
+
417
+ ```ts
418
+ import { setupSelectionBridge } from 'vite-plugin-visual-selector/runtime'
419
+
420
+ setupSelectionBridge({
421
+ parentWindow: window.parent,
422
+ enableHover: true,
423
+ enableClickSelection: true,
424
+ })
425
+ ```
426
+
427
+ ### `src/main.tsx`
428
+
429
+ ```ts
430
+ import './preview-bridge'
431
+ ```
432
+
433
+ ### 父页面
434
+
435
+ ```ts
436
+ window.addEventListener('message', (event) => {
437
+ if (event.data?.type === 'element-selected') {
438
+ // 打开右侧属性面板 / 更新工具栏 / 高亮源码等
439
+ }
440
+ })
441
+ ```
442
+
443
+ ---
444
+
445
+ ## 十一、编译前后效果示例
446
+
447
+ 源码:
448
+
449
+ ```tsx
450
+ {items.map((item) => (
451
+ <div className="cell">{item.label}</div>
452
+ ))}
453
+ ```
454
+
455
+ 经过插件 transform 后,效果近似于:
456
+
457
+ ```tsx
458
+ {items.map((item) => (
459
+ <div
460
+ data-source-location="src/App.tsx:12:6"
461
+ className="cell"
462
+ >
463
+ {item.label}
464
+ </div>
465
+ ))}
466
+ ```
467
+
468
+ 如果页面最终渲染出多个 `div`,它们都会带相同的 `data-source-location`,于是 runtime 就能把它们视为“同源元素组”。
469
+
470
+ ---
471
+
472
+ ## 十二、实现过程详解
473
+
474
+ 这一部分重点解释:这个插件到底是怎么工作的。
475
+
476
+ ### 整体分层
477
+
478
+ 插件分成两层:
479
+
480
+ ```txt
481
+ 编译时:JSX -> 注入 data-source-location
482
+ 运行时:读 data-source-location -> 查找同源元素 -> 绘制 overlay -> postMessage
483
+ ```
484
+
485
+ 也就是:
486
+
487
+ 1. **先在编译时埋点**
488
+ 2. **再在运行时消费这些标记**
489
+
490
+ ---
491
+
492
+ ### 1)编译时实现过程
493
+
494
+ 核心文件:
495
+
496
+ - `src/plugin.ts`
497
+
498
+ 使用到的核心工具:
499
+
500
+ - `@babel/parser`
501
+ - `@babel/traverse`
502
+ - `@babel/types`
503
+ - `magic-string`
504
+
505
+ #### 步骤 A:过滤目标文件
506
+
507
+ 插件只处理匹配 `include` 的文件,并跳过 `exclude`。
508
+
509
+ 默认情况就是:
510
+
511
+ - 处理 `.jsx` / `.tsx`
512
+ - 跳过 `node_modules`
513
+
514
+ #### 步骤 B:解析 AST
515
+
516
+ ```ts
517
+ const ast = parse(code, {
518
+ sourceType: 'module',
519
+ plugins: ['jsx', 'typescript'],
520
+ })
521
+ ```
522
+
523
+ 这样插件就能拿到 JSX 节点的结构信息和源码位置信息。
524
+
525
+ #### 步骤 C:只处理原生标签
526
+
527
+ 插件会检查当前节点是不是原生 DOM 元素。
528
+
529
+ 例如:
530
+
531
+ - `div` → 处理
532
+ - `span` → 处理
533
+ - `button` → 处理
534
+ - `Card` → 不处理
535
+ - `Layout` → 不处理
536
+
537
+ 原因很简单:
538
+
539
+ React 组件本身并不一定会直接成为真实 DOM,只有最终的原生标签才适合被 visual selector 消费。
540
+
541
+ #### 步骤 D:生成 source-location
542
+
543
+ 插件会根据:
544
+
545
+ - 文件相对路径
546
+ - AST 节点起始行号
547
+ - AST 节点起始列号
548
+
549
+ 生成:
550
+
551
+ ```txt
552
+ src/App.tsx:12:6
553
+ ```
554
+
555
+ 这里列号做了 `+1`,是为了输出更符合人类习惯的 1-based 列号。
556
+
557
+ #### 步骤 E:把属性插回源码
558
+
559
+ 通过 `MagicString` 在 JSX opening tag 中插入属性:
560
+
561
+ ```ts
562
+ magicString.appendLeft(node.name.end, ` ${attributeName}="${location}"`)
563
+ ```
564
+
565
+ 这样就不会手写字符串拼接整段 JSX,而是进行更可靠的源码片段注入。
566
+
567
+ #### 步骤 F:输出 sourcemap
568
+
569
+ 如果文件确实发生了变更,就返回:
570
+
571
+ ```ts
572
+ {
573
+ code: magicString.toString(),
574
+ map: magicString.generateMap({ hires: true }),
575
+ }
576
+ ```
577
+
578
+ 这样宿主工具链还能继续保持较好的调试体验。
579
+
580
+ ---
581
+
582
+ ### 2)运行时实现过程
583
+
584
+ 核心文件:
585
+
586
+ - `src/runtime.ts`
587
+
588
+ #### 步骤 A:安装 bridge
589
+
590
+ ```ts
591
+ setupSelectionBridge({...})
592
+ ```
593
+
594
+ 调用后会:
595
+
596
+ - 绑定 click 监听
597
+ - 绑定 mousemove 监听
598
+ - 维护 selected / hover overlay 状态
599
+
600
+ 而且当前实现会保证:
601
+
602
+ - 同一个页面只存在一个激活 bridge
603
+ - 再次调用时会先清理上一个 bridge
604
+
605
+ #### 步骤 B:识别可选元素
606
+
607
+ runtime 会从事件目标开始向上查找最近的:
608
+
609
+ ```txt
610
+ [data-source-location]
611
+ ```
612
+
613
+ 如果事件命中的是 overlay 本身,则会直接跳过,避免“高亮层选中高亮层”的问题。
614
+
615
+ #### 步骤 C:读取 selectorId
616
+
617
+ 当前 selectorId 本质上就是元素的:
618
+
619
+ ```ts
620
+ element.getAttribute(attributeName)
621
+ ```
622
+
623
+ 通常就是 `data-source-location` 的值。
624
+
625
+ #### 步骤 D:查找所有同源元素
626
+
627
+ 一旦拿到 selectorId,runtime 会执行:
628
+
629
+ ```ts
630
+ document.querySelectorAll(`[data-source-location="..."]`)
631
+ ```
632
+
633
+ 从整个文档中找出所有具有相同 source-location 的元素。
634
+
635
+ 这就是“点击一个元素,所有同类一起高亮”的核心。
636
+
637
+ #### 步骤 E:绘制 overlay
638
+
639
+ 对每一个匹配元素,runtime 都会创建一个新的 overlay:
640
+
641
+ - `position: absolute`
642
+ - `pointer-events: none`
643
+ - 高 z-index
644
+ - hover / selected 两套样式
645
+
646
+ 然后根据目标元素的 `getBoundingClientRect()`,把 overlay 定位到正确位置。
647
+
648
+ #### 步骤 F:发消息给父页面
649
+
650
+ click 时,runtime 会调用:
651
+
652
+ ```ts
653
+ parentWindow.postMessage(message, targetOrigin)
654
+ ```
655
+
656
+ 父编辑器收到消息后,就可以:
657
+
658
+ - 打开元素编辑面板
659
+ - 更新工具栏
660
+ - 选中对应源码节点
661
+ - 做 hover / selection 同步
662
+
663
+ ---
664
+
665
+ ## 十三、为什么这样设计
666
+
667
+ ### 为什么在编译时注入属性?
668
+
669
+ 因为这是最稳定、最轻量的方式。
670
+
671
+ 优点:
672
+
673
+ - 运行时不需要做源码分析
674
+ - DOM 一渲染出来就带有可追踪标识
675
+ - 同一个 JSX 表达式渲染出的多个元素天然共享同一个标识
676
+
677
+ ### 为什么只给原生 DOM 打标?
678
+
679
+ 因为编辑器最终操作的是浏览器里的真实 DOM,而不是 React 组件抽象层。
680
+
681
+ ### 为什么 overlay 挂在 `document.body`?
682
+
683
+ 因为这样不需要改动原始业务 DOM,侵入性最低,也更容易做绝对定位。
684
+
685
+ ### 为什么 runtime 不自动注入?
686
+
687
+ 因为显式接入更稳:
688
+
689
+ - 不需要猜宿主应用入口
690
+ - 不污染用户项目结构
691
+ - 容易和 iframe / preview 场景集成
692
+
693
+ ---
694
+
695
+ ## 十四、当前实现的限制
696
+
697
+ 当前版本有这些限制:
698
+
699
+ 1. **只支持 React / JSX / TSX**
700
+ 2. **只处理原生 DOM 元素**
701
+ 3. **runtime 需要手动调用 `setupSelectionBridge()`**
702
+ 4. **当前 overlay 会在 hover 时“先清再画”**
703
+ - 逻辑简单
704
+ - 但还没有做更细的性能优化
705
+ 5. **当前尚未内建 scroll / resize 自动重定位增强**
706
+ 6. **当前消息结构是最小可用版,不是完整编辑器协议**
707
+
708
+ ---
709
+
710
+ ## 十五、推荐接入方式
711
+
712
+ 如果你要把它接入自己的可视化编辑器,我建议这样分工:
713
+
714
+ ### 预览页 / iframe 内部
715
+
716
+ - 注册 `visualSelectorPlugin()`
717
+ - 启用 `setupSelectionBridge()`
718
+ - 渲染业务页面
719
+
720
+ ### 父编辑器页面
721
+
722
+ - 监听 `postMessage`
723
+ - 根据 `element-selected` 更新 UI
724
+ - 必要时主动调用 preview 内部能力做状态复位
725
+
726
+ ---
727
+
728
+ ## 十六、本地开发命令
729
+
730
+ ```bash
731
+ npm install
732
+ npm run typecheck
733
+ npm test
734
+ npm run build
735
+ ```
736
+
737
+ 运行 demo:
738
+
739
+ ```bash
740
+ npm run demo
741
+ ```
742
+
743
+ ---
744
+
745
+ ## 十七、项目结构
746
+
747
+ ```txt
748
+ src/
749
+ index.ts # 插件入口导出
750
+ plugin.ts # 编译时 JSX 注入逻辑
751
+ runtime.ts # 运行时 bridge / overlay / postMessage
752
+ shared/
753
+ constants.ts # 常量
754
+ types.ts # 公共类型
755
+
756
+ demo/
757
+ vite.config.ts
758
+ src/
759
+ App.tsx
760
+ bridge.ts
761
+ main.tsx
762
+
763
+ test/
764
+ plugin.test.ts
765
+ runtime.test.ts
766
+ runtime-api.test.ts
767
+ package.test.ts
768
+ demo-docs.test.ts
769
+ ```
770
+
771
+ ---
772
+
773
+ ## 十八、导出 API 一览
774
+
775
+ ### 主入口
776
+
777
+ ```ts
778
+ import { visualSelectorPlugin } from 'vite-plugin-visual-selector'
779
+ ```
780
+
781
+ ### runtime 入口
782
+
783
+ ```ts
784
+ import { setupSelectionBridge } from 'vite-plugin-visual-selector/runtime'
785
+ ```
786
+
787
+ ---
788
+
789
+ ## 十九、总结
790
+
791
+ 如果你只记住一句话,可以记这个:
792
+
793
+ > 这个插件的核心思想是:**编译时给 DOM 打上源码坐标,运行时再根据这个坐标把同源元素重新聚合起来。**
794
+
795
+ 这样就能用非常简单、非常稳定的方式,做出类似 Base44 那种“点击一个元素,所有同类一起高亮”的可视化编辑体验。
@@ -0,0 +1,7 @@
1
+ import { Plugin } from 'vite';
2
+ import { V as VisualSelectorPluginOptions } from './types-DlgpChn_.js';
3
+ export { S as SelectionBridgeOptions } from './types-DlgpChn_.js';
4
+
5
+ declare function visualSelectorPlugin(options?: VisualSelectorPluginOptions): Plugin;
6
+
7
+ export { VisualSelectorPluginOptions, visualSelectorPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ // src/plugin.ts
2
+ import { parse } from "@babel/parser";
3
+ import traverse from "@babel/traverse";
4
+ import * as t from "@babel/types";
5
+ import MagicString from "magic-string";
6
+ import path from "path";
7
+
8
+ // src/shared/constants.ts
9
+ var DEFAULT_ATTRIBUTE_NAME = "data-source-location";
10
+
11
+ // src/plugin.ts
12
+ function toRelativeUnixPath(root, filePath) {
13
+ return path.relative(root, filePath).split(path.sep).join("/");
14
+ }
15
+ function isIntrinsicElement(node) {
16
+ return t.isJSXIdentifier(node.name) && /^[a-z]/.test(node.name.name);
17
+ }
18
+ function hasSourceLocationAttribute(node, attributeName) {
19
+ return node.attributes.some(
20
+ (attribute) => t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && attribute.name.name === attributeName
21
+ );
22
+ }
23
+ function visualSelectorPlugin(options = {}) {
24
+ const attributeName = options.attributeName ?? DEFAULT_ATTRIBUTE_NAME;
25
+ const root = options.root ?? process.cwd();
26
+ const include = options.include ?? [/\.[jt]sx$/];
27
+ const exclude = options.exclude ?? [/node_modules/];
28
+ return {
29
+ name: "vite-plugin-visual-selector",
30
+ enforce: "pre",
31
+ transform(code, id) {
32
+ if (!include.some((pattern) => pattern.test(id))) {
33
+ return null;
34
+ }
35
+ if (exclude.some((pattern) => pattern.test(id))) {
36
+ return null;
37
+ }
38
+ const ast = parse(code, {
39
+ sourceType: "module",
40
+ plugins: ["jsx", "typescript"]
41
+ });
42
+ const magicString = new MagicString(code);
43
+ let mutated = false;
44
+ traverse(ast, {
45
+ JSXOpeningElement(jsxPath) {
46
+ const node = jsxPath.node;
47
+ if (!isIntrinsicElement(node)) {
48
+ return;
49
+ }
50
+ if (hasSourceLocationAttribute(node, attributeName)) {
51
+ return;
52
+ }
53
+ if (!node.loc || node.name.end == null) {
54
+ return;
55
+ }
56
+ const location = `${toRelativeUnixPath(root, id)}:${node.loc.start.line}:${node.loc.start.column + 1}`;
57
+ magicString.appendLeft(node.name.end, ` ${attributeName}="${location}"`);
58
+ mutated = true;
59
+ }
60
+ });
61
+ if (!mutated) {
62
+ return code;
63
+ }
64
+ return {
65
+ code: magicString.toString(),
66
+ map: magicString.generateMap({ hires: true })
67
+ };
68
+ }
69
+ };
70
+ }
71
+ export {
72
+ visualSelectorPlugin
73
+ };
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.ts","../src/shared/constants.ts"],"sourcesContent":["import { parse } from '@babel/parser'\nimport traverse from '@babel/traverse'\nimport * as t from '@babel/types'\nimport MagicString from 'magic-string'\nimport path from 'node:path'\nimport type { Plugin } from 'vite'\n\nimport { DEFAULT_ATTRIBUTE_NAME } from './shared/constants'\nimport type { VisualSelectorPluginOptions } from './shared/types'\n\nfunction toRelativeUnixPath(root: string, filePath: string) {\n return path.relative(root, filePath).split(path.sep).join('/')\n}\n\nfunction isIntrinsicElement(node: t.JSXOpeningElement) {\n return t.isJSXIdentifier(node.name) && /^[a-z]/.test(node.name.name)\n}\n\nfunction hasSourceLocationAttribute(node: t.JSXOpeningElement, attributeName: string) {\n return node.attributes.some(\n (attribute) =>\n t.isJSXAttribute(attribute) &&\n t.isJSXIdentifier(attribute.name) &&\n attribute.name.name === attributeName,\n )\n}\n\nexport function visualSelectorPlugin(\n options: VisualSelectorPluginOptions = {},\n): Plugin {\n const attributeName = options.attributeName ?? DEFAULT_ATTRIBUTE_NAME\n const root = options.root ?? process.cwd()\n const include = options.include ?? [/\\.[jt]sx$/]\n const exclude = options.exclude ?? [/node_modules/]\n\n return {\n name: 'vite-plugin-visual-selector',\n enforce: 'pre',\n transform(code, id) {\n if (!include.some((pattern) => pattern.test(id))) {\n return null\n }\n\n if (exclude.some((pattern) => pattern.test(id))) {\n return null\n }\n\n const ast = parse(code, {\n sourceType: 'module',\n plugins: ['jsx', 'typescript'],\n })\n\n const magicString = new MagicString(code)\n let mutated = false\n\n traverse(ast, {\n JSXOpeningElement(jsxPath) {\n const node = jsxPath.node\n\n if (!isIntrinsicElement(node)) {\n return\n }\n\n if (hasSourceLocationAttribute(node, attributeName)) {\n return\n }\n\n if (!node.loc || node.name.end == null) {\n return\n }\n\n const location = `${toRelativeUnixPath(root, id)}:${node.loc.start.line}:${node.loc.start.column + 1}`\n magicString.appendLeft(node.name.end, ` ${attributeName}=\"${location}\"`)\n mutated = true\n },\n })\n\n if (!mutated) {\n return code\n }\n\n return {\n code: magicString.toString(),\n map: magicString.generateMap({ hires: true }),\n }\n },\n }\n}\n","export const DEFAULT_ATTRIBUTE_NAME = 'data-source-location'\nexport const OVERLAY_ATTRIBUTE_NAME = 'data-visual-selector-overlay'\n"],"mappings":";AAAA,SAAS,aAAa;AACtB,OAAO,cAAc;AACrB,YAAY,OAAO;AACnB,OAAO,iBAAiB;AACxB,OAAO,UAAU;;;ACJV,IAAM,yBAAyB;;;ADUtC,SAAS,mBAAmB,MAAc,UAAkB;AAC1D,SAAO,KAAK,SAAS,MAAM,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,KAAK,GAAG;AAC/D;AAEA,SAAS,mBAAmB,MAA2B;AACrD,SAAS,kBAAgB,KAAK,IAAI,KAAK,SAAS,KAAK,KAAK,KAAK,IAAI;AACrE;AAEA,SAAS,2BAA2B,MAA2B,eAAuB;AACpF,SAAO,KAAK,WAAW;AAAA,IACrB,CAAC,cACG,iBAAe,SAAS,KACxB,kBAAgB,UAAU,IAAI,KAChC,UAAU,KAAK,SAAS;AAAA,EAC5B;AACF;AAEO,SAAS,qBACd,UAAuC,CAAC,GAChC;AACR,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,QAAM,OAAO,QAAQ,QAAQ,QAAQ,IAAI;AACzC,QAAM,UAAU,QAAQ,WAAW,CAAC,WAAW;AAC/C,QAAM,UAAU,QAAQ,WAAW,CAAC,cAAc;AAElD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU,MAAM,IAAI;AAClB,UAAI,CAAC,QAAQ,KAAK,CAAC,YAAY,QAAQ,KAAK,EAAE,CAAC,GAAG;AAChD,eAAO;AAAA,MACT;AAEA,UAAI,QAAQ,KAAK,CAAC,YAAY,QAAQ,KAAK,EAAE,CAAC,GAAG;AAC/C,eAAO;AAAA,MACT;AAEA,YAAM,MAAM,MAAM,MAAM;AAAA,QACtB,YAAY;AAAA,QACZ,SAAS,CAAC,OAAO,YAAY;AAAA,MAC/B,CAAC;AAED,YAAM,cAAc,IAAI,YAAY,IAAI;AACxC,UAAI,UAAU;AAEd,eAAS,KAAK;AAAA,QACZ,kBAAkB,SAAS;AACzB,gBAAM,OAAO,QAAQ;AAErB,cAAI,CAAC,mBAAmB,IAAI,GAAG;AAC7B;AAAA,UACF;AAEA,cAAI,2BAA2B,MAAM,aAAa,GAAG;AACnD;AAAA,UACF;AAEA,cAAI,CAAC,KAAK,OAAO,KAAK,KAAK,OAAO,MAAM;AACtC;AAAA,UACF;AAEA,gBAAM,WAAW,GAAG,mBAAmB,MAAM,EAAE,CAAC,IAAI,KAAK,IAAI,MAAM,IAAI,IAAI,KAAK,IAAI,MAAM,SAAS,CAAC;AACpG,sBAAY,WAAW,KAAK,KAAK,KAAK,IAAI,aAAa,KAAK,QAAQ,GAAG;AACvE,oBAAU;AAAA,QACZ;AAAA,MACF,CAAC;AAED,UAAI,CAAC,SAAS;AACZ,eAAO;AAAA,MACT;AAEA,aAAO;AAAA,QACL,MAAM,YAAY,SAAS;AAAA,QAC3B,KAAK,YAAY,YAAY,EAAE,OAAO,KAAK,CAAC;AAAA,MAC9C;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,8 @@
1
+ import { S as SelectionBridgeOptions } from './types-DlgpChn_.js';
2
+
3
+ declare function setupSelectionBridge(options?: SelectionBridgeOptions): {
4
+ destroy(): void;
5
+ clearSelection(): void;
6
+ };
7
+
8
+ export { setupSelectionBridge };
@@ -0,0 +1,150 @@
1
+ // src/shared/constants.ts
2
+ var DEFAULT_ATTRIBUTE_NAME = "data-source-location";
3
+ var OVERLAY_ATTRIBUTE_NAME = "data-visual-selector-overlay";
4
+
5
+ // src/runtime.ts
6
+ var activeCleanup = null;
7
+ function getElementSelectorId(element, attributeName) {
8
+ return element.getAttribute(attributeName);
9
+ }
10
+ function isOverlayElement(element) {
11
+ return element.hasAttribute(OVERLAY_ATTRIBUTE_NAME);
12
+ }
13
+ function getSelectableElement(target, attributeName) {
14
+ if (!(target instanceof Element)) {
15
+ return null;
16
+ }
17
+ const closestOverlay = target.closest(`[${OVERLAY_ATTRIBUTE_NAME}]`);
18
+ if (closestOverlay) {
19
+ return null;
20
+ }
21
+ return target.closest(`[${attributeName}]`);
22
+ }
23
+ function findMatchingElements(selectorId, attributeName, root) {
24
+ return Array.from(
25
+ root.querySelectorAll(`[${attributeName}="${selectorId}"]`)
26
+ );
27
+ }
28
+ function createOverlay(mode, doc) {
29
+ const overlay = doc.createElement("div");
30
+ overlay.setAttribute(OVERLAY_ATTRIBUTE_NAME, mode);
31
+ overlay.style.position = "absolute";
32
+ overlay.style.pointerEvents = "none";
33
+ overlay.style.zIndex = "9999";
34
+ overlay.style.transition = "all 0.1s ease-in-out";
35
+ if (mode === "selected") {
36
+ overlay.style.border = "2px solid #2563EB";
37
+ } else {
38
+ overlay.style.border = "2px solid #95a5fc";
39
+ overlay.style.backgroundColor = "rgba(99, 102, 241, 0.05)";
40
+ }
41
+ return overlay;
42
+ }
43
+ function positionOverlay(overlay, target, view) {
44
+ const rect = target.getBoundingClientRect();
45
+ overlay.style.top = `${rect.top + view.scrollY}px`;
46
+ overlay.style.left = `${rect.left + view.scrollX}px`;
47
+ overlay.style.width = `${rect.width}px`;
48
+ overlay.style.height = `${rect.height}px`;
49
+ }
50
+ function createMessage(type, element, attributeName) {
51
+ const rect = element.getBoundingClientRect();
52
+ return {
53
+ type,
54
+ tagName: element.tagName,
55
+ className: element.className,
56
+ selectorId: getElementSelectorId(element, attributeName),
57
+ dataSourceLocation: getElementSelectorId(element, attributeName),
58
+ rect: {
59
+ top: rect.top,
60
+ left: rect.left,
61
+ right: rect.right,
62
+ bottom: rect.bottom,
63
+ width: rect.width,
64
+ height: rect.height
65
+ }
66
+ };
67
+ }
68
+ function setupSelectionBridge(options = {}) {
69
+ activeCleanup?.();
70
+ const targetWindow = options.targetWindow ?? window;
71
+ const targetDocument = targetWindow.document;
72
+ const parentWindow = options.parentWindow ?? window.parent;
73
+ const attributeName = options.attributeName ?? DEFAULT_ATTRIBUTE_NAME;
74
+ const targetOrigin = options.targetOrigin ?? "*";
75
+ const enableHover = options.enableHover ?? true;
76
+ const enableClickSelection = options.enableClickSelection ?? true;
77
+ let selectedOverlays = [];
78
+ let hoverOverlays = [];
79
+ function clearOverlays(overlays) {
80
+ overlays.forEach((overlay) => overlay.remove());
81
+ }
82
+ function renderOverlays(mode, sourceElement) {
83
+ const selectorId = getElementSelectorId(sourceElement, attributeName);
84
+ if (!selectorId) {
85
+ return [];
86
+ }
87
+ return findMatchingElements(selectorId, attributeName, targetDocument).map((element) => {
88
+ const overlay = createOverlay(mode, targetDocument);
89
+ positionOverlay(overlay, element, targetWindow);
90
+ targetDocument.body.appendChild(overlay);
91
+ return overlay;
92
+ });
93
+ }
94
+ function onClick(event) {
95
+ if (!enableClickSelection) {
96
+ return;
97
+ }
98
+ const selectableElement = getSelectableElement(event.target, attributeName);
99
+ if (!selectableElement || isOverlayElement(selectableElement)) {
100
+ return;
101
+ }
102
+ clearOverlays(selectedOverlays);
103
+ selectedOverlays = renderOverlays("selected", selectableElement);
104
+ parentWindow.postMessage(
105
+ createMessage("element-selected", selectableElement, attributeName),
106
+ targetOrigin
107
+ );
108
+ }
109
+ function onMouseMove(event) {
110
+ if (!enableHover) {
111
+ return;
112
+ }
113
+ const selectableElement = getSelectableElement(event.target, attributeName);
114
+ clearOverlays(hoverOverlays);
115
+ hoverOverlays = [];
116
+ if (!selectableElement || isOverlayElement(selectableElement)) {
117
+ return;
118
+ }
119
+ hoverOverlays = renderOverlays("hover", selectableElement);
120
+ }
121
+ targetDocument.addEventListener("click", onClick, true);
122
+ targetDocument.addEventListener("mousemove", onMouseMove, true);
123
+ const cleanup = () => {
124
+ targetDocument.removeEventListener("click", onClick, true);
125
+ targetDocument.removeEventListener("mousemove", onMouseMove, true);
126
+ clearOverlays(selectedOverlays);
127
+ clearOverlays(hoverOverlays);
128
+ selectedOverlays = [];
129
+ hoverOverlays = [];
130
+ if (activeCleanup === cleanup) {
131
+ activeCleanup = null;
132
+ }
133
+ };
134
+ activeCleanup = cleanup;
135
+ return {
136
+ destroy() {
137
+ cleanup();
138
+ },
139
+ clearSelection() {
140
+ clearOverlays(selectedOverlays);
141
+ clearOverlays(hoverOverlays);
142
+ selectedOverlays = [];
143
+ hoverOverlays = [];
144
+ }
145
+ };
146
+ }
147
+ export {
148
+ setupSelectionBridge
149
+ };
150
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/constants.ts","../src/runtime.ts"],"sourcesContent":["export const DEFAULT_ATTRIBUTE_NAME = 'data-source-location'\nexport const OVERLAY_ATTRIBUTE_NAME = 'data-visual-selector-overlay'\n","import {\n DEFAULT_ATTRIBUTE_NAME,\n OVERLAY_ATTRIBUTE_NAME,\n} from './shared/constants'\nimport type {\n ElementSelectionMessage,\n SelectionBridgeOptions,\n} from './shared/types'\n\nlet activeCleanup: (() => void) | null = null\n\nfunction getElementSelectorId(element: Element, attributeName: string) {\n return element.getAttribute(attributeName)\n}\n\nfunction isOverlayElement(element: Element) {\n return element.hasAttribute(OVERLAY_ATTRIBUTE_NAME)\n}\n\nfunction getSelectableElement(target: EventTarget | null, attributeName: string) {\n if (!(target instanceof Element)) {\n return null\n }\n\n const closestOverlay = target.closest(`[${OVERLAY_ATTRIBUTE_NAME}]`)\n if (closestOverlay) {\n return null\n }\n\n return target.closest<HTMLElement>(`[${attributeName}]`)\n}\n\nfunction findMatchingElements(selectorId: string, attributeName: string, root: Document) {\n return Array.from(\n root.querySelectorAll<HTMLElement>(`[${attributeName}=\"${selectorId}\"]`),\n )\n}\n\nfunction createOverlay(mode: 'hover' | 'selected', doc: Document) {\n const overlay = doc.createElement('div')\n overlay.setAttribute(OVERLAY_ATTRIBUTE_NAME, mode)\n overlay.style.position = 'absolute'\n overlay.style.pointerEvents = 'none'\n overlay.style.zIndex = '9999'\n overlay.style.transition = 'all 0.1s ease-in-out'\n\n if (mode === 'selected') {\n overlay.style.border = '2px solid #2563EB'\n } else {\n overlay.style.border = '2px solid #95a5fc'\n overlay.style.backgroundColor = 'rgba(99, 102, 241, 0.05)'\n }\n\n return overlay\n}\n\nfunction positionOverlay(overlay: HTMLDivElement, target: HTMLElement, view: Window) {\n const rect = target.getBoundingClientRect()\n overlay.style.top = `${rect.top + view.scrollY}px`\n overlay.style.left = `${rect.left + view.scrollX}px`\n overlay.style.width = `${rect.width}px`\n overlay.style.height = `${rect.height}px`\n}\n\nfunction createMessage(\n type: 'element-selected' | 'element-hovered',\n element: HTMLElement,\n attributeName: string,\n): ElementSelectionMessage {\n const rect = element.getBoundingClientRect()\n\n return {\n type,\n tagName: element.tagName,\n className: element.className,\n selectorId: getElementSelectorId(element, attributeName),\n dataSourceLocation: getElementSelectorId(element, attributeName),\n rect: {\n top: rect.top,\n left: rect.left,\n right: rect.right,\n bottom: rect.bottom,\n width: rect.width,\n height: rect.height,\n },\n }\n}\n\nexport function setupSelectionBridge(\n options: SelectionBridgeOptions = {},\n) {\n activeCleanup?.()\n\n const targetWindow = options.targetWindow ?? window\n const targetDocument = targetWindow.document\n const parentWindow = options.parentWindow ?? window.parent\n const attributeName = options.attributeName ?? DEFAULT_ATTRIBUTE_NAME\n const targetOrigin = options.targetOrigin ?? '*'\n const enableHover = options.enableHover ?? true\n const enableClickSelection = options.enableClickSelection ?? true\n\n let selectedOverlays: HTMLDivElement[] = []\n let hoverOverlays: HTMLDivElement[] = []\n\n function clearOverlays(overlays: HTMLDivElement[]) {\n overlays.forEach((overlay) => overlay.remove())\n }\n\n function renderOverlays(mode: 'hover' | 'selected', sourceElement: HTMLElement) {\n const selectorId = getElementSelectorId(sourceElement, attributeName)\n if (!selectorId) {\n return []\n }\n\n return findMatchingElements(selectorId, attributeName, targetDocument).map((element) => {\n const overlay = createOverlay(mode, targetDocument)\n positionOverlay(overlay, element, targetWindow)\n targetDocument.body.appendChild(overlay)\n return overlay\n })\n }\n\n function onClick(event: MouseEvent) {\n if (!enableClickSelection) {\n return\n }\n\n const selectableElement = getSelectableElement(event.target, attributeName)\n if (!selectableElement || isOverlayElement(selectableElement)) {\n return\n }\n\n clearOverlays(selectedOverlays)\n selectedOverlays = renderOverlays('selected', selectableElement)\n parentWindow.postMessage(\n createMessage('element-selected', selectableElement, attributeName),\n targetOrigin,\n )\n }\n\n function onMouseMove(event: MouseEvent) {\n if (!enableHover) {\n return\n }\n\n const selectableElement = getSelectableElement(event.target, attributeName)\n clearOverlays(hoverOverlays)\n hoverOverlays = []\n\n if (!selectableElement || isOverlayElement(selectableElement)) {\n return\n }\n\n hoverOverlays = renderOverlays('hover', selectableElement)\n }\n\n targetDocument.addEventListener('click', onClick, true)\n targetDocument.addEventListener('mousemove', onMouseMove, true)\n\n const cleanup = () => {\n targetDocument.removeEventListener('click', onClick, true)\n targetDocument.removeEventListener('mousemove', onMouseMove, true)\n clearOverlays(selectedOverlays)\n clearOverlays(hoverOverlays)\n selectedOverlays = []\n hoverOverlays = []\n if (activeCleanup === cleanup) {\n activeCleanup = null\n }\n }\n\n activeCleanup = cleanup\n\n return {\n destroy() {\n cleanup()\n },\n clearSelection() {\n clearOverlays(selectedOverlays)\n clearOverlays(hoverOverlays)\n selectedOverlays = []\n hoverOverlays = []\n },\n }\n}\n"],"mappings":";AAAO,IAAM,yBAAyB;AAC/B,IAAM,yBAAyB;;;ACQtC,IAAI,gBAAqC;AAEzC,SAAS,qBAAqB,SAAkB,eAAuB;AACrE,SAAO,QAAQ,aAAa,aAAa;AAC3C;AAEA,SAAS,iBAAiB,SAAkB;AAC1C,SAAO,QAAQ,aAAa,sBAAsB;AACpD;AAEA,SAAS,qBAAqB,QAA4B,eAAuB;AAC/E,MAAI,EAAE,kBAAkB,UAAU;AAChC,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB,OAAO,QAAQ,IAAI,sBAAsB,GAAG;AACnE,MAAI,gBAAgB;AAClB,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,QAAqB,IAAI,aAAa,GAAG;AACzD;AAEA,SAAS,qBAAqB,YAAoB,eAAuB,MAAgB;AACvF,SAAO,MAAM;AAAA,IACX,KAAK,iBAA8B,IAAI,aAAa,KAAK,UAAU,IAAI;AAAA,EACzE;AACF;AAEA,SAAS,cAAc,MAA4B,KAAe;AAChE,QAAM,UAAU,IAAI,cAAc,KAAK;AACvC,UAAQ,aAAa,wBAAwB,IAAI;AACjD,UAAQ,MAAM,WAAW;AACzB,UAAQ,MAAM,gBAAgB;AAC9B,UAAQ,MAAM,SAAS;AACvB,UAAQ,MAAM,aAAa;AAE3B,MAAI,SAAS,YAAY;AACvB,YAAQ,MAAM,SAAS;AAAA,EACzB,OAAO;AACL,YAAQ,MAAM,SAAS;AACvB,YAAQ,MAAM,kBAAkB;AAAA,EAClC;AAEA,SAAO;AACT;AAEA,SAAS,gBAAgB,SAAyB,QAAqB,MAAc;AACnF,QAAM,OAAO,OAAO,sBAAsB;AAC1C,UAAQ,MAAM,MAAM,GAAG,KAAK,MAAM,KAAK,OAAO;AAC9C,UAAQ,MAAM,OAAO,GAAG,KAAK,OAAO,KAAK,OAAO;AAChD,UAAQ,MAAM,QAAQ,GAAG,KAAK,KAAK;AACnC,UAAQ,MAAM,SAAS,GAAG,KAAK,MAAM;AACvC;AAEA,SAAS,cACP,MACA,SACA,eACyB;AACzB,QAAM,OAAO,QAAQ,sBAAsB;AAE3C,SAAO;AAAA,IACL;AAAA,IACA,SAAS,QAAQ;AAAA,IACjB,WAAW,QAAQ;AAAA,IACnB,YAAY,qBAAqB,SAAS,aAAa;AAAA,IACvD,oBAAoB,qBAAqB,SAAS,aAAa;AAAA,IAC/D,MAAM;AAAA,MACJ,KAAK,KAAK;AAAA,MACV,MAAM,KAAK;AAAA,MACX,OAAO,KAAK;AAAA,MACZ,QAAQ,KAAK;AAAA,MACb,OAAO,KAAK;AAAA,MACZ,QAAQ,KAAK;AAAA,IACf;AAAA,EACF;AACF;AAEO,SAAS,qBACd,UAAkC,CAAC,GACnC;AACA,kBAAgB;AAEhB,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,iBAAiB,aAAa;AACpC,QAAM,eAAe,QAAQ,gBAAgB,OAAO;AACpD,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,uBAAuB,QAAQ,wBAAwB;AAE7D,MAAI,mBAAqC,CAAC;AAC1C,MAAI,gBAAkC,CAAC;AAEvC,WAAS,cAAc,UAA4B;AACjD,aAAS,QAAQ,CAAC,YAAY,QAAQ,OAAO,CAAC;AAAA,EAChD;AAEA,WAAS,eAAe,MAA4B,eAA4B;AAC9E,UAAM,aAAa,qBAAqB,eAAe,aAAa;AACpE,QAAI,CAAC,YAAY;AACf,aAAO,CAAC;AAAA,IACV;AAEA,WAAO,qBAAqB,YAAY,eAAe,cAAc,EAAE,IAAI,CAAC,YAAY;AACtF,YAAM,UAAU,cAAc,MAAM,cAAc;AAClD,sBAAgB,SAAS,SAAS,YAAY;AAC9C,qBAAe,KAAK,YAAY,OAAO;AACvC,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,WAAS,QAAQ,OAAmB;AAClC,QAAI,CAAC,sBAAsB;AACzB;AAAA,IACF;AAEA,UAAM,oBAAoB,qBAAqB,MAAM,QAAQ,aAAa;AAC1E,QAAI,CAAC,qBAAqB,iBAAiB,iBAAiB,GAAG;AAC7D;AAAA,IACF;AAEA,kBAAc,gBAAgB;AAC9B,uBAAmB,eAAe,YAAY,iBAAiB;AAC/D,iBAAa;AAAA,MACX,cAAc,oBAAoB,mBAAmB,aAAa;AAAA,MAClE;AAAA,IACF;AAAA,EACF;AAEA,WAAS,YAAY,OAAmB;AACtC,QAAI,CAAC,aAAa;AAChB;AAAA,IACF;AAEA,UAAM,oBAAoB,qBAAqB,MAAM,QAAQ,aAAa;AAC1E,kBAAc,aAAa;AAC3B,oBAAgB,CAAC;AAEjB,QAAI,CAAC,qBAAqB,iBAAiB,iBAAiB,GAAG;AAC7D;AAAA,IACF;AAEA,oBAAgB,eAAe,SAAS,iBAAiB;AAAA,EAC3D;AAEA,iBAAe,iBAAiB,SAAS,SAAS,IAAI;AACtD,iBAAe,iBAAiB,aAAa,aAAa,IAAI;AAE9D,QAAM,UAAU,MAAM;AACpB,mBAAe,oBAAoB,SAAS,SAAS,IAAI;AACzD,mBAAe,oBAAoB,aAAa,aAAa,IAAI;AACjE,kBAAc,gBAAgB;AAC9B,kBAAc,aAAa;AAC3B,uBAAmB,CAAC;AACpB,oBAAgB,CAAC;AACjB,QAAI,kBAAkB,SAAS;AAC7B,sBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,kBAAgB;AAEhB,SAAO;AAAA,IACL,UAAU;AACR,cAAQ;AAAA,IACV;AAAA,IACA,iBAAiB;AACf,oBAAc,gBAAgB;AAC9B,oBAAc,aAAa;AAC3B,yBAAmB,CAAC;AACpB,sBAAgB,CAAC;AAAA,IACnB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,16 @@
1
+ interface VisualSelectorPluginOptions {
2
+ include?: RegExp[];
3
+ exclude?: RegExp[];
4
+ attributeName?: string;
5
+ root?: string;
6
+ }
7
+ interface SelectionBridgeOptions {
8
+ attributeName?: string;
9
+ targetWindow?: Window;
10
+ parentWindow?: Pick<Window, 'postMessage'>;
11
+ targetOrigin?: string;
12
+ enableHover?: boolean;
13
+ enableClickSelection?: boolean;
14
+ }
15
+
16
+ export type { SelectionBridgeOptions as S, VisualSelectorPluginOptions as V };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "vite-plugin-visual-selector",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./runtime": {
14
+ "types": "./dist/runtime.d.ts",
15
+ "import": "./dist/runtime.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "sideEffects": false,
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "typecheck": "tsc --noEmit",
27
+ "demo": "vite --config demo/vite.config.ts"
28
+ },
29
+ "peerDependencies": {
30
+ "vite": ">=5"
31
+ },
32
+ "devDependencies": {
33
+ "@babel/parser": "^7.27.0",
34
+ "@babel/traverse": "^7.27.0",
35
+ "@babel/types": "^7.27.0",
36
+ "@types/node": "^24.0.0",
37
+ "@types/react": "^19.0.0",
38
+ "@types/react-dom": "^19.0.0",
39
+ "@vitejs/plugin-react": "^5.0.0",
40
+ "jsdom": "^26.0.0",
41
+ "magic-string": "^0.30.0",
42
+ "react": "^19.0.0",
43
+ "react-dom": "^19.0.0",
44
+ "tsup": "^8.0.0",
45
+ "typescript": "^5.8.0",
46
+ "vite": "^6.0.0",
47
+ "vitest": "^3.0.0"
48
+ }
49
+ }