diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b8301d2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Inspect CSS 是一个浏览器扩展(Manifest V3),用于在任意网站上检查、编辑和实验 CSS。支持 Chrome 和 Firefox。 + +## Development Commands + +```bash +# 开发模式(带 HMR) +pnpm dev # Chrome 开发 +pnpm dev:firefox # Firefox 开发 + +# 构建 +pnpm build # Chrome 生产构建 +pnpm build:firefox # Firefox 生产构建 +pnpm build:prod # 同时构建 Chrome 和 Firefox 并打包 zip + +# 代码质量 +pnpm lint # ESLint 检查 +pnpm lint:fix # 自动修复 +pnpm test # 运行 Vitest 测试 +``` + +## Architecture + +### 目录结构 + +``` +src/ +├── pages/ +│ ├── background/ # Service Worker (Manifest V3) +│ ├── content/ # Content Script 入口 +│ │ ├── index.ts # 动态加载 UI +│ │ └── ui/ # Vue 主应用(注入到页面) +│ └── components/ # 共享 Vue 组件 +├── lib/ # 核心逻辑(CSS 解析、CodeMirror 配置) +├── storages/ # 浏览器存储封装 +├── interfaces/ # TypeScript 类型定义 +└── utils/ # 工具函数 +``` + +### 关键入口 + +- **Background**: `src/pages/background/index.ts` - 扩展后台 Service Worker +- **Content Script**: `src/pages/content/index.ts` - 通过动态 import 加载 UI +- **Manifest**: `manifest.js` - 扩展配置(非 JSON,构建时转换) + +### 路径别名 + +```typescript +@root → 项目根目录 +@src → src/ +@pages → src/pages/ +@assets → src/assets/ +``` + +## Tech Stack + +- **Framework**: Vue 3 (Composition API, ``) +- **Build**: Vite + 自定义插件(`utils/plugins/`) +- **Styling**: Tailwind CSS +- **Editor**: CodeMirror 6 +- **UI**: Radix Vue +- **跨浏览器**: webextension-polyfill (aliased as `browser`) + +## Extension Development Notes + +1. **Content Script 限制**: 使用动态 `import('@pages/content/ui')` 加载 UI 模块 +2. **Storage**: 使用 `src/storages/` 中的封装进行持久化存储 +3. **Firefox 构建**: 通过 `__FIREFOX__` 环境变量区分,构建命令会自动处理 +4. **HMR**: 开发模式支持热重载,修改后自动刷新扩展 diff --git a/package.json b/package.json index 24a5aff..e3fdc89 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "type": "module", "dependencies": { + "@babel/runtime": "^7.28.6", "@codemirror/autocomplete": "^6.12.0", "@codemirror/commands": "^6.3.3", "@codemirror/lang-css": "^6.2.1", @@ -32,6 +33,7 @@ "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.23.0", "@floating-ui/vue": "^1.0.3", + "@lezer/lr": "^1.4.8", "@medv/finder": "^3.1.0", "@uiw/codemirror-extensions-color": "^4.21.21", "@uiw/codemirror-themes": "^4.21.21", @@ -54,6 +56,7 @@ "devDependencies": { "@commitlint/cli": "18.4.4", "@commitlint/config-conventional": "18.6.0", + "@lezer/highlight": "^1.2.3", "@rollup/plugin-typescript": "11.1.6", "@rushstack/eslint-patch": "^1.6.1", "@types/chrome": "0.0.251", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4beb98b..2ce25e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,12 @@ importers: .: dependencies: + '@babel/runtime': + specifier: ^7.28.6 + version: 7.28.6 '@codemirror/autocomplete': specifier: ^6.12.0 - version: 6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1) + version: 6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.5.1) '@codemirror/commands': specifier: ^6.3.3 version: 6.6.1 @@ -32,6 +35,9 @@ importers: '@floating-ui/vue': specifier: ^1.0.3 version: 1.1.4(vue@3.5.0(typescript@5.5.4)) + '@lezer/lr': + specifier: ^1.4.8 + version: 1.4.8 '@medv/finder': specifier: ^3.1.0 version: 3.2.0 @@ -93,6 +99,9 @@ importers: '@commitlint/config-conventional': specifier: 18.6.0 version: 18.6.0 + '@lezer/highlight': + specifier: ^1.2.3 + version: 1.2.3 '@rollup/plugin-typescript': specifier: 11.1.6 version: 11.1.6(rollup@4.3.0)(tslib@2.6.2)(typescript@5.5.4) @@ -125,7 +134,7 @@ importers: version: 5.1.3(vite@5.4.3(@types/node@20.8.10)(terser@5.31.6))(vue@3.5.0(typescript@5.5.4)) '@vue/eslint-config-airbnb': specifier: ^8.0.0 - version: 8.0.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint-plugin-vue@9.28.0(eslint@8.56.0))(eslint@8.56.0) + version: 8.0.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint-plugin-vue@9.28.0(eslint@8.56.0))(eslint@8.56.0) archiver: specifier: ^6.0.1 version: 6.0.2 @@ -274,6 +283,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@babel/standalone@7.25.6': resolution: {integrity: sha512-Kf2ZcZVqsKbtYhlA7sP0z5A3q5hmCVYMKMWRWNK/5OVwHIve3JY1djVRmIVAx8FMueLIfZGKQDIILK2w8zO4mg==} engines: {node: '>=6.9.0'} @@ -606,11 +619,14 @@ packages: '@lezer/common@1.2.1': resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==} + '@lezer/common@1.5.1': + resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} + '@lezer/css@1.1.8': resolution: {integrity: sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==} - '@lezer/highlight@1.2.1': - resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} '@lezer/html@1.3.10': resolution: {integrity: sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==} @@ -618,8 +634,8 @@ packages: '@lezer/javascript@1.4.17': resolution: {integrity: sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==} - '@lezer/lr@1.4.2': - resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} + '@lezer/lr@1.4.8': + resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} '@lokesh.dhakar/quantize@1.3.0': resolution: {integrity: sha512-4KBSyaMj65d8A+2vnzLxtHFu4OmBU4IKO0yLxZ171Itdf9jGV4w+WbG7VsKts2jUdRkFSzsZqpZOz6hTB3qGAw==} @@ -4150,6 +4166,8 @@ snapshots: dependencies: '@babel/types': 7.25.6 + '@babel/runtime@7.28.6': {} + '@babel/standalone@7.25.6': optional: true @@ -4186,6 +4204,13 @@ snapshots: '@codemirror/view': 6.33.0 '@lezer/common': 1.2.1 + '@codemirror/autocomplete@6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.5.1)': + dependencies: + '@codemirror/language': 6.10.2 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.33.0 + '@lezer/common': 1.5.1 + '@codemirror/commands@6.6.1': dependencies: '@codemirror/language': 6.10.2 @@ -4230,8 +4255,8 @@ snapshots: '@codemirror/state': 6.4.1 '@codemirror/view': 6.33.0 '@lezer/common': 1.2.1 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 style-mod: 4.1.2 '@codemirror/lint@6.8.1': @@ -4525,31 +4550,33 @@ snapshots: '@lezer/common@1.2.1': {} + '@lezer/common@1.5.1': {} + '@lezer/css@1.1.8': dependencies: '@lezer/common': 1.2.1 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 - '@lezer/highlight@1.2.1': + '@lezer/highlight@1.2.3': dependencies: - '@lezer/common': 1.2.1 + '@lezer/common': 1.5.1 '@lezer/html@1.3.10': dependencies: '@lezer/common': 1.2.1 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 '@lezer/javascript@1.4.17': dependencies: '@lezer/common': 1.2.1 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 - '@lezer/lr@1.4.2': + '@lezer/lr@1.4.8': dependencies: - '@lezer/common': 1.2.1 + '@lezer/common': 1.5.1 '@lokesh.dhakar/quantize@1.3.0': {} @@ -4961,13 +4988,13 @@ snapshots: de-indent: 1.0.2 he: 1.2.0 - '@vue/eslint-config-airbnb@8.0.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint-plugin-vue@9.28.0(eslint@8.56.0))(eslint@8.56.0)': + '@vue/eslint-config-airbnb@8.0.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint-plugin-vue@9.28.0(eslint@8.56.0))(eslint@8.56.0)': dependencies: eslint: 8.56.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) - eslint-import-resolver-custom-alias: 1.3.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0)) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0))(eslint@8.56.0) + eslint-import-resolver-custom-alias: 1.3.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0)) eslint-import-resolver-node: 0.3.9 - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.56.0) eslint-plugin-react: 7.35.2(eslint@8.56.0) eslint-plugin-vue: 9.28.0(eslint@8.56.0) @@ -5922,11 +5949,11 @@ snapshots: escape-string-regexp@5.0.0: optional: true - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0))(eslint@8.56.0): dependencies: confusing-browser-globals: 1.0.11 eslint: 8.56.0 - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0) object.assign: 4.1.5 object.entries: 1.1.8 semver: 6.3.1 @@ -5935,9 +5962,9 @@ snapshots: dependencies: eslint: 8.56.0 - eslint-import-resolver-custom-alias@1.3.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0)): + eslint-import-resolver-custom-alias@1.3.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0)): dependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0) glob-parent: 6.0.2 resolve: 1.22.8 @@ -5955,7 +5982,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.56.0 - eslint-module-utils: 2.9.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0) + eslint-module-utils: 2.9.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.1.0 @@ -5968,7 +5995,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.9.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0): + eslint-module-utils@2.9.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -5989,7 +6016,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.9.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0) + eslint-module-utils: 2.9.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -6006,7 +6033,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -6017,7 +6044,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.9.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-plugin-import@2.29.0)(eslint@8.56.0))(eslint@8.56.0) + eslint-module-utils: 2.9.0(@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.56.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 diff --git a/src/pages/content/ui/app-plugin.ts b/src/pages/content/ui/app-plugin.ts index 6fd51a0..4b739cf 100644 --- a/src/pages/content/ui/app-plugin.ts +++ b/src/pages/content/ui/app-plugin.ts @@ -24,6 +24,7 @@ export interface AppState { tempHide: boolean; interactive: boolean; hasGlobalCSS: boolean; + showScanner: boolean; } export interface AppStateProvider { state: AppState; @@ -61,6 +62,7 @@ const defaultState: AppState = { showGrid: false, interactive: true, hasGlobalCSS: false, + showScanner: false, }; export const appPlugin: Plugin = { @@ -76,11 +78,13 @@ export const appPlugin: Plugin = { if ( Object.hasOwn(newState, 'showGrid') || - Object.hasOwn(newState, 'interactive') + Object.hasOwn(newState, 'interactive') || + Object.hasOwn(newState, 'showScanner') ) { settingsStorage.set({ showGrid: appState.showGrid, interactive: appState.interactive, + showScanner: appState.showScanner, }); } } diff --git a/src/pages/content/ui/app/AppElementDetail.vue b/src/pages/content/ui/app/AppElementDetail.vue index 88daa46..999a087 100644 --- a/src/pages/content/ui/app/AppElementDetail.vue +++ b/src/pages/content/ui/app/AppElementDetail.vue @@ -9,6 +9,10 @@ :container-el="containerRef" :last-position="lastPosition" :selected-el="selectedEl" + :style-data="elStyleData" + :el-selector="elSelector" + :basic-selector="elProperties?.selector" + :properties="elProperties" @close-window="closeWindow" @update-window-pos="updateWindowPosition" /> diff --git a/src/pages/content/ui/app/AppElementScanner.vue b/src/pages/content/ui/app/AppElementScanner.vue index 399f703..98f3ea3 100644 --- a/src/pages/content/ui/app/AppElementScanner.vue +++ b/src/pages/content/ui/app/AppElementScanner.vue @@ -14,7 +14,7 @@ />
+ + +
+
+import { ref } from 'vue'; import { EL_ATTR_NAME, SESSION_STORAGE_KEY } from '@root/src/utils/constant'; -import { XIcon, GripHorizontalIcon } from 'lucide-vue-next'; +import { XIcon, GripHorizontalIcon, CopyIcon, CheckIcon } from 'lucide-vue-next'; +import { copyToClipboard } from '@root/src/utils/helper'; +import { StyleDataItem } from '../../app-plugin'; +import { generateAICopy } from '@src/utils/generate-ai-copy'; +import { ElementBasicSelector, ElementProperties } from '@root/src/utils/getElProperties'; interface Props { selectedEl?: Element; containerEl?: Element; lastPosition: null | { x: number; y: number }; + styleData?: StyleDataItem | null; + elSelector?: string; + basicSelector?: ElementBasicSelector; + properties?: ElementProperties | null; } const props = withDefaults(defineProps(), { selectedEl: undefined, containerEl: undefined, lastPosition: () => ({ x: 0, y: 0 }), + styleData: null, + elSelector: '', + basicSelector: undefined, + properties: null, }); const emits = defineEmits<{ (e: 'closeWindow'): void; (e: 'updateWindowPos', x: number, y: number): void; }>(); +const copyState = ref<'idle' | 'copied'>('idle'); + +function handleCopyCSS() { + if (!props.styleData || !props.elSelector || !props.selectedEl || !props.basicSelector || !props.properties) return; + + const content = generateAICopy({ + element: props.selectedEl, + selector: props.elSelector, + basicSelector: props.basicSelector, + properties: props.properties, + styleData: { + currentProps: props.styleData.currentProps, + initialProps: props.styleData.initialProps, + }, + }); + + copyToClipboard(content).then(() => { + copyState.value = 'copied'; + setTimeout(() => { + copyState.value = 'idle'; + }, 1500); + }); +} + function startDragging(pointerDownEvent: PointerEvent) { if (!props.containerEl) return; diff --git a/src/storages/settings.storage.ts b/src/storages/settings.storage.ts index bcc1828..529623e 100644 --- a/src/storages/settings.storage.ts +++ b/src/storages/settings.storage.ts @@ -8,6 +8,7 @@ export interface ColorVariant { export interface Settings { showGrid: boolean; interactive: boolean; + showScanner: boolean; } type SettingsStorage = BaseStorage & { @@ -16,7 +17,7 @@ type SettingsStorage = BaseStorage & { const storage = createStorage( 'settings', - { interactive: true, showGrid: false }, + { interactive: true, showGrid: false, showScanner: false }, { storageType: StorageType.Local, }, diff --git a/src/utils/generate-ai-copy.ts b/src/utils/generate-ai-copy.ts new file mode 100644 index 0000000..84c4084 --- /dev/null +++ b/src/utils/generate-ai-copy.ts @@ -0,0 +1,115 @@ +import { ElementAppliedStyleRules } from './CSSRulesUtils'; +import { ElementBasicSelector, ElementProperties } from './getElProperties'; +import { generateElementCSS } from './generate-element-css'; + +interface GenerateAICopyOptions { + element: Element; + selector: string; + basicSelector: ElementBasicSelector; + properties: ElementProperties; + styleData: { + currentProps: ElementAppliedStyleRules; + initialProps: ElementAppliedStyleRules; + }; +} + +function getOuterHTMLWithDepth(element: Element, maxDepth: number): string { + if (maxDepth <= 0) { + const hasChildren = element.children.length > 0; + const tagName = element.tagName.toLowerCase(); + const attrs = Array.from(element.attributes) + .map((attr) => `${attr.name}="${attr.value}"`) + .join(' '); + const openTag = attrs ? `<${tagName} ${attrs}>` : `<${tagName}>`; + + if (hasChildren) { + return `${openTag}...children...`; + } + const textContent = element.textContent?.trim() || ''; + return `${openTag}${textContent}`; + } + + const clone = element.cloneNode(true) as Element; + + function truncateDeep(el: Element, depth: number) { + if (depth <= 0) { + Array.from(el.children).forEach((child) => { + const placeholder = document.createTextNode('...children...'); + el.replaceChild(placeholder, child); + }); + return; + } + Array.from(el.children).forEach((child) => { + truncateDeep(child as Element, depth - 1); + }); + } + + truncateDeep(clone, maxDepth); + return clone.outerHTML; +} + +function formatHTML(html: string): string { + let formatted = ''; + let indent = 0; + const tokens = html.split(/(<[^>]+>)/g).filter(Boolean); + + for (const token of tokens) { + if (token.startsWith('')) { + formatted += ' '.repeat(indent) + token + '\n'; + if (!token.includes('