diff --git a/bun.lock b/bun.lock
index ae67c8f..83385c2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,9 +6,11 @@
"name": "person-panel",
"dependencies": {
"@libsql/client": "0.17.3",
- "@module-federation/vite": "^1.15.5",
+ "@module-federation/vite": "1.15.5",
"@nuxt/icon": "2.2.2",
+ "@types/lodash-es": "4.17.12",
"bcryptjs": "3.0.3",
+ "bolt-ui": "workspace:*",
"cache": "workspace:*",
"common": "workspace:*",
"croner": "10.0.1",
@@ -17,6 +19,7 @@
"drizzle-pkg": "workspace:*",
"drizzle-seed": "0.3.1",
"drizzle-zod": "0.8.3",
+ "lodash-es": "4.18.1",
"log4js": "6.9.1",
"logger": "workspace:*",
"mime": "4.1.0",
@@ -34,12 +37,16 @@
"@types/multer": "2.1.0",
"@types/node": "25.8.0",
"drizzle-kit": "0.31.10",
+ "sass-embedded": "1.100.0",
"tsconfig": "workspace:*",
"tsx": "4.21.0",
"typescript": "6.0.2",
"vue3-toastify": "0.2.9",
},
},
+ "packages/bolt-ui": {
+ "name": "bolt-ui",
+ },
"packages/cache": {
"name": "cache",
"version": "0.1.0",
@@ -120,6 +127,8 @@
"@bomb.sh/tab": ["@bomb.sh/tab@0.0.15", "https://registry.npmmirror.com/@bomb.sh/tab/-/tab-0.0.15.tgz", { "peerDependencies": { "cac": "^6.7.14", "citty": "^0.1.6 || ^0.2.0", "commander": "^13.1.0" }, "optionalPeers": ["cac", "citty", "commander"], "bin": { "tab": "dist/bin/cli.mjs" } }, "sha512-Y90ub44TAvbdO9P8mcD/XPyQjFhiR5xmd4Fk7JErmWmEWEUimNnjWiBrVZ16Tj3GA1rLZ+uvCN2V/pzLawv31g=="],
+ "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.0", "", {}, "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA=="],
+
"@clack/core": ["@clack/core@1.3.1", "https://registry.npmmirror.com/@clack/core/-/core-1.3.1.tgz", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA=="],
"@clack/prompts": ["@clack/prompts@1.4.0", "https://registry.npmmirror.com/@clack/prompts/-/prompts-1.4.0.tgz", { "dependencies": { "@clack/core": "1.3.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA=="],
@@ -590,6 +599,10 @@
"@types/jsesc": ["@types/jsesc@2.5.1", "https://registry.npmmirror.com/@types/jsesc/-/jsesc-2.5.1.tgz", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="],
+ "@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
+
+ "@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
+
"@types/multer": ["@types/multer@2.1.0", "https://registry.npmmirror.com/@types/multer/-/multer-2.1.0.tgz", { "dependencies": { "@types/express": "*" } }, "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA=="],
"@types/node": ["@types/node@25.8.0", "https://registry.npmmirror.com/@types/node/-/node-25.8.0.tgz", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="],
@@ -720,6 +733,8 @@
"bl": ["bl@4.1.0", "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
+ "bolt-ui": ["bolt-ui@workspace:packages/bolt-ui"],
+
"boolbase": ["boolbase@1.0.0", "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
@@ -762,6 +777,8 @@
"color-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+ "colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
+
"commander": ["commander@2.20.3", "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"common": ["common@workspace:packages/common"],
@@ -1000,6 +1017,8 @@
"h3": ["h3@1.15.11", "https://registry.npmmirror.com/h3/-/h3-1.15.11.tgz", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
"hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="],
@@ -1024,6 +1043,8 @@
"image-meta": ["image-meta@0.2.2", "https://registry.npmmirror.com/image-meta/-/image-meta-0.2.2.tgz", {}, "sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA=="],
+ "immutable": ["immutable@5.1.6", "", {}, "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ=="],
+
"import-meta-resolve": ["import-meta-resolve@4.2.0", "https://registry.npmmirror.com/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
"impound": ["impound@1.1.5", "https://registry.npmmirror.com/impound/-/impound-1.1.5.tgz", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "es-module-lexer": "^2.0.0", "pathe": "^2.0.3", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" } }, "sha512-5AUn+QE0UofqNHu5f2Skf6Svukdg4ehOIq8O0EtqIx4jta0CDZYBPqpIHt0zrlUTiFVYlLpeH39DoikXBjPKpA=="],
@@ -1132,6 +1153,8 @@
"lodash": ["lodash@4.18.1", "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
+ "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
+
"lodash.defaults": ["lodash.defaults@4.2.0", "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.isarguments": ["lodash.isarguments@3.1.0", "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
@@ -1446,10 +1469,52 @@
"run-parallel": ["run-parallel@1.2.0", "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
+ "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
+
"safe-buffer": ["safe-buffer@5.2.1", "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+ "sass": ["sass@1.100.0", "", { "dependencies": { "chokidar": "^5.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ=="],
+
+ "sass-embedded": ["sass-embedded@1.100.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", "immutable": "^5.1.5", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.100.0", "sass-embedded-android-arm": "1.100.0", "sass-embedded-android-arm64": "1.100.0", "sass-embedded-android-riscv64": "1.100.0", "sass-embedded-android-x64": "1.100.0", "sass-embedded-darwin-arm64": "1.100.0", "sass-embedded-darwin-x64": "1.100.0", "sass-embedded-linux-arm": "1.100.0", "sass-embedded-linux-arm64": "1.100.0", "sass-embedded-linux-musl-arm": "1.100.0", "sass-embedded-linux-musl-arm64": "1.100.0", "sass-embedded-linux-musl-riscv64": "1.100.0", "sass-embedded-linux-musl-x64": "1.100.0", "sass-embedded-linux-riscv64": "1.100.0", "sass-embedded-linux-x64": "1.100.0", "sass-embedded-unknown-all": "1.100.0", "sass-embedded-win32-arm64": "1.100.0", "sass-embedded-win32-x64": "1.100.0" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-Ut8wlQSk19tm7jMK6mz6cF1+e+E7tUnW2tM02zQDPnOTcVbV8qCQG8UWxZkkNlY50+hV3hqP24OOkUlMz8xBpw=="],
+
+ "sass-embedded-all-unknown": ["sass-embedded-all-unknown@1.100.0", "", { "dependencies": { "sass": "1.100.0" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-auFtXY/kwYILmSVjtBDwyj0axcLbYYiffOKWoaXHnI5bsYwiRbBh3EneR1rpbX2ZIZCrwX93i5pxKLTZF/662Q=="],
+
+ "sass-embedded-android-arm": ["sass-embedded-android-arm@1.100.0", "", { "os": "android", "cpu": "arm" }, "sha512-70f3HgX2pFNmzpGQ86n5e6QfWn2fP4QUQGfFQK0P1XH73ZLIzLo2YqygrGKGKeeqtc5eU2Wl1/xQzhzuKnO4kw=="],
+
+ "sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.100.0", "", { "os": "android", "cpu": "arm64" }, "sha512-W+Ru9JwTnfU0UX3jSZcbqFdtKFMcYdfFwytc57h2DgnqCOIiAqI2E06mABZBZC+r3LwXCBuS5GbXAGeVgvVDkA=="],
+
+ "sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.100.0", "", { "os": "android", "cpu": "none" }, "sha512-icU3o0V/uCSytSpf+tX5Lf51BvyQEbLzDUJfUi9etSauYBGHpPKkdtdZH0si4v98phq11Kl8rSV1SggksxF1Hg=="],
+
+ "sass-embedded-android-x64": ["sass-embedded-android-x64@1.100.0", "", { "os": "android", "cpu": "x64" }, "sha512-mevF9VQk6gEYByy8+jusaHGmd7Usb2ytX/DsEOd0JtOGCtcf1kh575xJ6OUBDIcJ15uLnbau/0iy1eP6WVBvWA=="],
+
+ "sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.100.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1PVlYi61POo93IT/FfrG1mc1tAHxeSTyUALF2aOFmXGWjVXr3bQzEQiBGCOvQbj/ix+5hNyXFXcEMEyKvtUJJA=="],
+
+ "sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.100.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-x97o3JnGyImZNCIVs9wQHJUE5QCvmVIKaH1cwrz/5dK7OT1FpeNiW+u9TUomP9hG6Ekjd8EL8NBHpxTfIhdjmg=="],
+
+ "sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.100.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9Ul7O1eKrc5YlhwWjkp8tZPSe3UEwSZ1uwUZOQom1HL0pRlBA6F/IlGZYFTLwnHMIP1fc77MMNaBRfc05mKMpw=="],
+
+ "sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.100.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Dwjmj8Z6VRy7rAi53JAdEwIyUjpfl7PhpSc2/LpQPQx+aO5Dp7Spaipkax0ufJl1SoDUdchCsM4y/88YaluorQ=="],
+
+ "sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.100.0", "", { "os": "linux", "cpu": "arm" }, "sha512-sl0JgbGloPyJg66XXx5UDSDScZ0oU85DpMQU4JU/sCUCFj1Z8zZ69SJWKTCNE4/jwnce7WI2zPCV5AG+RHOZJw=="],
+
+ "sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.100.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XpACJB2KjSLjf2e9uuvGVdOURsoNrFqgRiihhXyUHK9W0t3LIHb7z5MA/7XGPIT9bWSOO2zyw+rH/FHtDV/Yrg=="],
+
+ "sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.100.0", "", { "os": "linux", "cpu": "none" }, "sha512-ShvI0Kx04mwoCARwZ0UjiT97isQvzO80tAt91zmFyHLN9kelc/IrQi940farSm2xQVPCKdeVyeG0ekBsokSpYQ=="],
+
+ "sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.100.0", "", { "os": "linux", "cpu": "x64" }, "sha512-TDBCRWNuS4RDLQXvRc1gjZlWiWTWaWGp0Bwu/IKwJxov81lsvrCs3TihTyNXtW7V5aoN4Ky3r0QOkNb3mwmBnA=="],
+
+ "sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.100.0", "", { "os": "linux", "cpu": "none" }, "sha512-j4ENJGOheO+fm3j/yorLxCjBP6/XskrZx7dTLlT+lXYwN/qqCqoA/gsNLI0McS3DFM6GBwPiffzWsdWS8t6sEQ=="],
+
+ "sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.100.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0vUSN8j0WGtCJIOPh//EmUvYGHW0QOe5iul8qyhPk50MAcw49MA0r34AhftjDdx94ILPF6vApFs0gwHPQRlpVA=="],
+
+ "sass-embedded-unknown-all": ["sass-embedded-unknown-all@1.100.0", "", { "dependencies": { "sass": "1.100.0" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A=="],
+
+ "sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.100.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-iE+yxj+hUXwwbqpHkXxgAWTzeRfcWxJ7SSTQEPMk48lwq3oCrWLlz5sQuWHbuTK/i0GKQfROdP+hOmPi89yjUg=="],
+
+ "sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.100.0", "", { "os": "win32", "cpu": "x64" }, "sha512-qI4F8MI7/KYoy9NdjJfhSspG42WPkADSNDvwEV7qWvCSFC83koJssRsKO2/PfY+niZz6BG65Ic/D+A11h959hw=="],
+
"sax": ["sax@1.6.0", "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"scule": ["scule@1.3.0", "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
@@ -1536,7 +1601,7 @@
"stylehacks": ["stylehacks@7.0.11", "https://registry.npmmirror.com/stylehacks/-/stylehacks-7.0.11.tgz", { "dependencies": { "browserslist": "^4.28.2", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-iODNfhXVLqc5LADs+Y6Oh5wJuK5ZcHbVng8aiK3y9pjMQdc5hLrBW0eFU6FtnpNrE6PoEg/MmFTU4waotj5WNg=="],
- "supports-color": ["supports-color@10.2.2", "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
+ "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
@@ -1544,6 +1609,10 @@
"svgo": ["svgo@4.0.1", "https://registry.npmmirror.com/svgo/-/svgo-4.0.1.tgz", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="],
+ "sync-child-process": ["sync-child-process@1.0.2", "", { "dependencies": { "sync-message-port": "^1.0.0" } }, "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA=="],
+
+ "sync-message-port": ["sync-message-port@1.2.0", "", {}, "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg=="],
+
"tagged-tag": ["tagged-tag@1.0.0", "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
"tailwindcss": ["tailwindcss@4.3.0", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.0.tgz", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="],
@@ -1640,6 +1709,8 @@
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+ "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
+
"vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.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-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
"vite-dev-rpc": ["vite-dev-rpc@1.1.0", "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", { "dependencies": { "birpc": "^2.4.0", "vite-hot-client": "^2.1.0" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" } }, "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A=="],
@@ -1764,6 +1835,8 @@
"@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "https://registry.npmmirror.com/napi-wasm/-/napi-wasm-1.1.3.tgz", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
+ "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
+
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@rollup/plugin-inject/estree-walker": ["estree-walker@2.0.2", "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 8a7b683..bdf5695 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -1,10 +1,16 @@
import tailwindcss from '@tailwindcss/vite'
-import { federation } from "@module-federation/vite";
+// import { federation } from "@module-federation/vite";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
+ modules: ['@nuxt/icon', 'bolt-ui/nuxt'],
+
+ boltUi: {
+ importStyles: false,
+ },
+
app: {
head: {
link: [
@@ -69,7 +75,6 @@ export default defineNuxtConfig({
}
},
- modules: ['@nuxt/icon'],
icon: {
mode: 'css',
cssLayer: 'base'
diff --git a/package.json b/package.json
index a5a74ca..24e1d56 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,9 @@
"@libsql/client": "0.17.3",
"@module-federation/vite": "1.15.5",
"@nuxt/icon": "2.2.2",
+ "@types/lodash-es": "4.17.12",
"bcryptjs": "3.0.3",
+ "bolt-ui": "workspace:*",
"cache": "workspace:*",
"common": "workspace:*",
"croner": "10.0.1",
@@ -31,6 +33,7 @@
"drizzle-pkg": "workspace:*",
"drizzle-seed": "0.3.1",
"drizzle-zod": "0.8.3",
+ "lodash-es": "4.18.1",
"log4js": "6.9.1",
"logger": "workspace:*",
"mime": "4.1.0",
@@ -48,6 +51,7 @@
"@types/multer": "2.1.0",
"@types/node": "25.8.0",
"drizzle-kit": "0.31.10",
+ "sass-embedded": "1.100.0",
"tsconfig": "workspace:*",
"tsx": "4.21.0",
"typescript": "6.0.2",
diff --git a/packages/bolt-ui/components/Button/index.ts b/packages/bolt-ui/components/Button/index.ts
new file mode 100644
index 0000000..2955940
--- /dev/null
+++ b/packages/bolt-ui/components/Button/index.ts
@@ -0,0 +1,5 @@
+import { withInstall } from 'bolt-ui/utils/vue/install'
+import Button from './src/Button.vue'
+
+export const BoButton = withInstall(Button)
+export default BoButton
diff --git a/packages/bolt-ui/components/Button/src/Button.vue b/packages/bolt-ui/components/Button/src/Button.vue
new file mode 100644
index 0000000..48bb5ef
--- /dev/null
+++ b/packages/bolt-ui/components/Button/src/Button.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/packages/bolt-ui/components/Button/style/index.ts b/packages/bolt-ui/components/Button/style/index.ts
new file mode 100644
index 0000000..4bf1d74
--- /dev/null
+++ b/packages/bolt-ui/components/Button/style/index.ts
@@ -0,0 +1 @@
+import 'bolt-ui/theme-chalk/src/button.scss'
diff --git a/packages/bolt-ui/components/ConfigProvider/index.ts b/packages/bolt-ui/components/ConfigProvider/index.ts
new file mode 100644
index 0000000..8688ea2
--- /dev/null
+++ b/packages/bolt-ui/components/ConfigProvider/index.ts
@@ -0,0 +1,5 @@
+import { withInstall } from 'bolt-ui/utils/vue/install'
+import ConfigProvider from './src/ConfigProvider.vue'
+
+export const BoConfigProvider = withInstall(ConfigProvider)
+export default BoConfigProvider
diff --git a/packages/bolt-ui/components/ConfigProvider/src/ConfigProvider.vue b/packages/bolt-ui/components/ConfigProvider/src/ConfigProvider.vue
new file mode 100644
index 0000000..8398667
--- /dev/null
+++ b/packages/bolt-ui/components/ConfigProvider/src/ConfigProvider.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/packages/bolt-ui/components/ConfigProvider/src/Token.ts b/packages/bolt-ui/components/ConfigProvider/src/Token.ts
new file mode 100644
index 0000000..a389cf0
--- /dev/null
+++ b/packages/bolt-ui/components/ConfigProvider/src/Token.ts
@@ -0,0 +1,3 @@
+import { ComputedRef, InjectionKey } from 'vue'
+
+export const ConfigToken: InjectionKey> = Symbol('ConfigToken')
diff --git a/packages/bolt-ui/components/ConfigProvider/src/useConfigProvider.ts b/packages/bolt-ui/components/ConfigProvider/src/useConfigProvider.ts
new file mode 100644
index 0000000..5bcffc9
--- /dev/null
+++ b/packages/bolt-ui/components/ConfigProvider/src/useConfigProvider.ts
@@ -0,0 +1,10 @@
+import { inject } from 'vue'
+import { ConfigToken } from './Token'
+
+export function useConfigProvider() {
+ const config = inject(ConfigToken)
+ if (!config) {
+ throw new Error('ConfigProvider not found')
+ }
+ return config
+}
diff --git a/packages/bolt-ui/components/ConfigProvider/style/index.ts b/packages/bolt-ui/components/ConfigProvider/style/index.ts
new file mode 100644
index 0000000..e7f0809
--- /dev/null
+++ b/packages/bolt-ui/components/ConfigProvider/style/index.ts
@@ -0,0 +1 @@
+export {}
diff --git a/packages/bolt-ui/components/index.ts b/packages/bolt-ui/components/index.ts
new file mode 100644
index 0000000..f113dff
--- /dev/null
+++ b/packages/bolt-ui/components/index.ts
@@ -0,0 +1,2 @@
+export * from './Button'
+export * from './ConfigProvider'
diff --git a/packages/bolt-ui/hooks/createContext/index.ts b/packages/bolt-ui/hooks/createContext/index.ts
new file mode 100644
index 0000000..b807bb0
--- /dev/null
+++ b/packages/bolt-ui/hooks/createContext/index.ts
@@ -0,0 +1,67 @@
+import { getCurrentInstance, inject, provide } from 'vue'
+
+import type { InjectionKey } from 'vue'
+
+export interface CreateContextOptions {
+ /** 严格模式:未找到 Provider 且没有 defaultValue 时是否抛错,默认 true */
+ strict?: boolean
+ /** 默认值工厂:当没有 Provider 时调用,用返回值兜底,避免共享状态 */
+ defaultValue?: () => T
+ /** 自定义 InjectionKey,一般不需要传 */
+ key?: InjectionKey
+}
+
+/**
+ * 创建一套基于 provide/inject 的上下文工具。
+ *
+ * 使用方式:
+ * const FooContext = createContext('Foo', {
+ * strict: true,
+ * defaultValue: () => ({ ... })
+ * })
+ *
+ * FooContext.provideContext(...)
+ * const ctx = FooContext.useContext()
+ */
+export const createContext = (name?: string, options: CreateContextOptions = {}) => {
+ const { strict = true, defaultValue, key } = options
+ const contextKey: InjectionKey = key ?? (Symbol(name ?? 'Context') as InjectionKey)
+
+ const useContext = () => {
+ // 在 setup 之外调用没有意义,这里主动给出更友好的错误
+ if (!getCurrentInstance()) {
+ throw new Error('useContext 只能在 setup 或生命周期钩子中使用')
+ }
+
+ const ctx = inject(contextKey, undefined)
+ if (ctx !== undefined) {
+ return ctx
+ }
+
+ if (defaultValue) {
+ // 每次调用 defaultValue 都返回一个“新”默认值,避免跨实例共享
+ return defaultValue()
+ }
+
+ if (strict) {
+ throw new Error(
+ `未找到上层 Provider,请确保已经调用 provideContext 或在正确的组件树中使用(${name ?? 'Context'})`
+ )
+ }
+
+ return undefined as unknown as T
+ }
+
+ const provideContext = (context: T) => {
+ provide(contextKey, context)
+ }
+
+ return {
+ /** 当前上下文使用的 InjectionKey */
+ key: contextKey,
+ /** 在上层组件中调用,用于提供上下文 */
+ provideContext,
+ /** 在子组件中调用,用于消费上下文,如果未找到会抛出错误 / 或返回默认值 */
+ useContext
+ }
+}
diff --git a/packages/bolt-ui/hooks/index.ts b/packages/bolt-ui/hooks/index.ts
new file mode 100644
index 0000000..5569b19
--- /dev/null
+++ b/packages/bolt-ui/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useClickOutside'
diff --git a/packages/bolt-ui/hooks/useClickOutside/index.ts b/packages/bolt-ui/hooks/useClickOutside/index.ts
new file mode 100644
index 0000000..ae22329
--- /dev/null
+++ b/packages/bolt-ui/hooks/useClickOutside/index.ts
@@ -0,0 +1,38 @@
+import { onMounted, onUnmounted, Ref } from 'vue'
+
+export function useClickOutside(
+ elementRef: Ref | HTMLElement,
+ callback: (event: MouseEvent) => void,
+ options: {
+ ignore?: Ref[] // 需要忽略的元素
+ } = {}
+) {
+ const { ignore = [] } = options
+
+ const handler = (event: MouseEvent) => {
+ const el = elementRef instanceof HTMLElement ? elementRef : elementRef.value
+
+ if (!el) return
+
+ // 检查点击是否在忽略的元素内
+ const isIgnored = ignore.some((ref) => {
+ const element = ref?.value
+ return element && (element === event.target || element.contains(event.target as Node))
+ })
+
+ if (isIgnored) return
+
+ // 检查点击是否在目标元素外
+ if (!(el === event.target || el.contains(event.target as Node))) {
+ callback(event)
+ }
+ }
+
+ onMounted(() => {
+ document.addEventListener('click', handler, true)
+ })
+
+ onUnmounted(() => {
+ document.removeEventListener('click', handler, true)
+ })
+}
diff --git a/packages/bolt-ui/index.ts b/packages/bolt-ui/index.ts
new file mode 100644
index 0000000..ca23918
--- /dev/null
+++ b/packages/bolt-ui/index.ts
@@ -0,0 +1,2 @@
+export * from './components'
+export * from './hooks'
diff --git a/packages/bolt-ui/locales/index.ts b/packages/bolt-ui/locales/index.ts
new file mode 100644
index 0000000..e5ad4e8
--- /dev/null
+++ b/packages/bolt-ui/locales/index.ts
@@ -0,0 +1,68 @@
+import { get } from 'lodash-es'
+
+import zh from './languages/zh.json'
+import en from './languages/en.json'
+import { reactive } from 'vue'
+
+const Languages = {
+ zh: zh,
+ en: en
+}
+
+export type LanguagesType = keyof typeof Languages
+
+const LocaleState = reactive<{
+ locale: LanguagesType
+}>({
+ locale: 'zh'
+})
+
+type FlattenObject = T extends object
+ ? {
+ [K in keyof T & (string | number)]: FlattenObject<
+ T[K],
+ Prefix extends '' ? `${K}` : `${Prefix}.${K}`
+ >
+ }[keyof T & (string | number)]
+ : Prefix
+
+type FlattenKeys = FlattenObject
+
+type TranslationKey = FlattenKeys
+
+function useLocale() {
+ function setLocale(locale: LanguagesType) {
+ LocaleState.locale = locale
+ }
+
+ function getLocale(): string {
+ return LocaleState.locale
+ }
+
+ function t(key: TranslationKey, replacements?: Record): string {
+ let text: string =
+ LocaleState.locale in Languages
+ ? get(Languages[LocaleState.locale], key)
+ : get(Languages['zh'], key)
+ if (!text) {
+ text = get(Languages['zh'], key)
+ if (!text) {
+ return key
+ }
+ }
+ if (replacements) {
+ Object.entries(replacements).forEach(([key, value]) => {
+ text = text.replace(new RegExp(`{${key}}`, 'g'), value)
+ })
+ }
+ return text
+ }
+
+ return {
+ setLocale,
+ getLocale,
+ t
+ }
+}
+
+export { useLocale }
diff --git a/packages/bolt-ui/locales/languages/en.json b/packages/bolt-ui/locales/languages/en.json
new file mode 100644
index 0000000..371ccb3
--- /dev/null
+++ b/packages/bolt-ui/locales/languages/en.json
@@ -0,0 +1,11 @@
+{
+ "input": {
+ "placeholder": "Please enter content"
+ },
+ "select": {
+ "placeholder": "Please select"
+ },
+ "empty": {
+ "description": "No Data"
+ }
+}
diff --git a/packages/bolt-ui/locales/languages/zh.json b/packages/bolt-ui/locales/languages/zh.json
new file mode 100644
index 0000000..9868ad6
--- /dev/null
+++ b/packages/bolt-ui/locales/languages/zh.json
@@ -0,0 +1,11 @@
+{
+ "input": {
+ "placeholder": "请输入内容"
+ },
+ "select": {
+ "placeholder": "请选择"
+ },
+ "empty": {
+ "description": "暂无数据"
+ }
+}
diff --git a/packages/bolt-ui/nuxt.ts b/packages/bolt-ui/nuxt.ts
new file mode 100644
index 0000000..d55b219
--- /dev/null
+++ b/packages/bolt-ui/nuxt.ts
@@ -0,0 +1,136 @@
+import {
+ defineNuxtModule,
+ addComponent,
+ addImports,
+ createResolver,
+} from '@nuxt/kit'
+import { readdirSync, existsSync, readFileSync } from 'node:fs'
+import { join } from 'node:path'
+
+export interface ModuleOptions {
+ /**
+ * Whether to import global base styles (CSS variables / design tokens).
+ * @default true
+ */
+ importStyles?: boolean
+}
+
+function kebabCase(str: string): string {
+ return str.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
+}
+
+// ── 从源码中提取 export function / export const 名称 ─────────
+function extractExports(filePath: string): string[] {
+ try {
+ const content = readFileSync(filePath, 'utf-8')
+ const names: string[] = []
+ const re = /export\s+(?:function|const|let|var)\s+(\w+)/g
+ let m: RegExpExecArray | null
+ while ((m = re.exec(content)) !== null) names.push(m[1] as any)
+ return names
+ } catch {
+ return []
+ }
+}
+
+function isComposable(name: string): boolean {
+ return name.startsWith('use') || name === 'createContext'
+}
+
+// ── Vite 插件:自动注入组件样式 ────────────────────────────
+function BoltUiStylePlugin(styleMap: Record) {
+ return {
+ name: 'bolt-ui:style-inject',
+ enforce: 'post',
+ transform(code: string, id: string) {
+ if (!/\.(vue|tsx?|jsx?)$/.test(id)) return null
+ if (id.includes('node_modules')) return null
+
+ const matchedStyles = Object.entries(styleMap)
+ .filter(([name]) => code.includes(name))
+ .map(([, style]) => style)
+
+ if (matchedStyles.length === 0) return null
+
+ const imports = [...new Set(matchedStyles)]
+ .map(p => `import '${p}';`)
+ .join('\n')
+
+ return {
+ code: imports + '\n' + code,
+ map: null,
+ }
+ },
+ }
+}
+
+export default defineNuxtModule({
+ meta: {
+ name: 'bolt-ui',
+ configKey: 'boltUi',
+ },
+ defaults: {
+ importStyles: true,
+ },
+ setup(options, nuxt) {
+ const resolver = createResolver(import.meta.url)
+
+ // ── CSS 变量层(design tokens)────────────────────────────
+ if (options.importStyles) {
+ nuxt.options.css.push(resolver.resolve('./theme-chalk/src/theme/index.scss'))
+ }
+
+ // ── 自动扫描组件目录 ────────────────────────────────────
+ const componentsDir = resolver.resolve('./components')
+ const componentStyleMap: Record = {}
+
+ for (const entry of readdirSync(componentsDir, { withFileTypes: true })) {
+ if (!entry.isDirectory()) continue
+
+ const name = entry.name
+ const indexPath = join(componentsDir, name, 'index.ts')
+ if (!existsSync(indexPath)) continue
+
+ // 注册组件
+ addComponent({
+ name: `Bo${name}`,
+ export: `Bo${name}`,
+ filePath: resolver.resolve(`./components/${name}/index.ts`),
+ })
+
+ // 构建样式映射(约定:theme-chalk/src/${kebab}.scss)
+ const scssPath = join(componentsDir, '..', 'theme-chalk/src', `${kebabCase(name)}.scss`)
+ if (existsSync(scssPath)) {
+ componentStyleMap[`Bo${name}`] = `bolt-ui/theme-chalk/src/${kebabCase(name)}.scss`
+ }
+ }
+
+ // ── Vite 插件:按需注入组件样式 ─────────────────────────
+ nuxt.hook('vite:extendConfig', (config) => {
+ // @ts-ignore
+ config.plugins = config.plugins || []
+ config.plugins.push(BoltUiStylePlugin(componentStyleMap) as any)
+ })
+
+ // ── 自动扫描 composable 目录 ──────────────────────────
+ for (const scanDir of ['hooks', 'utils', 'locales']) {
+ const absDir = resolver.resolve(`./${scanDir}`)
+ if (!existsSync(absDir)) continue
+
+ const walk = (dir: string) => {
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const fullPath = join(dir, entry.name)
+ if (entry.isDirectory()) {
+ walk(fullPath)
+ } else if (entry.name === 'index.ts') {
+ const names = extractExports(fullPath).filter(isComposable)
+ for (const name of names) {
+ addImports({ name, as: name, from: fullPath })
+ }
+ }
+ }
+ }
+ walk(absDir)
+ }
+ },
+})
diff --git a/packages/bolt-ui/package.json b/packages/bolt-ui/package.json
new file mode 100644
index 0000000..7155e2d
--- /dev/null
+++ b/packages/bolt-ui/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "bolt-ui",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "build": "tsc -p ./tsconfig.json"
+ }
+}
diff --git a/packages/bolt-ui/resolver.ts b/packages/bolt-ui/resolver.ts
new file mode 100644
index 0000000..84dc59d
--- /dev/null
+++ b/packages/bolt-ui/resolver.ts
@@ -0,0 +1,67 @@
+const noStylesComponents: any[] = []
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export default (options: any = {}): any => {
+ let optionsResolved: any
+
+ async function resolveOptions() {
+ if (optionsResolved) return optionsResolved
+ optionsResolved = {
+ exclude: undefined,
+ noStylesComponents: options.noStylesComponents || [],
+ ...options
+ }
+ return optionsResolved
+ }
+ return {
+ type: 'component',
+ resolve: async (name: string) => {
+ const options = await resolveOptions()
+
+ if ([...options.noStylesComponents, ...noStylesComponents].includes(name))
+ return resolveComponent(name, { ...options, importStyle: false })
+ else return resolveComponent(name, options)
+ }
+ }
+}
+
+// function kebabCase(key: string) {
+// const result = key.replace(/([A-Z])/g, ' $1').trim()
+// return result.split(' ').join('-').toLowerCase()
+// }
+
+function pascalCase(key: string): string {
+ // 第一步:将所有分隔符(-、_、空格)替换为空格,统一处理
+ const replaced = key.replace(/[-_\s]/g, ' ')
+ // 第二步:处理可能的连续大写字母(如HTML),在大写字母前加空格(除了开头)
+ const spaced = replaced.replace(/([A-Z])/g, (_, p1, index) => {
+ return index === 0 ? p1 : ` ${p1}`
+ })
+ // 第三步:去除首尾空格,并按空格拆分为单词数组
+ const words = spaced.trim().split(/\s+/)
+ // 第四步:每个单词首字母大写,其余小写,然后拼接
+ return words
+ .map((word) => {
+ if (word.length === 0) return ''
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
+ })
+ .join('')
+}
+
+function resolveComponent(name: string, options: any) {
+ if (options.exclude && name.match(options.exclude)) return
+
+ if (!name.match(/^Bo[A-Z]/)) return
+
+ const partialName = pascalCase(name.slice(2))
+ return {
+ name,
+ from: `${'bolt-ui'}`,
+ sideEffects: getSideEffects(partialName)
+ }
+}
+
+function getSideEffects(dirName: string) {
+ const componentsFolder = 'bolt-ui/components'
+ return [`${componentsFolder}/${dirName}/style/index`, 'bolt-ui/theme-chalk/src/theme/index.scss']
+}
diff --git a/packages/bolt-ui/theme-chalk/src/button.scss b/packages/bolt-ui/theme-chalk/src/button.scss
new file mode 100644
index 0000000..e0a967f
--- /dev/null
+++ b/packages/bolt-ui/theme-chalk/src/button.scss
@@ -0,0 +1,50 @@
+@use 'core/_base' as *;
+@use 'sass:selector';
+
+@include setNamespace('button');
+
+#{b()} {
+ display: inline-block;
+ white-space: nowrap;
+ cursor: pointer;
+ background: #fff;
+ // border: 1px solid #dcdfe6;
+ // border-color: #dcdfe6;
+ border-radius: 8px;
+ border: none;
+ padding: 0 20px;
+ height: 40px;
+ line-height: 40px;
+ font-size: 14px;
+
+ {m('primary')} {
+ background: var(--color-primary);
+ color: var(--color-primary-content);
+ }
+ {m('secondary')} {
+ background: var(--color-secondary);
+ color: var(--color-secondary-content);
+ }
+ {m('tertiary')} {
+ background: #f8f9fa;
+ color: #212529;
+ }
+ {m('danger')} {
+ background: #dc3545;
+ color: #fff;
+ }
+ {m('warning')} {
+ background: #ffc107;
+ color: #212529;
+ }
+
+ &:hover {
+ opacity: 0.9;
+ }
+ &:active {
+ opacity: 0.8;
+ }
+ &:disabled {
+ opacity: 0.5;
+ }
+}
diff --git a/packages/bolt-ui/theme-chalk/src/card.scss b/packages/bolt-ui/theme-chalk/src/card.scss
new file mode 100644
index 0000000..85b30f0
--- /dev/null
+++ b/packages/bolt-ui/theme-chalk/src/card.scss
@@ -0,0 +1,11 @@
+@use 'core/_base' as *;
+@use 'sass:selector';
+
+@include setNamespace('card');
+
+#{b()} {
+ background-color: #fff;
+ border-radius: 4px;
+ padding: 10px;
+ box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+}
diff --git a/packages/bolt-ui/theme-chalk/src/core/_base.scss b/packages/bolt-ui/theme-chalk/src/core/_base.scss
new file mode 100644
index 0000000..98f7ccc
--- /dev/null
+++ b/packages/bolt-ui/theme-chalk/src/core/_base.scss
@@ -0,0 +1,44 @@
+$prefix: 'bo';
+$namespace: '';
+
+@mixin setNamespace($nc) {
+ $namespace: $nc !global;
+}
+
+@function b() {
+ @return '.#{$prefix}-#{$namespace}';
+}
+
+@function e($element) {
+ $block: b();
+ @return '#{$block}__#{$element}';
+}
+
+@function m($modifier) {
+ $block: b();
+ @return '#{$block}--#{$modifier}';
+}
+
+@function bm($blockSuffix, $modifier) {
+ $block: b();
+ @return '#{$block}-#{$blockSuffix}--#{$modifier}';
+}
+
+@function em($element, $modifier) {
+ $block: b();
+ @return '#{$block}__#{$element}--#{$modifier}';
+}
+
+@function be($blockSuffix, $element) {
+ $block: b();
+ @return '#{$block}-#{$blockSuffix}__#{$element}';
+}
+
+@function bem($blockSuffix, $element, $modifier) {
+ $block: b();
+ @return '#{$block}-#{$blockSuffix}__#{$element}--#{$modifier}';
+}
+
+@function is($state) {
+ @return '.is-#{$state}';
+}
diff --git a/packages/bolt-ui/theme-chalk/src/dropdown.scss b/packages/bolt-ui/theme-chalk/src/dropdown.scss
new file mode 100644
index 0000000..4f92481
--- /dev/null
+++ b/packages/bolt-ui/theme-chalk/src/dropdown.scss
@@ -0,0 +1,51 @@
+@use 'core/_base' as *;
+@use 'sass:selector';
+
+@include setNamespace('dropdown');
+
+#{b()} {
+ position: relative;
+
+ #{e('content')} {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 9999;
+ display: grid;
+ grid-template-rows: 1fr;
+ transition: grid-template-rows 0.1s ease-out;
+ overflow: hidden;
+ min-width: 80px;
+ {is('hidden')} {
+ grid-template-rows: 0fr;
+ }
+ #{e('wrapper')} {
+ min-height: 0;
+ #{e('list')} {
+ box-sizing: border-box;
+ background-color: #fff;
+ border: 1px solid #dcdfe6;
+ border-radius: 4px;
+
+ #{e('item')} {
+ padding: 10px;
+ font-size: 14px;
+ line-height: 1;
+ display: flex;
+ justify-content: center;
+ cursor: pointer;
+ &:hover {
+ background-color: #f5f5f5;
+ }
+ }
+
+ #{e('line')} {
+ width: 100%;
+ height: 1px;
+ background-color: #dcdfe6;
+ transform: scaleY(0.5);
+ }
+ }
+ }
+ }
+}
diff --git a/packages/bolt-ui/theme-chalk/src/empty.scss b/packages/bolt-ui/theme-chalk/src/empty.scss
new file mode 100644
index 0000000..81d0228
--- /dev/null
+++ b/packages/bolt-ui/theme-chalk/src/empty.scss
@@ -0,0 +1,38 @@
+@use 'core/_base' as *;
+@use 'sass:selector';
+
+@include setNamespace('empty');
+
+#{b()} {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 40px 20px;
+ text-align: center;
+ color: #909399;
+ font-size: 14px;
+
+ #{e('image')} {
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ img {
+ display: block;
+ user-select: none;
+ }
+ }
+
+ #{e('description')} {
+ margin-bottom: 20px;
+ color: #909399;
+ font-size: 14px;
+ line-height: 1.5;
+ }
+
+ #{e('footer')} {
+ margin-top: 10px;
+ }
+}
diff --git a/packages/bolt-ui/theme-chalk/src/float.scss b/packages/bolt-ui/theme-chalk/src/float.scss
new file mode 100644
index 0000000..17bb05f
--- /dev/null
+++ b/packages/bolt-ui/theme-chalk/src/float.scss
@@ -0,0 +1,59 @@
+@use 'core/_base' as *;
+
+@include setNamespace('float');
+
+#{b()} {
+ box-sizing: border-box;
+ margin: 0;
+ min-width: 0;
+ /* fixed + 块级 + 100% 会占满视口宽,getBoundingClientRect 近 innerWidth,clamp 会把 x 锁死 */
+ width: max-content;
+ max-width: calc(100vw - 1rem);
+ border-radius: var(--radius-box, 0.5rem);
+ box-shadow:
+ 0 4px 6px -1px rgb(0 0 0 / 0.08),
+ 0 2px 4px -2px rgb(0 0 0 / 0.06);
+ background-color: var(--color-base-100, #fafafa);
+ border: var(--border, 1px) solid var(--color-base-300, oklch(92% 0.04 240));
+ overflow: hidden;
+
+ {is('dragging')} {
+ cursor: grabbing;
+ user-select: none;
+ touch-action: none;
+ }
+
+ {is('no-drag')} {
+ #{e('handle')} {
+ cursor: default;
+ touch-action: auto;
+ }
+ }
+
+ #{e('handle')} {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 2rem;
+ cursor: grab;
+ touch-action: none;
+ flex-shrink: 0;
+
+ &:active {
+ cursor: grabbing;
+ }
+ }
+
+ #{e('handle-grip')} {
+ display: block;
+ width: 2rem;
+ height: 0.25rem;
+ border-radius: 999px;
+ opacity: 0.35;
+ }
+
+ #{e('body')} {
+ box-sizing: border-box;
+ min-height: 0;
+ }
+}
diff --git a/packages/bolt-ui/theme-chalk/src/input.scss b/packages/bolt-ui/theme-chalk/src/input.scss
new file mode 100644
index 0000000..7b0d232
--- /dev/null
+++ b/packages/bolt-ui/theme-chalk/src/input.scss
@@ -0,0 +1,94 @@
+@use 'core/_base' as *;
+@use 'sass:selector';
+
+@include setNamespace('input');
+
+#{b()} {
+ position: relative;
+ display: inline-flex;
+ width: 100%;
+ box-sizing: border-box;
+ vertical-align: middle;
+ {m('mini')} #{e('wrapper')} {
+ height: 32px;
+ line-height: 32px;
+ font-size: 12px;
+ }
+ {m('small')} #{e('wrapper')} {
+ height: 36px;
+ line-height: 36px;
+ font-size: 14px;
+ }
+ {m('large')} #{e('wrapper')} {
+ height: 44px;
+ line-height: 44px;
+ font-size: 16px;
+ }
+
+ #{e('wrapper')} {
+ width: 100%;
+ display: inline-flex;
+ font-size: 14px;
+ height: 40px;
+ line-height: 40px;
+ color: #606266;
+ transition:
+ border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1),
+ background-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+ background-color: #e9e9e9;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ padding: 0 15px;
+ box-sizing: border-box;
+ &:not(:disabled):has(#{e('inner')}:focus) {
+ border-color: #007bff;
+ box-shadow: 0 0 3px 2px rgba(0, 123, 255, 0.4);
+ background-color: #fff;
+ }
+ }
+
+ #{e('inner')} {
+ -webkit-appearance: none;
+ background-image: none;
+ background: none;
+ border: none;
+ box-sizing: border-box;
+ line-height: inherit;
+ color: inherit;
+ font-size: inherit;
+ outline: none;
+ width: 100%;
+ }
+
+ #{e('prefix')} {
+ margin-right: 8px;
+ font-size: 1.5em;
+ }
+ #{e('suffix')} {
+ margin-left: 8px;
+ font-size: 1.5em;
+ }
+
+ {is('disabled')} {
+ #{e('wrapper')} {
+ // color: #ccc;
+ // background-color: #e9e9e9;
+ opacity: 0.6;
+ }
+ #{e('inner')} {
+ cursor: not-allowed;
+ }
+ }
+ {is('readonly')} {
+ #{e('inner')} {
+ cursor: default;
+ }
+ position: relative;
+ transition: width 0.2s ease-in-out;
+ {is('hover-show')}:hover #{e('wrapper')} {
+ position: absolute;
+ transform: translateY(-50%);
+ width: 300px;
+ }
+ }
+}
diff --git a/packages/bolt-ui/theme-chalk/src/select.scss b/packages/bolt-ui/theme-chalk/src/select.scss
new file mode 100644
index 0000000..38274d0
--- /dev/null
+++ b/packages/bolt-ui/theme-chalk/src/select.scss
@@ -0,0 +1,144 @@
+@use 'core/_base' as *;
+@use 'sass:selector';
+
+@include setNamespace('select');
+
+#{b()} {
+ position: relative;
+ display: inline-flex;
+ width: 100%;
+ box-sizing: border-box;
+ vertical-align: middle;
+
+ {m('mini')} #{e('wrapper')} {
+ height: 32px;
+ line-height: 32px;
+ font-size: 12px;
+ }
+ {m('small')} #{e('wrapper')} {
+ height: 36px;
+ line-height: 36px;
+ font-size: 14px;
+ }
+ {m('large')} #{e('wrapper')} {
+ height: 44px;
+ line-height: 44px;
+ font-size: 16px;
+ }
+
+ #{e('wrapper')} {
+ width: 100%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ height: 40px;
+ line-height: 40px;
+ color: #606266;
+ transition:
+ border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1),
+ background-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+ background-color: #e9e9e9;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ padding: 0 15px;
+ box-sizing: border-box;
+ cursor: pointer;
+
+ &:hover:not(:disabled) {
+ background-color: #fff;
+ }
+
+ &:not(:disabled):has(#{e('inner')}:focus) {
+ border-color: #007bff;
+ box-shadow: 0 0 3px 2px rgba(0, 123, 255, 0.4);
+ background-color: #fff;
+ }
+ }
+
+ #{e('inner')} {
+ flex: 1;
+ box-sizing: border-box;
+ line-height: inherit;
+ color: inherit;
+ font-size: inherit;
+ outline: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ #{e('suffix')} {
+ margin-left: 8px;
+ display: flex;
+ align-items: center;
+ color: #909399;
+ transition: transform 0.2s;
+ flex-shrink: 0;
+ }
+
+ #{e('dropdown')} {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ right: 0;
+ z-index: 99;
+ display: grid;
+ grid-template-rows: 1fr;
+ transition: grid-template-rows 0.1s ease-out;
+ overflow: hidden;
+
+ {is('hidden')} {
+ grid-template-rows: 0fr;
+ }
+
+ #{e('dropdown-wrapper')} {
+ min-height: 0;
+ #{e('dropdown-list')} {
+ box-sizing: border-box;
+ background-color: #fff;
+ border: 1px solid #dcdfe6;
+ border-radius: 4px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ max-height: 274px;
+ overflow-y: auto;
+
+ #{e('dropdown-item')} {
+ padding: 10px 15px;
+ font-size: 14px;
+ line-height: 1.5;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ color: #606266;
+
+ &:hover {
+ background-color: #f5f7fa;
+ }
+
+ {is('selected')} {
+ color: #007bff;
+ background-color: #ecf5ff;
+ }
+ }
+
+ #{e('dropdown-line')} {
+ width: 100%;
+ height: 1px;
+ background-color: #dcdfe6;
+ transform: scaleY(0.5);
+ }
+ }
+ }
+ }
+
+ {is('disabled')} {
+ #{e('wrapper')} {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+ #{e('inner')} {
+ cursor: not-allowed;
+ }
+ }
+}
diff --git a/packages/bolt-ui/theme-chalk/src/theme/index.scss b/packages/bolt-ui/theme-chalk/src/theme/index.scss
new file mode 100644
index 0000000..ca5dcce
--- /dev/null
+++ b/packages/bolt-ui/theme-chalk/src/theme/index.scss
@@ -0,0 +1,38 @@
+:root {
+ --color-base-100: oklch(98% 0.02 240);
+ --color-base-200: oklch(95% 0.03 240);
+ --color-base-300: oklch(92% 0.04 240);
+ --color-base-content: oklch(20% 0.05 240);
+ --color-primary: oklch(55% 0.3 240);
+ --color-primary-content: oklch(98% 0.01 240);
+ --color-secondary: oklch(70% 0.25 200);
+ --color-secondary-content: oklch(98% 0.01 200);
+ --color-accent: oklch(65% 0.25 160);
+ --color-accent-content: oklch(98% 0.01 160);
+ --color-neutral: oklch(50% 0.05 240);
+ --color-neutral-content: oklch(98% 0.01 240);
+ --color-info: oklch(70% 0.2 220);
+ --color-info-content: oklch(98% 0.01 220);
+ --color-success: oklch(65% 0.25 140);
+ --color-success-content: oklch(98% 0.01 140);
+ --color-warning: oklch(80% 0.25 80);
+ --color-warning-content: oklch(20% 0.05 80);
+ --color-error: oklch(65% 0.3 30);
+ --color-error-content: oklch(98% 0.01 30);
+
+ /* border radius */
+ --radius-selector: 1rem;
+ --radius-field: 0.25rem;
+ --radius-box: 0.5rem;
+
+ /* base sizes */
+ --size-selector: 0.25rem;
+ --size-field: 0.25rem;
+
+ /* border size */
+ --border: 1px;
+
+ /* effects */
+ --depth: 1;
+ --noise: 0;
+}
diff --git a/packages/bolt-ui/tsconfig.json b/packages/bolt-ui/tsconfig.json
new file mode 100644
index 0000000..4b43cf6
--- /dev/null
+++ b/packages/bolt-ui/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "tsconfig/tsconfig.json",
+ "include": ["resolver.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/bolt-ui/utils/hooks/use-namespace/index.ts b/packages/bolt-ui/utils/hooks/use-namespace/index.ts
new file mode 100644
index 0000000..bf49490
--- /dev/null
+++ b/packages/bolt-ui/utils/hooks/use-namespace/index.ts
@@ -0,0 +1,127 @@
+import { computed, getCurrentInstance, inject, isRef, ref, unref } from 'vue'
+
+import type { InjectionKey, MaybeRef, Ref } from 'vue'
+
+export const defaultNamespace = 'bo'
+const statePrefix = 'is-'
+
+const _bem = (
+ namespace: string,
+ block: string,
+ blockSuffix: string,
+ element: string,
+ modifier: string
+) => {
+ let cls = `${namespace}-${block}`
+ if (blockSuffix) {
+ cls += `-${blockSuffix}`
+ }
+ if (element) {
+ cls += `__${element}`
+ }
+ if (modifier) {
+ cls += `--${modifier}`
+ }
+ return cls
+}
+
+export const namespaceContextKey: InjectionKey[> =
+ Symbol('namespaceContextKey')
+
+/**
+ * 获取上下文的命名空间
+ * @param namespaceOverrides 覆盖命令空间
+ * @returns 覆盖后的命令空间
+ */
+export const useGetDerivedNamespace = (namespaceOverrides?: Ref) => {
+ const derivedNamespace =
+ namespaceOverrides ||
+ (getCurrentInstance()
+ ? inject(namespaceContextKey, ref(defaultNamespace))
+ : ref(defaultNamespace))
+ const namespace = computed(() => {
+ return unref(derivedNamespace) || defaultNamespace
+ })
+ return namespace
+}
+
+export const useNamespace = (
+ block: MaybeRef,
+ namespaceOverrides?: Ref
+) => {
+ const namespace = useGetDerivedNamespace(namespaceOverrides)
+ const getBlock = () => (isRef(block) ? block.value : block)
+ const b = (blockSuffix = '') => _bem(namespace.value, getBlock(), blockSuffix, '', '') // bo-button-test
+ const e = (element?: string) =>
+ element ? _bem(namespace.value, getBlock(), '', element, '') : '' // bo-button__test
+ const m = (modifier?: string) =>
+ modifier ? _bem(namespace.value, getBlock(), '', '', modifier) : '' // bo-button--test
+ const be = (blockSuffix?: string, element?: string) =>
+ blockSuffix && element
+ ? _bem(namespace.value, getBlock(), blockSuffix, element, '') // bo-button-test__test
+ : ''
+ const em = (element?: string, modifier?: string) =>
+ element && modifier
+ ? _bem(namespace.value, getBlock(), '', element, modifier) // bo-button__test--test
+ : ''
+ const bm = (blockSuffix?: string, modifier?: string) =>
+ blockSuffix && modifier
+ ? _bem(namespace.value, getBlock(), blockSuffix, '', modifier) // bo-button-test--test
+ : ''
+ const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
+ blockSuffix && element && modifier
+ ? _bem(namespace.value, getBlock(), blockSuffix, element, modifier) // bo-button-test__test--test
+ : ''
+ const is: {
+ (name: string, state: boolean | undefined): string
+ (name: string): string
+ } = (name: string, ...args: [boolean | undefined] | []) => {
+ const state = args.length >= 1 ? args[0]! : true
+ return name && state ? `${statePrefix}${name}` : '' // is-test
+ }
+
+ // for css var
+ // --bo-xxx: value;
+ const cssVar = (object: Record) => {
+ const styles: Record = {}
+ for (const key in object) {
+ if (object[key]) {
+ styles[`--${namespace.value}-${key}`] = object[key] // --bo-test: test
+ }
+ }
+ return styles
+ }
+ // with block
+ const cssVarBlock = (object: Record) => {
+ const styles: Record = {}
+ for (const key in object) {
+ if (object[key]) {
+ styles[`--${namespace.value}-${getBlock()}-${key}`] = object[key] // --bo-button-test: test
+ }
+ }
+ return styles
+ }
+
+ const cssVarName = (name: string) => `--${namespace.value}-${name}` // --bo-test
+ const cssVarBlockName = (name: string) => `--${namespace.value}-${getBlock()}-${name}` // --bo-button-test
+
+ return {
+ namespace,
+ // bo-button-test
+ b,
+ e,
+ m,
+ be,
+ em,
+ bm,
+ bem,
+ is,
+ // css
+ cssVar,
+ cssVarName,
+ cssVarBlock,
+ cssVarBlockName
+ }
+}
+
+export type UseNamespaceReturn = ReturnType
diff --git a/packages/bolt-ui/utils/vue/install.ts b/packages/bolt-ui/utils/vue/install.ts
new file mode 100644
index 0000000..cfab85f
--- /dev/null
+++ b/packages/bolt-ui/utils/vue/install.ts
@@ -0,0 +1,26 @@
+import type { App } from 'vue'
+import type { SFCWithInstall } from './types'
+import { NOOP } from './types'
+
+export const withNoopInstall = (component: T) => {
+ ;(component as SFCWithInstall).install = NOOP
+
+ return component as SFCWithInstall
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const withInstall = >(main: T, extra?: E) => {
+ ;(main as SFCWithInstall).install = (app: App): void => {
+ for (const comp of [main, ...Object.values(extra ?? {})]) {
+ app.component(comp.name, comp)
+ }
+ }
+
+ if (extra) {
+ for (const [key, comp] of Object.entries(extra)) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ;(main as any)[key] = comp
+ }
+ }
+ return main as SFCWithInstall & E
+}
diff --git a/packages/bolt-ui/utils/vue/types.ts b/packages/bolt-ui/utils/vue/types.ts
new file mode 100644
index 0000000..93375fd
--- /dev/null
+++ b/packages/bolt-ui/utils/vue/types.ts
@@ -0,0 +1,12 @@
+import type { AppContext, Plugin } from 'vue'
+import type { Ref } from 'vue'
+
+export type SFCWithInstall = T & Plugin
+
+export type SFCInstallWithContext = SFCWithInstall & {
+ _context: AppContext | null
+}
+
+export const NOOP = () => {}
+
+export type MaybeRef = T | Ref
]