From 3a3798e37b06c33dc88990294a7f3c46f0e2de98 Mon Sep 17 00:00:00 2001 From: Jz0ojiang Date: Sat, 5 Sep 2026 17:51:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=20iconify=20?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=BA=93=EF=BC=8C=E5=8F=B3=E6=A0=8F=E6=94=B9?= =?UTF-8?q?=E6=88=90=20Figma=20=E5=BC=8F=E6=94=B6=E8=B5=B7=E5=88=86?= =?UTF-8?q?=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 图标统一走 unplugin-icons + hugeicons,构建期内联,出口收在 src/ui/icons.ts。 顺带修掉 TopBar 那个 undo 图标缺弧命令的 path。 右栏下半四组默认全收,收起态只有一行 32px;顶栏导出按钮改成先展开 「导出与模板」再滚,否则滚到底只看得到一行标题。 属性区去掉分组标题,改成 Figma 那种「字母/图标 + 无边框输入框」两列密排, 新增 W/H 宽高比锁(比例在开锁那一刻定格,不每次编辑现算)。 删掉 Sidebar.vue 和 panels/CanvasPanel.vue:上一轮改 Figma 布局之后 它们已无人 import,--sidebar-w 同批遗留。 --- bun.lock | 62 +++++ package.json | 4 + scripts/find-icon.mjs | 13 + src/components/BottomDock.vue | 33 ++- src/components/Inspector.vue | 368 +++++++++++++++++--------- src/components/LayersPanel.vue | 136 +++------- src/components/SelectionMenu.vue | 119 ++++++--- src/components/Sidebar.vue | 82 ------ src/components/ToolPanels.vue | 237 +++++++---------- src/components/TopBar.vue | 79 +++--- src/components/panels/CanvasPanel.vue | 78 ------ src/style.css | 15 +- src/ui/icons.ts | 81 ++++++ src/ui/toolGroups.ts | 22 ++ tsconfig.json | 2 +- vite.config.ts | 10 +- 16 files changed, 722 insertions(+), 619 deletions(-) create mode 100644 scripts/find-icon.mjs delete mode 100644 src/components/Sidebar.vue delete mode 100644 src/components/panels/CanvasPanel.vue create mode 100644 src/ui/icons.ts create mode 100644 src/ui/toolGroups.ts diff --git a/bun.lock b/bun.lock index f440426..ee57d3f 100644 --- a/bun.lock +++ b/bun.lock @@ -9,14 +9,20 @@ "vue": "^3.5.13", }, "devDependencies": { + "@iconify-json/hugeicons": "^1.2.34", + "@iconify-json/mingcute": "^1.2.8", + "@iconify-json/solar": "^1.2.10", "@vitejs/plugin-vue": "^6.0.0", "typescript": "^5.7.3", + "unplugin-icons": "^23.0.1", "vite": "^7.0.0", "vue-tsc": "^2.2.0", }, }, }, "packages": { + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], @@ -89,8 +95,26 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], + "@iconify-json/hugeicons": ["@iconify-json/hugeicons@1.2.34", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-DxTrDx+P/NNgBE0wo6yXRMSyzZziCUqyb/X/HLgvI/z8XynchCw0ptNeNwHac6B5vq3PtR0R2ltCSSGiltkzLg=="], + + "@iconify-json/mingcute": ["@iconify-json/mingcute@1.2.8", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-J8VXpRwTpx5yzOzyhx3T4IXnfXIKTWFl3rZ9okcW4c+NMMuwkLvdmM4rkjB83tnYIhg5+kkeMpmw8tvSnxd4rQ=="], + + "@iconify-json/solar": ["@iconify-json/solar@1.2.10", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-ChpZ3kNw8nDhHfUoLq5JJ3AdIzytFm57dFkjAS0fjXnL5a7WwibTINjMKfvqQDhuXBrgqejNLyGp8CCpG8UyCg=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.5", "", { "dependencies": { "@antfu/install-pkg": "^2.0.1", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-nT+AWyh5caHY6xa0PdLXpUooIz6l7qsT+adqsCTYBfbdn5Q18VrWs2fvuTa7KEoJnPBfEPqWkmAIht0EHtCi2A=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], @@ -177,6 +201,8 @@ "@vue/shared": ["@vue/shared@3.5.42", "", {}, "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g=="], + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "alien-signals": ["alien-signals@1.0.13", "", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="], @@ -195,6 +221,8 @@ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "confbox": ["confbox@0.3.1", "", {}, "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA=="], + "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -223,6 +251,8 @@ "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="], + "fabric": ["fabric@7.4.0", "", { "optionalDependencies": { "canvas": "^3.2.0", "jsdom": "^26.1.0" } }, "sha512-NalYDc3eifTl1C33zryQwpH6+XA/2ClxQrH9vkASkZw3tbkRmorpikhYMmxhUTmi7O3e9ODz0vOT8qfaCh9IVA=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -245,6 +275,8 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], @@ -253,6 +285,8 @@ "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + "local-pkg": ["local-pkg@1.2.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q=="], + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -265,6 +299,8 @@ "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], @@ -279,16 +315,24 @@ "nwsapi": ["nwsapi@2.2.27", "", {}, "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw=="], + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + "pkg-types": ["pkg-types@2.3.2", "", { "dependencies": { "confbox": "^0.3.0", "exsolve": "^1.1.1", "pathe": "^2.0.3" } }, "sha512-v0sVXzj7oPGysr543YYZLYbcJNJsKikSsp/fFzoxQ12ewY3ZZr7oCPC8y7OlmxfYB3QPvriXmuPD8KZggE1vqg=="], + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], @@ -297,6 +341,8 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -329,6 +375,8 @@ "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], @@ -343,6 +391,12 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "unplugin-icons": ["unplugin-icons@23.0.1", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/utils": "^3.1.0", "local-pkg": "^1.1.2", "obug": "^2.1.1", "unplugin": "^2.3.11" }, "peerDependencies": { "@svgr/core": ">=7.0.0", "@svgx/core": "^1.0.1", "@vue/compiler-sfc": "^3.0.2", "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["@svgr/core", "@svgx/core", "@vue/compiler-sfc", "svelte"] }, "sha512-rv0XEJepajKzDLvRUWASM8K+8+/CCfZn2jtogXqg6RIp7kpatRc/aFrVJn8ANQA09e++lPEEv9yX8cC9enc+QQ=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], @@ -357,6 +411,8 @@ "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], @@ -371,6 +427,12 @@ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "@iconify/utils/@antfu/install-pkg": ["@antfu/install-pkg@2.0.1", "", { "dependencies": { "package-manager-detector": "^1.7.0", "tinyexec": "^1.2.4" } }, "sha512-iCKVQcIC0e3oDxEfs3SHQGW+ovhBMZmS1TE+bTk50rVyMCBmCfClv7Qi3HQKlumYwvjb/iIMeWCW2i67q6kFfQ=="], + "@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], } } diff --git a/package.json b/package.json index f7ae51e..3fc6100 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,12 @@ "vue": "^3.5.13" }, "devDependencies": { + "@iconify-json/hugeicons": "^1.2.34", + "@iconify-json/mingcute": "^1.2.8", + "@iconify-json/solar": "^1.2.10", "@vitejs/plugin-vue": "^6.0.0", "typescript": "^5.7.3", + "unplugin-icons": "^23.0.1", "vite": "^7.0.0", "vue-tsc": "^2.2.0" } diff --git a/scripts/find-icon.mjs b/scripts/find-icon.mjs new file mode 100644 index 0000000..740a872 --- /dev/null +++ b/scripts/find-icon.mjs @@ -0,0 +1,13 @@ +// 查图标名:node scripts/find-icon.mjs <关键词...> +// 在已安装的 @iconify-json/{hugeicons,solar,mingcute} 里按名字模糊匹配。 +import { readFileSync } from "node:fs"; +const sets = ["hugeicons", "solar", "mingcute"]; +const terms = process.argv.slice(2); +for (const s of sets) { + const j = JSON.parse(readFileSync(`node_modules/@iconify-json/${s}/icons.json`, "utf8")); + const names = Object.keys(j.icons); + for (const t of terms) { + const hit = names.filter((n) => n.includes(t)); + if (hit.length) console.log(`[${s}] ${t}: ` + hit.slice(0, 14).join(", ") + (hit.length > 14 ? ` … (${hit.length})` : "")); + } +} diff --git a/src/components/BottomDock.vue b/src/components/BottomDock.vue index 8363fb4..39ffdb0 100644 --- a/src/components/BottomDock.vue +++ b/src/components/BottomDock.vue @@ -3,8 +3,7 @@ * 【T7 — 底部悬浮胶囊】 * * 浮在画布区下缘居中的圆角胶囊,装高频快捷操作: - * − | 缩放百分比(点开选 50% / 100% / 200% / 适应画布 / 缩放到选中)| + - * ⤢ 适应画布 + * − | 缩放百分比(点开选 50% / 100% / 200% / 适应画布 / 缩放到选中)| + | 适应画布 * * 缩放数值读 `useViewport().zoom`(revision 无关,zoom 是 vue ref,会跟着 * 视口变化自动更新)。它是**浮层**,绝对定位在 .area-stage 里,不占布局空间。 @@ -20,6 +19,7 @@ import { useEditor } from "@/editor/useEditor"; import { useViewport } from "@/editor/useViewport"; import { runCommand } from "@/editor/commands"; import { keyHintOf } from "@/editor/useShortcuts"; +import { IconZoomIn, IconZoomOut, IconFit, IconChevronDown } from "@/ui/icons"; const { zoom, zoomAt, MIN_ZOOM, MAX_ZOOM } = useViewport(); const { canvas } = useEditor(); @@ -64,16 +64,6 @@ onBeforeUnmount(() => { window.removeEventListener("keydown", onKeydown); }); -/* ================================================================ 图标 */ -const ICON_ATTRS = 'width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"'; - -const icons: Record = { - minus: ``, - plus: ``, - chevron: ``, - fit: ``, -}; - /** 当前 zoom 是否正好是某个菜单数值档(±0.05% 容差),用于打勾。 */ function atLevel(target: number): boolean { return Math.abs(zoom.value - target / 100) < 0.0005; @@ -83,12 +73,14 @@ function atLevel(target: number): boolean { @@ -199,8 +195,11 @@ function atLevel(target: number): boolean { font-weight: 600; } .pct-caret { - display: inline-flex; color: var(--fg-dim); + font-size: 14px; +} +.dock .icon { + font-size: 15px; } /* ---- 缩放菜单 ---- */ diff --git a/src/components/Inspector.vue b/src/components/Inspector.vue index a66eb38..5d86420 100644 --- a/src/components/Inspector.vue +++ b/src/components/Inspector.vue @@ -1,23 +1,26 @@ - - - - diff --git a/src/components/ToolPanels.vue b/src/components/ToolPanels.vue index a70f099..40c6f3d 100644 --- a/src/components/ToolPanels.vue +++ b/src/components/ToolPanels.vue @@ -5,126 +5,96 @@ import SourcePanel from "./panels/SourcePanel.vue"; import FilterPanel from "./panels/FilterPanel.vue"; import TextPanel from "./panels/TextPanel.vue"; import ExportPanel from "./panels/ExportPanel.vue"; +import { + IconImage, + IconFilter, + IconText, + IconExport, + IconPlus, + IconChevronDown, +} from "@/ui/icons"; +import { toolGroupRequest, type ToolGroupId } from "@/ui/toolGroups"; /** - * 【T10 — 工具面板的重新安置】 + * 右栏下半:背景 / 滤镜 / 文字 / 导出与模板 四个工具面板的容器。 * - * 旧版把所有东西塞在一个右侧栏里。改成 Figma 式布局之后,那四个面板 - * (背景 / 滤镜 / 文字 / 导出与模板)没有任何挂载点——功能不可达。这里 - * 把它们收成**可折叠的分组**,占右栏下半层(App.vue 已挂好,不动布局): + * 版式照 Figma 的设计面板:每组平时只是**一行 32px 的标题**(图标 + 名字 + 右侧 + * 加号),点开才展开内容。**默认四组全收**——这四个面板加起来一千多行控件, + * 常驻展开的话右栏一屏塞不下,找任何一个都得先滚。 * - * | 组 | 来源 | 内容何时可用 | - * |----------|---------------|---------------------------| - * | 背景 | SourcePanel | 常驻 | - * | 滤镜 | FilterPanel | 选中图片时 | - * | 文字 | TextPanel | 选中文字时 | - * | 导出与模板| ExportPanel | 常驻 | + * 自动展开只发生在一种情况:选中的对象类型正好对应某一组(选图片 → 滤镜, + * 选文字 → 文字)。这是「我要改这个」的明确信号,替用户省一次点击;反过来 + * 不自动收起,收起是用户自己的事。 * - * 规则: - * - 分组标题可手动折叠(展开与否只是 UI 状态,不销毁面板)。 - * - **选中对象自动展开相关的那组**:选中图片自动展开「滤镜」, - * 选中文字自动展开「文字」。 - * - 内容是否可用看 `selectionKind`(filter 要图片、text 要文字)。 - * 面板组件**保持挂载**(用 v-show 而不是 v-if 隐藏),这样它们内部 - * watch activeObject 的状态能一直跟随画布;条件不满足时组内显示一行提示。 + * 面板组件用 v-show 而不是 v-if 隐藏,保持挂载,它们内部 watch activeObject + * 的状态才能一直跟着画布走。滤镜和文字组在条件不满足时显示一行提示。 + * + * 别处要把用户送到某个面板,走 `@/ui/toolGroups` 的 `openToolGroup(id)`。 */ const { selectionKind } = useEditor(); -type GroupId = "bg" | "filter" | "text" | "export"; +const GROUPS = [ + { id: "bg", label: "背景", icon: IconImage, scope: "" }, + { id: "filter", label: "滤镜", icon: IconFilter, scope: "图片" }, + { id: "text", label: "文字", icon: IconText, scope: "文字" }, + { id: "export", label: "导出与模板", icon: IconExport, scope: "" }, +] as const; -/** 每组是否展开。展开与否是纯 UI 状态;组的内容是否可用看 selectionKind。 */ -const open = reactive>({ - bg: true, +/** 每组是否展开。纯 UI 状态;组的内容是否可用看 selectionKind。 */ +const open = reactive>({ + bg: false, filter: false, text: false, - export: true, + export: false, }); const isImage = computed(() => selectionKind.value === "image"); const isText = computed(() => selectionKind.value === "text"); -/** 选中对象类型变化 → 自动展开相关的那组。只展开不收起,收起是用户自己的事。 */ watch(selectionKind, (kind) => { if (kind === "image") open.filter = true; else if (kind === "text") open.text = true; }); -function toggle(id: GroupId) { +/** 别处(顶栏的导出按钮)点名要展开某一组。 */ +watch(toolGroupRequest, (req) => { + if (req) open[req.id] = true; +}); + +function toggle(id: ToolGroupId) { open[id] = !open[id]; } diff --git a/src/components/panels/CanvasPanel.vue b/src/components/panels/CanvasPanel.vue deleted file mode 100644 index 9448d0b..0000000 --- a/src/components/panels/CanvasPanel.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - - - diff --git a/src/style.css b/src/style.css index b7f85d2..5fa5ded 100644 --- a/src/style.css +++ b/src/style.css @@ -19,9 +19,6 @@ /* 画布区的底色,比面板更深一档,让画布本身浮出来。 */ --stage-bg: #0f0f11; - /* 旧的单侧栏宽度,等 T10 把旧面板迁完就删。 */ - --sidebar-w: 320px; - color-scheme: dark; font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; font-size: 13px; @@ -82,3 +79,15 @@ label { gap: 8px; align-items: center; } + +/* 图标(unplugin-icons 给每个图标组件挂的 defaultClass)。 + * hugeicons 是 fill:none + stroke:currentColor,所以颜色跟着按钮的 color 走, + * 这里只管尺寸和基线对齐。要另一档大小就在容器上写 `.icon { font-size: … }`—— + * 图标的宽高是 1em。 */ +.icon { + width: 1em; + height: 1em; + flex: none; + font-size: 16px; + display: block; +} diff --git a/src/ui/icons.ts b/src/ui/icons.ts new file mode 100644 index 0000000..2b5bc5d --- /dev/null +++ b/src/ui/icons.ts @@ -0,0 +1,81 @@ +/** + * 全站图标出口。**组件不要直接写 `~icons/...`**,一律从这里 import。 + * + * 这么做的理由是「同一个动作在任何地方长同一个样」:删除在图层行、右键菜单、 + * 选中浮条上是三个组件,但只要它们都用 `IconDelete`,换图标就是改这一行。 + * + * 图标集统一用 **hugeicons**(24×24、`fill:none` + `stroke:currentColor`、 + * stroke-width 1.5),风格和 Figma 的界面图标一致。混用别的集会在同一行里 + * 出现粗细不一的描边,所以要加新图标先在 hugeicons 里找: + * + * node scripts/find-icon.mjs <关键词> + * + * `~icons/*` 由 vite.config.ts 的 unplugin-icons 在构建期解析成 Vue 组件, + * 只有用到的图标会进产物,不联网。类型来自 tsconfig 的 + * `unplugin-icons/types/vue`。 + * + * 尺寸和颜色由 CSS 控制:插件给每个图标挂了 `.icon` 类,规则在 style.css。 + */ + +/* ---- 工具 ---- */ +export { default as IconSelect } from "~icons/hugeicons/cursor-01"; +export { default as IconText } from "~icons/hugeicons/text-font"; +export { default as IconRect } from "~icons/hugeicons/square"; +export { default as IconImage } from "~icons/hugeicons/image-01"; + +/* ---- 历史与导出 ---- */ +export { default as IconUndo } from "~icons/hugeicons/undo"; +export { default as IconRedo } from "~icons/hugeicons/redo"; +export { default as IconExport } from "~icons/hugeicons/download-01"; + +/* ---- 视口 ---- */ +export { default as IconZoomIn } from "~icons/hugeicons/plus-sign"; +export { default as IconZoomOut } from "~icons/hugeicons/minus-sign"; +export { default as IconFit } from "~icons/hugeicons/maximize-01"; + +/* ---- 图层行 ---- */ +export { default as IconEye } from "~icons/hugeicons/eye"; +export { default as IconEyeOff } from "~icons/hugeicons/eye-off"; +export { default as IconLocked } from "~icons/hugeicons/square-lock-01"; +export { default as IconUnlocked } from "~icons/hugeicons/square-unlock-01"; +export { default as IconDelete } from "~icons/hugeicons/delete-02"; +export { default as IconDuplicate } from "~icons/hugeicons/copy-01"; + +/* ---- 层序 ---- */ +export { default as IconBringToFront } from "~icons/hugeicons/layer-bring-to-front"; +export { default as IconSendToBack } from "~icons/hugeicons/layer-send-to-back"; +export { default as IconBringForward } from "~icons/hugeicons/arrow-up-01"; +export { default as IconSendBackward } from "~icons/hugeicons/arrow-down-01"; + +/* ---- 属性 ---- */ +export { default as IconArtboard } from "~icons/hugeicons/artboard"; +export { default as IconLinked } from "~icons/hugeicons/link-02"; +export { default as IconUnlinked } from "~icons/hugeicons/unlink-02"; +export { default as IconAngle } from "~icons/hugeicons/angle"; +export { default as IconRadius } from "~icons/hugeicons/square-round-corner"; +export { default as IconOpacity } from "~icons/hugeicons/contrast"; + +/* ---- 对齐与分布 ---- */ +export { default as IconAlignLeft } from "~icons/hugeicons/align-left"; +export { default as IconAlignCenterH } from "~icons/hugeicons/align-horizontal-center"; +export { default as IconAlignRight } from "~icons/hugeicons/align-right"; +export { default as IconAlignTop } from "~icons/hugeicons/align-top"; +export { default as IconAlignCenterV } from "~icons/hugeicons/align-vertical-center"; +export { default as IconAlignBottom } from "~icons/hugeicons/align-bottom"; +export { default as IconDistributeH } from "~icons/hugeicons/distribute-horizontal-center"; +export { default as IconDistributeV } from "~icons/hugeicons/distribute-vertical-center"; + +/* ---- 文字 ---- */ +export { default as IconBold } from "~icons/hugeicons/text-bold"; +export { default as IconTextLeft } from "~icons/hugeicons/text-align-left"; +export { default as IconTextCenter } from "~icons/hugeicons/text-align-center"; +export { default as IconTextRight } from "~icons/hugeicons/text-align-right"; + +/* ---- 右栏分组 ---- */ +export { default as IconFilter } from "~icons/hugeicons/blur"; +export { default as IconTemplate } from "~icons/hugeicons/layers-01"; + +/* ---- 通用 ---- */ +export { default as IconChevronRight } from "~icons/hugeicons/chevron-right"; +export { default as IconChevronDown } from "~icons/hugeicons/chevron-down"; +export { default as IconPlus } from "~icons/hugeicons/plus-sign"; diff --git a/src/ui/toolGroups.ts b/src/ui/toolGroups.ts new file mode 100644 index 0000000..ce8ea72 --- /dev/null +++ b/src/ui/toolGroups.ts @@ -0,0 +1,22 @@ +import { ref } from "vue"; + +/** 右栏下半的四个工具分组。 */ +export type ToolGroupId = "bg" | "filter" | "text" | "export"; + +/** + * 「请求展开某一组」的信号。 + * + * 四个组默认全部收起,所以顶栏那个导出按钮光滚到底是没用的——滚过去只看得到 + * 一行标题。别处要把用户送到某个面板前,就调 `openToolGroup(id)`, + * `ToolPanels` watch 这个 ref 把对应的组打开。 + * + * 带一个自增序号,是为了让**连点两次同一个组**也能触发 watch:只放 id 的话 + * 第二次赋的是同一个字符串,ref 不变,watch 不响应,用户会觉得按钮坏了。 + */ +export const toolGroupRequest = ref<{ id: ToolGroupId; seq: number } | null>(null); + +let seq = 0; + +export function openToolGroup(id: ToolGroupId) { + toolGroupRequest.value = { id, seq: seq++ }; +} diff --git a/tsconfig.json b/tsconfig.json index ea8e2c7..df85e23 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2022", "DOM", "DOM.Iterable"], - "types": ["vite/client", "node"], + "types": ["vite/client", "node", "unplugin-icons/types/vue"], "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, diff --git a/vite.config.ts b/vite.config.ts index 5945ce0..d441d34 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ import { fileURLToPath, URL } from "node:url"; import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; +import Icons from "unplugin-icons/vite"; import { fontsPlugin } from "./src/fonts/vite-plugin-fonts"; /** @@ -9,7 +10,14 @@ import { fontsPlugin } from "./src/fonts/vite-plugin-fonts"; * 这是刻意的——自用工具不值得为此单起一个后端进程。 */ export default defineConfig({ - plugins: [vue(), fontsPlugin()], + plugins: [ + vue(), + // 图标:`import IconFoo from "~icons/solar/xxx-linear"` 在**构建期**从 + // `@iconify-json/*` 里取出 SVG 编成 Vue 组件,不联网、不打包整套图标集。 + // `defaultClass` 让所有图标共享一条样式规则(尺寸/描边色见 style.css 的 .icon)。 + Icons({ compiler: "vue3", scale: 1, defaultClass: "icon" }), + fontsPlugin(), + ], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)),