├── .gitignore ├── README.md ├── assets ├── react.svg ├── runtime_screenshot_1.png └── runtime_screenshot_2.png ├── entrypoints ├── background.ts ├── components │ ├── element_info │ │ ├── card_head.tsx │ │ ├── card_input.tsx │ │ ├── element_card.tsx │ │ └── element_info.tsx │ └── spotlight │ │ └── spotlight.tsx ├── content.tsx ├── hooks │ └── use_dynamic_styles.ts ├── rule │ └── rule.ts ├── store │ ├── element_card.ts │ ├── mouse.ts │ └── rule.ts └── utlis │ └── tools.tsx ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── public ├── css │ └── element_info │ │ └── index.css ├── icon │ ├── 128.png │ ├── 16.png │ ├── 32.png │ ├── 48.png │ └── 96.png └── wxt.svg ├── tsconfig.json ├── uno.config.ts └── wxt.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .output 12 | stats.html 13 | stats-*.json 14 | .wxt 15 | web-ext.config.ts 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### 基于wxt开发的浏览器插件方便您快速调试tailwind css 2 | 3 | ###### 启动命令 4 | 5 | ``` 6 | pnpm install 7 | pnpm run dev 8 | ``` 9 | 10 | ###### 已完成功能 11 | - 悬停显示元素信息高亮显示 12 | - 元素class可选可删除 13 | - 添加tialwind css 类和补全信息 14 | - 复制元素样式 dom整体 15 | 16 | 17 | ###### 待完成 18 | - copy 原生css转成 tailwind css 19 | 20 | 21 | ###### 运行截图 22 | ![alt 运行图](assets/runtime_screenshot_1.png) 23 | ![alt 运行图](assets/runtime_screenshot_2.png) 24 | 25 | 26 | 27 | ###### 项目目录 28 | 29 | ``` 30 | . 31 | ├── .gitignore # Git忽略文件,列出在版本控制中要忽略的文件和目录 32 | ├── assets # 静态资源文件夹 33 | │ └── ...静态资源 34 | ├── entrypoints # 入口点文件夹 35 | │ ├── background.ts # Chrome扩展的后台脚本 36 | │ ├── components # 组件文件夹 37 | │ │ ├── element_info # 元素信息相关组件 38 | │ │ │ ├── card_head.tsx # 卡片头部组件 39 | │ │ │ ├── card_input.tsx # 卡片输入组件 40 | │ │ │ ├── element_card.tsx # 元素卡片组件 41 | │ │ │ └── element_info.tsx # 元素信息组件 42 | │ │ └── spotlight # 聚焦组件 43 | │ │ └── spotlight.tsx # 聚焦组件实现 44 | │ ├── content.tsx # 内容脚本 45 | │ ├── hooks # 自定义hooks 46 | │ │ └── use_dynamic_styles.ts # 动态样式的自定义hook 47 | │ ├── rule # 规则相关文件 48 | │ │ └── rule.ts # 规则定义 49 | │ ├── store # 状态管理 50 | │ │ ├── element_card.ts # 元素卡片状态管理 51 | │ │ ├── mouse.ts # 鼠标状态管理 52 | │ │ └── rule.ts # 规则状态管理 53 | │ └── utils # 工具函数 54 | │ └── tools.tsx # 工具函数实现 55 | ├── eslint.config.js # ESLint配置文件 56 | ├── index.js # 项目的入口文件 57 | ├── package.json # 项目的依赖配置和元数据 58 | ├── pnpm-lock.yaml # pnpm锁定文件,记录依赖的确切版本 59 | ├── public # 公共资源文件夹 60 | │ ├── css # CSS文件夹 61 | │ │ └── element_info # 元素信息相关CSS 62 | │ │ └── index.css # 元素信息组件样式 63 | │ ├── icon # 图标文件夹 64 | │ │ ├── 128.png # 128x128图标 65 | │ │ ├── 16.png # 16x16图标 66 | │ │ ├── 32.png # 32x32图标 67 | │ │ ├── 48.png # 48x48图标 68 | │ │ └── 96.png # 96x96图标 69 | │ └── wxt.svg # WXT图标文件 70 | ├── README.md # 项目说明文档 71 | ├── tsconfig.json # TypeScript配置文件 72 | ├── uno.config.ts # UnoCSS配置文件 73 | └── wxt.config.ts # WXT框架配置文件 74 | ``` -------------------------------------------------------------------------------- /assets/react.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/runtime_screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suanju/tailexamine/89d7457f480bab9c993939797649e260525c1e63/assets/runtime_screenshot_1.png -------------------------------------------------------------------------------- /assets/runtime_screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suanju/tailexamine/89d7457f480bab9c993939797649e260525c1e63/assets/runtime_screenshot_2.png -------------------------------------------------------------------------------- /entrypoints/background.ts: -------------------------------------------------------------------------------- 1 | export default defineBackground(() => { 2 | chrome.action.onClicked.addListener((tab) => { 3 | chrome.tabs.sendMessage(tab.id!, { toggle: true }); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /entrypoints/components/element_info/card_head.tsx: -------------------------------------------------------------------------------- 1 | import { useElementCardStore } from "@/entrypoints/store/element_card"; 2 | import { useMouseStore } from "@/entrypoints/store/mouse"; 3 | import { CloseOutlined, CopyOutlined, FullscreenOutlined, NodeExpandOutlined, NumberOutlined, SnippetsOutlined } from "@ant-design/icons"; 4 | import { Tooltip } from "antd"; 5 | import { useRef, useEffect, useCallback, useState } from "react"; 6 | import { throttle } from 'radash'; 7 | 8 | export default () => { 9 | const { element } = useMouseStore(); 10 | const { elementCardPosition, setElementCardPosition, isMove, setIsMove } = useElementCardStore(); 11 | const dragStartRef = useRef<{ x: number, y: number }>({ x: 0, y: 0 }); 12 | const [isDragging, setIsDragging] = useState(false); 13 | const [isTipsOpen, setIsTipsOpen] = useState(false); 14 | const [tooltipText, setTooltipText] = useState({ copyClasses: 'Copy classes', copyElement: 'Copy element', moveWindow: 'Move window', closeWindow: 'Close window' }); 15 | 16 | const updatePosition = useCallback((e: MouseEvent) => { 17 | if (isMove) { 18 | const { clientX, clientY } = e; 19 | //减少重绘次数 20 | requestAnimationFrame(() => { 21 | setElementCardPosition(clientY - dragStartRef.current.y, clientX - dragStartRef.current.x); 22 | }); 23 | } 24 | }, [isMove, setElementCardPosition]); 25 | 26 | const cleanupListeners = useCallback(() => { 27 | setIsTipsOpen(false); 28 | document.body.style.userSelect = ''; 29 | document.removeEventListener('mousemove', updatePosition); 30 | document.removeEventListener('mouseup', handleMouseUp); 31 | }, [updatePosition]); 32 | 33 | const addListeners = useCallback(() => { 34 | setIsTipsOpen(true); 35 | document.body.style.userSelect = 'none'; 36 | document.addEventListener('mousemove', updatePosition); 37 | document.addEventListener('mouseup', handleMouseUp); 38 | }, [updatePosition]); 39 | 40 | const handleMouseUp = useCallback(() => { 41 | setIsMove(false); 42 | cleanupListeners(); 43 | }, [cleanupListeners, setIsMove]); 44 | 45 | const toggleDragMode = useCallback((e: React.MouseEvent) => { 46 | e.stopPropagation(); 47 | if (!isDragging) { 48 | dragStartRef.current = { 49 | x: e.clientX - elementCardPosition.left, 50 | y: e.clientY - elementCardPosition.top, 51 | }; 52 | setIsMove(true); 53 | addListeners(); 54 | } else { 55 | setIsMove(false); 56 | cleanupListeners(); 57 | } 58 | setIsDragging((prev) => !prev); 59 | }, [isDragging, elementCardPosition.left, elementCardPosition.top, setIsMove, addListeners, cleanupListeners]); 60 | 61 | // 窗口拖动 62 | useEffect(() => { 63 | if (isDragging) { 64 | addListeners(); 65 | } else { 66 | setIsTipsOpen(true); 67 | cleanupListeners(); 68 | } 69 | return cleanupListeners; 70 | }, [isDragging, addListeners, cleanupListeners]); 71 | 72 | // 关闭窗口 73 | const close = () => { 74 | // setElement(null); 75 | // setIsListeningMouse(true); 76 | // @ts-ignore 77 | window.removeTailexamine() 78 | }; 79 | 80 | // 复制类名,使用节流 81 | const copyClasses = throttle({ interval: 2000 }, 82 | () => { 83 | if (element) { 84 | const classNames = element.className; 85 | navigator.clipboard.writeText(classNames).then(() => { 86 | setTooltipText((prev) => ({ ...prev, copyClasses: '☑️ Classes copied!' })); 87 | setTimeout(() => { 88 | setTooltipText((prev) => ({ ...prev, copyClasses: 'Copy classes' })); 89 | }, 1000); 90 | }); 91 | } 92 | } 93 | ); 94 | 95 | // 复制完整元素,使用节流 96 | const copyElement = throttle({ interval: 2000 }, 97 | () => { 98 | if (element) { 99 | const elementHtml = element.outerHTML; 100 | navigator.clipboard.writeText(elementHtml).then(() => { 101 | setTooltipText((prev) => ({ ...prev, copyElement: '☑️ Element copied!' })); 102 | setTimeout(() => { 103 | setTooltipText((prev) => ({ ...prev, copyElement: 'Copy element' })); 104 | }, 2000); 105 | }); 106 | } 107 | } 108 | ); 109 | 110 | return ( 111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 130 | 131 | 132 | 133 | 134 |
135 |
136 | ); 137 | }; 138 | -------------------------------------------------------------------------------- /entrypoints/components/element_info/card_input.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { ElementInfoId } from "@/entrypoints/components/element_info/element_info"; 3 | import { useMouseStore } from "@/entrypoints/store/mouse"; 4 | import useDynamicStyles from '@/entrypoints/hooks/use_dynamic_styles'; 5 | import { Select, Space } from "antd"; 6 | import { getColorFromRule, areCSSRulesSame } from "@/entrypoints/utlis/tools"; 7 | import { options, rules } from "../../rule/rule"; 8 | 9 | const optionRender = (option: any) => { 10 | const { isColor, color } = getColorFromRule(option.data.descr); 11 | return ( 12 | 13 | 14 | {isColor ?
15 |
: } 16 | {option.data.label} 17 |
18 | 19 | {option.data.descr} 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default ({ delTag }: { delTag: (Tag: string) => void }) => { 26 | const { element, setElement } = useMouseStore(); 27 | const { addStyle } = useDynamicStyles(); 28 | const [selectedValue, setSelectedValue] = useState(null); 29 | const [searchValue, setSearchValue] = useState(""); 30 | const [availableOptions, setAvailableOptions] = useState(options); 31 | 32 | useEffect(() => { 33 | if (searchValue) { 34 | setAvailableOptions(options.filter((option) => 35 | option.label.includes(searchValue) || option.descr.includes(searchValue) 36 | )); 37 | } else { 38 | setAvailableOptions([]); 39 | } 40 | }, [searchValue]); 41 | 42 | // 处理搜索框输入变化 43 | const handleSearch = (value: string) => { 44 | setSearchValue(value); 45 | }; 46 | 47 | // 选中事件 48 | const handleSelect = (value: string) => { 49 | if (element) { 50 | //作用相同规则进行去除 51 | element.classList.forEach((className: string) => { 52 | console.log("className", className, rules[className]); 53 | if (rules[className]) { 54 | //删除规则 55 | console.log("删除规则", className, areCSSRulesSame(rules[className], rules[value])); 56 | if (areCSSRulesSame(rules[className], rules[value])) { 57 | element.classList.remove(className); 58 | delTag(className) 59 | } 60 | } 61 | }); 62 | element.classList.add(value); 63 | const styleRule = rules[value]; 64 | if (styleRule) addStyle(value, styleRule); 65 | setSelectedValue(null); 66 | setSearchValue(""); 67 | setElement(element); 68 | } 69 | }; 70 | 71 | return ( 72 |
73 |
74 |