Merge pull request #104 from LOG1997/96-ui-optimization
96 UI optimization
This commit is contained in:
@@ -18,20 +18,6 @@ while read local_ref local_sha remote_ref remote_sha; do
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# 对于普通分支推送,检查最新 tag
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
|
||||
if [ $? -eq 0 ]; then
|
||||
# 只校验以 "v" 开头的 tag
|
||||
if [[ $LATEST_TAG == v* ]]; then
|
||||
echo "🏷️ 检查最新的 tag: $LATEST_TAG"
|
||||
node .husky/scripts/verifyTagVersion.js "$LATEST_TAG"
|
||||
else
|
||||
echo "ℹ️ 最新 tag ($LATEST_TAG) 不是版本 tag,跳过校验"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ 没有找到任何 tag,跳过版本校验"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
21
components.json
Normal file
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "new-york",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/style.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"composables": "@/composables"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
10
package.json
10
package.json
@@ -22,6 +22,8 @@
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"axios": "^1.7.8",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dexie": "^4.2.1",
|
||||
"github-markdown-css": "^5.8.0",
|
||||
@@ -31,7 +33,9 @@
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persist": "^1.0.0",
|
||||
"reka-ui": "^2.6.1",
|
||||
"sparticles": "^1.3.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "0.166.0",
|
||||
"three-css3d": "1.0.6",
|
||||
"uuid": "^13.0.0",
|
||||
@@ -84,6 +88,7 @@
|
||||
"sass-loader": "^16.0.3",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"terser": "^5.36.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^20.1.0",
|
||||
"unplugin-icons": "^22.3.0",
|
||||
@@ -95,5 +100,6 @@
|
||||
"vite-plugin-vue-devtools": "^8.0.2",
|
||||
"vitest": "^4.0.15",
|
||||
"vue-tsc": "^3.0.7"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.8.1+sha1.a4eff733d0c4ccc179997f0ef4986f6e92427781"
|
||||
}
|
||||
|
||||
182
pnpm-lock.yaml
generated
182
pnpm-lock.yaml
generated
@@ -20,6 +20,12 @@ importers:
|
||||
canvas-confetti:
|
||||
specifier: ^1.9.3
|
||||
version: 1.9.3
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
@@ -47,9 +53,15 @@ importers:
|
||||
pinia-plugin-persist:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(pinia@3.0.3(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3))
|
||||
reka-ui:
|
||||
specifier: ^2.6.1
|
||||
version: 2.6.1(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))
|
||||
sparticles:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
tailwind-merge:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
three:
|
||||
specifier: 0.166.0
|
||||
version: 0.166.0
|
||||
@@ -201,6 +213,9 @@ importers:
|
||||
terser:
|
||||
specifier: ^5.36.0
|
||||
version: 5.36.0
|
||||
tw-animate-css:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
typescript:
|
||||
specifier: ~5.9.3
|
||||
version: 5.9.3
|
||||
@@ -1265,6 +1280,18 @@ packages:
|
||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
|
||||
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@floating-ui/vue@1.1.9':
|
||||
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -1297,6 +1324,12 @@ packages:
|
||||
'@iconify/utils@3.0.2':
|
||||
resolution: {integrity: sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==}
|
||||
|
||||
'@internationalized/date@3.10.0':
|
||||
resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==}
|
||||
|
||||
'@internationalized/number@3.6.5':
|
||||
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
|
||||
|
||||
'@intlify/core-base@11.2.2':
|
||||
resolution: {integrity: sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -1610,6 +1643,9 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: '>=9.0.0'
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||
|
||||
'@tailwindcss/node@4.1.13':
|
||||
resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==}
|
||||
|
||||
@@ -1709,6 +1745,14 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^5.2.0 || ^6 || ^7
|
||||
|
||||
'@tanstack/virtual-core@3.13.13':
|
||||
resolution: {integrity: sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.13':
|
||||
resolution: {integrity: sha512-Cf2xIEE8nWAfsX0N5nihkPYMeQRT+pHt4NEkuP8rNCn6lVnLDiV8rC8IeIxbKmQC0yPnj4SIBLwXYVf86xxKTQ==}
|
||||
peerDependencies:
|
||||
vue: ^2.7.0 || ^3.0.0
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.9.5':
|
||||
resolution: {integrity: sha512-P5XDyCwq3VbWGAplyfP/bgmuUITVDcypxgZUyX45SM7HbU1Nrkk0cNK1HCOkuNBAVVbWen2GUNWah/AiupHHXg==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -2189,14 +2233,23 @@ packages:
|
||||
vue:
|
||||
optional: true
|
||||
|
||||
'@vueuse/core@12.8.2':
|
||||
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
|
||||
|
||||
'@vueuse/core@14.1.0':
|
||||
resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@vueuse/metadata@12.8.2':
|
||||
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
|
||||
|
||||
'@vueuse/metadata@14.1.0':
|
||||
resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
|
||||
|
||||
'@vueuse/shared@12.8.2':
|
||||
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
|
||||
|
||||
'@vueuse/shared@14.1.0':
|
||||
resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
|
||||
peerDependencies:
|
||||
@@ -2353,6 +2406,10 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
aria-query@5.1.3:
|
||||
resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
|
||||
|
||||
@@ -2584,6 +2641,9 @@ packages:
|
||||
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
clean-regexp@1.0.0:
|
||||
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2596,6 +2656,10 @@ packages:
|
||||
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
codepage@1.15.0:
|
||||
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -2829,6 +2893,9 @@ packages:
|
||||
resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -4669,6 +4736,11 @@ packages:
|
||||
resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
|
||||
hasBin: true
|
||||
|
||||
reka-ui@2.6.1:
|
||||
resolution: {integrity: sha512-XK7cJDQoNuGXfCNzBBo/81Yg/OgjPwvbabnlzXG2VsdSgNsT6iIkuPBPr+C0Shs+3bb0x0lbPvgQAhMSCKm5Ww==}
|
||||
peerDependencies:
|
||||
vue: '>= 3.2.0'
|
||||
|
||||
repeat-element@1.1.4:
|
||||
resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -5034,6 +5106,9 @@ packages:
|
||||
systemjs@6.15.1:
|
||||
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
|
||||
|
||||
tailwind-merge@3.4.0:
|
||||
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
||||
|
||||
tailwindcss@4.1.13:
|
||||
resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==}
|
||||
|
||||
@@ -5158,6 +5233,9 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tw-animate-css@1.4.0:
|
||||
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -5462,6 +5540,17 @@ packages:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@vue/composition-api': ^1.0.0-rc.1
|
||||
vue: ^3.0.0-0 || ^2.6.0
|
||||
peerDependenciesMeta:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
|
||||
vue-dompurify-html@5.2.0:
|
||||
resolution: {integrity: sha512-GX+BStkKEJ8wu/+hU1EK2nu/gzXWhb4XzBu6aowpsuU/3nkvXvZ2jx4nZ9M3jtS/Vu7J7MtFXjc7x3cWQ+zbVQ==}
|
||||
peerDependencies:
|
||||
@@ -6871,6 +6960,26 @@ snapshots:
|
||||
'@eslint/core': 0.17.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.3
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@floating-ui/vue@1.1.9(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.4
|
||||
'@floating-ui/utils': 0.2.10
|
||||
vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.6':
|
||||
@@ -6907,6 +7016,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@internationalized/date@3.10.0':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.17
|
||||
|
||||
'@internationalized/number@3.6.5':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.17
|
||||
|
||||
'@intlify/core-base@11.2.2':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 11.2.2
|
||||
@@ -7141,6 +7258,10 @@ snapshots:
|
||||
estraverse: 5.3.0
|
||||
picomatch: 4.0.3
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@tailwindcss/node@4.1.13':
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
@@ -7220,6 +7341,13 @@ snapshots:
|
||||
tailwindcss: 4.1.13
|
||||
vite: 7.1.6(@types/node@24.10.2)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.8.2)
|
||||
|
||||
'@tanstack/virtual-core@3.13.13': {}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.13(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.13.13
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.9.5':
|
||||
optional: true
|
||||
|
||||
@@ -7830,6 +7958,15 @@ snapshots:
|
||||
typescript: 5.9.3
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@vueuse/core@12.8.2(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
'@vueuse/metadata': 12.8.2
|
||||
'@vueuse/shared': 12.8.2(typescript@5.9.3)
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/core@14.1.0(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
@@ -7837,8 +7974,16 @@ snapshots:
|
||||
'@vueuse/shared': 14.1.0(vue@3.5.13(typescript@5.9.3))
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@vueuse/metadata@12.8.2': {}
|
||||
|
||||
'@vueuse/metadata@14.1.0': {}
|
||||
|
||||
'@vueuse/shared@12.8.2(typescript@5.9.3)':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/shared@14.1.0(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
@@ -8013,6 +8158,10 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
aria-query@5.1.3:
|
||||
dependencies:
|
||||
deep-equal: 2.2.3
|
||||
@@ -8265,6 +8414,10 @@ snapshots:
|
||||
isobject: 3.0.1
|
||||
static-extend: 0.1.2
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
clean-regexp@1.0.0:
|
||||
dependencies:
|
||||
escape-string-regexp: 1.0.5
|
||||
@@ -8277,6 +8430,8 @@ snapshots:
|
||||
|
||||
clone@2.1.2: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
codepage@1.15.0: {}
|
||||
|
||||
collection-visit@1.0.0:
|
||||
@@ -8492,6 +8647,8 @@ snapshots:
|
||||
is-descriptor: 1.0.3
|
||||
isobject: 3.0.1
|
||||
|
||||
defu@6.1.4: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
@@ -10534,6 +10691,23 @@ snapshots:
|
||||
dependencies:
|
||||
jsesc: 3.1.0
|
||||
|
||||
reka-ui@2.6.1(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.4
|
||||
'@floating-ui/vue': 1.1.9(vue@3.5.13(typescript@5.9.3))
|
||||
'@internationalized/date': 3.10.0
|
||||
'@internationalized/number': 3.6.5
|
||||
'@tanstack/vue-virtual': 3.13.13(vue@3.5.13(typescript@5.9.3))
|
||||
'@vueuse/core': 12.8.2(typescript@5.9.3)
|
||||
'@vueuse/shared': 12.8.2(typescript@5.9.3)
|
||||
aria-hidden: 1.2.6
|
||||
defu: 6.1.4
|
||||
ohash: 2.0.11
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- typescript
|
||||
|
||||
repeat-element@1.1.4: {}
|
||||
|
||||
repeat-string@1.6.1: {}
|
||||
@@ -10897,6 +11071,8 @@ snapshots:
|
||||
|
||||
systemjs@6.15.1: {}
|
||||
|
||||
tailwind-merge@3.4.0: {}
|
||||
|
||||
tailwindcss@4.1.13: {}
|
||||
|
||||
tailwindcss@4.1.17: {}
|
||||
@@ -11008,6 +11184,8 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tw-animate-css@1.4.0: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -11321,6 +11499,10 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.3)):
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
vue-dompurify-html@5.2.0(vue@3.5.13(typescript@5.9.3)):
|
||||
dependencies:
|
||||
dompurify: 3.2.1
|
||||
|
||||
12
src/App.vue
12
src/App.vue
@@ -1,19 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, provide, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { provide } from 'vue'
|
||||
import { loadingKey, loadingState } from '@/components/Loading'
|
||||
// import PlayMusic from '@/components/PlayMusic/index.vue'
|
||||
import useStore from '@/store'
|
||||
import { themeChange } from '@/utils'
|
||||
|
||||
provide(loadingKey, loadingState)
|
||||
const { t } = useI18n()
|
||||
const globalConfig = useStore().globalConfig
|
||||
const prizeConfig = useStore().prizeConfig
|
||||
const system = useStore().system
|
||||
const { getTheme: localTheme } = storeToRefs(globalConfig)
|
||||
const { getPrizeConfig: prizeList } = storeToRefs(prizeConfig)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
37
src/components.d.ts
vendored
37
src/components.d.ts
vendored
@@ -11,14 +11,51 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Button: typeof import('./components/ui/button/Button.vue')['default']
|
||||
Command: typeof import('./components/ui/command/Command.vue')['default']
|
||||
CommandDialog: typeof import('./components/ui/command/CommandDialog.vue')['default']
|
||||
CommandEmpty: typeof import('./components/ui/command/CommandEmpty.vue')['default']
|
||||
CommandGroup: typeof import('./components/ui/command/CommandGroup.vue')['default']
|
||||
CommandInput: typeof import('./components/ui/command/CommandInput.vue')['default']
|
||||
CommandItem: typeof import('./components/ui/command/CommandItem.vue')['default']
|
||||
CommandList: typeof import('./components/ui/command/CommandList.vue')['default']
|
||||
CommandSeparator: typeof import('./components/ui/command/CommandSeparator.vue')['default']
|
||||
CommandShortcut: typeof import('./components/ui/command/CommandShortcut.vue')['default']
|
||||
DaiysuiTable: typeof import('./components/DaiysuiTable/index.vue')['default']
|
||||
Dialog: typeof import('./components/Dialog/index.vue')['default']
|
||||
DialogClose: typeof import('./components/ui/dialog/DialogClose.vue')['default']
|
||||
DialogContent: typeof import('./components/ui/dialog/DialogContent.vue')['default']
|
||||
DialogDescription: typeof import('./components/ui/dialog/DialogDescription.vue')['default']
|
||||
DialogFooter: typeof import('./components/ui/dialog/DialogFooter.vue')['default']
|
||||
DialogHeader: typeof import('./components/ui/dialog/DialogHeader.vue')['default']
|
||||
DialogOverlay: typeof import('./components/ui/dialog/DialogOverlay.vue')['default']
|
||||
DialogScrollContent: typeof import('./components/ui/dialog/DialogScrollContent.vue')['default']
|
||||
DialogTitle: typeof import('./components/ui/dialog/DialogTitle.vue')['default']
|
||||
DialogTrigger: typeof import('./components/ui/dialog/DialogTrigger.vue')['default']
|
||||
DropdownMenu: typeof import('./components/ui/dropdown-menu/DropdownMenu.vue')['default']
|
||||
DropdownMenuCheckboxItem: typeof import('./components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue')['default']
|
||||
DropdownMenuContent: typeof import('./components/ui/dropdown-menu/DropdownMenuContent.vue')['default']
|
||||
DropdownMenuGroup: typeof import('./components/ui/dropdown-menu/DropdownMenuGroup.vue')['default']
|
||||
DropdownMenuItem: typeof import('./components/ui/dropdown-menu/DropdownMenuItem.vue')['default']
|
||||
DropdownMenuLabel: typeof import('./components/ui/dropdown-menu/DropdownMenuLabel.vue')['default']
|
||||
DropdownMenuRadioGroup: typeof import('./components/ui/dropdown-menu/DropdownMenuRadioGroup.vue')['default']
|
||||
DropdownMenuRadioItem: typeof import('./components/ui/dropdown-menu/DropdownMenuRadioItem.vue')['default']
|
||||
DropdownMenuSeparator: typeof import('./components/ui/dropdown-menu/DropdownMenuSeparator.vue')['default']
|
||||
DropdownMenuShortcut: typeof import('./components/ui/dropdown-menu/DropdownMenuShortcut.vue')['default']
|
||||
DropdownMenuSub: typeof import('./components/ui/dropdown-menu/DropdownMenuSub.vue')['default']
|
||||
DropdownMenuSubContent: typeof import('./components/ui/dropdown-menu/DropdownMenuSubContent.vue')['default']
|
||||
DropdownMenuSubTrigger: typeof import('./components/ui/dropdown-menu/DropdownMenuSubTrigger.vue')['default']
|
||||
DropdownMenuTrigger: typeof import('./components/ui/dropdown-menu/DropdownMenuTrigger.vue')['default']
|
||||
EditSeparateDialog: typeof import('./components/NumberSeparate/EditSeparateDialog.vue')['default']
|
||||
FileUpload: typeof import('./components/FileUpload/index.vue')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
ImageSync: typeof import('./components/ImageSync/index.vue')['default']
|
||||
Loading: typeof import('./components/Loading/index.vue')['default']
|
||||
PageHeader: typeof import('./components/PageHeader/index.vue')['default']
|
||||
Popover: typeof import('./components/ui/popover/Popover.vue')['default']
|
||||
PopoverAnchor: typeof import('./components/ui/popover/PopoverAnchor.vue')['default']
|
||||
PopoverContent: typeof import('./components/ui/popover/PopoverContent.vue')['default']
|
||||
PopoverTrigger: typeof import('./components/ui/popover/PopoverTrigger.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SvgIcon: typeof import('./components/SvgIcon/index.vue')['default']
|
||||
|
||||
29
src/components/ui/button/Button.vue
Normal file
29
src/components/ui/button/Button.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { ButtonVariants } from "."
|
||||
import { Primitive } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "."
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants["variant"]
|
||||
size?: ButtonVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: "button",
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="button"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
38
src/components/ui/button/index.ts
Normal file
38
src/components/ui/button/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export { default as Button } from "./Button.vue"
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
"icon": "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||
87
src/components/ui/command/Command.vue
Normal file
87
src/components/ui/command/Command.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxRootEmits, ListboxRootProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ListboxRoot, useFilter, useForwardPropsEmits } from "reka-ui"
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { provideCommandContext } from "."
|
||||
|
||||
const props = withDefaults(defineProps<ListboxRootProps & { class?: HTMLAttributes["class"] }>(), {
|
||||
modelValue: "",
|
||||
})
|
||||
|
||||
const emits = defineEmits<ListboxRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
|
||||
const allItems = ref<Map<string, string>>(new Map())
|
||||
const allGroups = ref<Map<string, Set<string>>>(new Map())
|
||||
|
||||
const { contains } = useFilter({ sensitivity: "base" })
|
||||
const filterState = reactive({
|
||||
search: "",
|
||||
filtered: {
|
||||
/** The count of all visible items. */
|
||||
count: 0,
|
||||
/** Map from visible item id to its search score. */
|
||||
items: new Map() as Map<string, number>,
|
||||
/** Set of groups with at least one visible item. */
|
||||
groups: new Set() as Set<string>,
|
||||
},
|
||||
})
|
||||
|
||||
function filterItems() {
|
||||
if (!filterState.search) {
|
||||
filterState.filtered.count = allItems.value.size
|
||||
// Do nothing, each item will know to show itself because search is empty
|
||||
return
|
||||
}
|
||||
|
||||
// Reset the groups
|
||||
filterState.filtered.groups = new Set()
|
||||
let itemCount = 0
|
||||
|
||||
// Check which items should be included
|
||||
for (const [id, value] of allItems.value) {
|
||||
const score = contains(value, filterState.search)
|
||||
filterState.filtered.items.set(id, score ? 1 : 0)
|
||||
if (score)
|
||||
itemCount++
|
||||
}
|
||||
|
||||
// Check which groups have at least 1 item shown
|
||||
for (const [groupId, group] of allGroups.value) {
|
||||
for (const itemId of group) {
|
||||
if (filterState.filtered.items.get(itemId)! > 0) {
|
||||
filterState.filtered.groups.add(groupId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterState.filtered.count = itemCount
|
||||
}
|
||||
|
||||
watch(() => filterState.search, () => {
|
||||
filterItems()
|
||||
})
|
||||
|
||||
provideCommandContext({
|
||||
allItems,
|
||||
allGroups,
|
||||
filterState,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListboxRoot
|
||||
data-slot="command"
|
||||
v-bind="forwarded"
|
||||
:class="cn('bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ListboxRoot>
|
||||
</template>
|
||||
31
src/components/ui/command/CommandDialog.vue
Normal file
31
src/components/ui/command/CommandDialog.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
||||
import { useForwardPropsEmits } from "reka-ui"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import Command from "./Command.vue"
|
||||
|
||||
const props = withDefaults(defineProps<DialogRootProps & {
|
||||
title?: string
|
||||
description?: string
|
||||
}>(), {
|
||||
title: "Command Palette",
|
||||
description: "Search for a command to run...",
|
||||
})
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-slot="slotProps" v-bind="forwarded">
|
||||
<DialogContent class="overflow-hidden p-0 ">
|
||||
<DialogHeader class="sr-only">
|
||||
<DialogTitle>{{ title }}</DialogTitle>
|
||||
<DialogDescription>{{ description }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Command>
|
||||
<slot v-bind="slotProps" />
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
27
src/components/ui/command/CommandEmpty.vue
Normal file
27
src/components/ui/command/CommandEmpty.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Primitive } from "reka-ui"
|
||||
import { computed } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCommand } from "."
|
||||
|
||||
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const { filterState } = useCommand()
|
||||
const isRender = computed(() => !!filterState.search && filterState.filtered.count === 0,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
v-if="isRender"
|
||||
data-slot="command-empty"
|
||||
v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
45
src/components/ui/command/CommandGroup.vue
Normal file
45
src/components/ui/command/CommandGroup.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxGroupProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ListboxGroup, ListboxGroupLabel, useId } from "reka-ui"
|
||||
import { computed, onMounted, onUnmounted } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { provideCommandGroupContext, useCommand } from "."
|
||||
|
||||
const props = defineProps<ListboxGroupProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
heading?: string
|
||||
}>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const { allGroups, filterState } = useCommand()
|
||||
const id = useId()
|
||||
|
||||
const isRender = computed(() => !filterState.search ? true : filterState.filtered.groups.has(id))
|
||||
|
||||
provideCommandGroupContext({ id })
|
||||
onMounted(() => {
|
||||
if (!allGroups.value.has(id))
|
||||
allGroups.value.set(id, new Set())
|
||||
})
|
||||
onUnmounted(() => {
|
||||
allGroups.value.delete(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListboxGroup
|
||||
v-bind="delegatedProps"
|
||||
:id="id"
|
||||
data-slot="command-group"
|
||||
:class="cn('text-foreground overflow-hidden p-1', props.class)"
|
||||
:hidden="isRender ? undefined : true"
|
||||
>
|
||||
<ListboxGroupLabel v-if="heading" data-slot="command-group-heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{{ heading }}
|
||||
</ListboxGroupLabel>
|
||||
<slot />
|
||||
</ListboxGroup>
|
||||
</template>
|
||||
39
src/components/ui/command/CommandInput.vue
Normal file
39
src/components/ui/command/CommandInput.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxFilterProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Search } from "lucide-vue-next"
|
||||
import { ListboxFilter, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCommand } from "."
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<ListboxFilterProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
|
||||
const { filterState } = useCommand()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
class="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<Search class="size-4 shrink-0 opacity-50" />
|
||||
<ListboxFilter
|
||||
v-bind="{ ...forwardedProps, ...$attrs }"
|
||||
v-model="filterState.search"
|
||||
data-slot="command-input"
|
||||
auto-focus
|
||||
:class="cn('placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50', props.class)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
76
src/components/ui/command/CommandItem.vue
Normal file
76
src/components/ui/command/CommandItem.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxItemEmits, ListboxItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit, useCurrentElement } from "@vueuse/core"
|
||||
import { ListboxItem, useForwardPropsEmits, useId } from "reka-ui"
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCommand, useCommandGroup } from "."
|
||||
|
||||
const props = defineProps<ListboxItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<ListboxItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
|
||||
const id = useId()
|
||||
const { filterState, allItems, allGroups } = useCommand()
|
||||
const groupContext = useCommandGroup()
|
||||
|
||||
const isRender = computed(() => {
|
||||
if (!filterState.search) {
|
||||
return true
|
||||
}
|
||||
else {
|
||||
const filteredCurrentItem = filterState.filtered.items.get(id)
|
||||
// If the filtered items is undefined means not in the all times map yet
|
||||
// Do the first render to add into the map
|
||||
if (filteredCurrentItem === undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check with filter
|
||||
return filteredCurrentItem > 0
|
||||
}
|
||||
})
|
||||
|
||||
const itemRef = ref()
|
||||
const currentElement = useCurrentElement(itemRef)
|
||||
onMounted(() => {
|
||||
if (!(currentElement.value instanceof HTMLElement))
|
||||
return
|
||||
|
||||
// textValue to perform filter
|
||||
allItems.value.set(id, currentElement.value.textContent ?? (props.value?.toString() ?? ""))
|
||||
|
||||
const groupId = groupContext?.id
|
||||
if (groupId) {
|
||||
if (!allGroups.value.has(groupId)) {
|
||||
allGroups.value.set(groupId, new Set([id]))
|
||||
}
|
||||
else {
|
||||
allGroups.value.get(groupId)?.add(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
allItems.value.delete(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListboxItem
|
||||
v-if="isRender"
|
||||
v-bind="forwarded"
|
||||
:id="id"
|
||||
ref="itemRef"
|
||||
data-slot="command-item"
|
||||
:class="cn('data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
|
||||
@select="() => {
|
||||
filterState.search = ''
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</ListboxItem>
|
||||
</template>
|
||||
25
src/components/ui/command/CommandList.vue
Normal file
25
src/components/ui/command/CommandList.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ListboxContent, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<ListboxContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListboxContent
|
||||
data-slot="command-list"
|
||||
v-bind="forwarded"
|
||||
:class="cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', props.class)"
|
||||
>
|
||||
<div role="presentation">
|
||||
<slot />
|
||||
</div>
|
||||
</ListboxContent>
|
||||
</template>
|
||||
21
src/components/ui/command/CommandSeparator.vue
Normal file
21
src/components/ui/command/CommandSeparator.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { SeparatorProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Separator } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<SeparatorProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Separator
|
||||
data-slot="command-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border -mx-1 h-px', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Separator>
|
||||
</template>
|
||||
17
src/components/ui/command/CommandShortcut.vue
Normal file
17
src/components/ui/command/CommandShortcut.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
25
src/components/ui/command/index.ts
Normal file
25
src/components/ui/command/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Ref } from "vue"
|
||||
import { createContext } from "reka-ui"
|
||||
|
||||
export { default as Command } from "./Command.vue"
|
||||
export { default as CommandDialog } from "./CommandDialog.vue"
|
||||
export { default as CommandEmpty } from "./CommandEmpty.vue"
|
||||
export { default as CommandGroup } from "./CommandGroup.vue"
|
||||
export { default as CommandInput } from "./CommandInput.vue"
|
||||
export { default as CommandItem } from "./CommandItem.vue"
|
||||
export { default as CommandList } from "./CommandList.vue"
|
||||
export { default as CommandSeparator } from "./CommandSeparator.vue"
|
||||
export { default as CommandShortcut } from "./CommandShortcut.vue"
|
||||
|
||||
export const [useCommand, provideCommandContext] = createContext<{
|
||||
allItems: Ref<Map<string, string>>
|
||||
allGroups: Ref<Map<string, Set<string>>>
|
||||
filterState: {
|
||||
search: string
|
||||
filtered: { count: number, items: Map<string, number>, groups: Set<string> }
|
||||
}
|
||||
}>("Command")
|
||||
|
||||
export const [useCommandGroup, provideCommandGroupContext] = createContext<{
|
||||
id?: string
|
||||
}>("CommandGroup")
|
||||
19
src/components/ui/dialog/Dialog.vue
Normal file
19
src/components/ui/dialog/Dialog.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
||||
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="dialog"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</DialogRoot>
|
||||
</template>
|
||||
15
src/components/ui/dialog/DialogClose.vue
Normal file
15
src/components/ui/dialog/DialogClose.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogCloseProps } from "reka-ui"
|
||||
import { DialogClose } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose
|
||||
data-slot="dialog-close"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
53
src/components/ui/dialog/DialogContent.vue
Normal file
53
src/components/ui/dialog/DialogContent.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { X } from "lucide-vue-next"
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import DialogOverlay from "./DialogOverlay.vue"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<DialogContentProps & { class?: HTMLAttributes["class"], showCloseButton?: boolean }>(), {
|
||||
showCloseButton: true,
|
||||
})
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogContent
|
||||
data-slot="dialog-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
v-if="showCloseButton"
|
||||
data-slot="dialog-close"
|
||||
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<X />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
23
src/components/ui/dialog/DialogDescription.vue
Normal file
23
src/components/ui/dialog/DialogDescription.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogDescriptionProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DialogDescription, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
data-slot="dialog-description"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
||||
15
src/components/ui/dialog/DialogFooter.vue
Normal file
15
src/components/ui/dialog/DialogFooter.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
:class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
src/components/ui/dialog/DialogHeader.vue
Normal file
17
src/components/ui/dialog/DialogHeader.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
21
src/components/ui/dialog/DialogOverlay.vue
Normal file
21
src/components/ui/dialog/DialogOverlay.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogOverlayProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DialogOverlay } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogOverlay
|
||||
data-slot="dialog-overlay"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogOverlay>
|
||||
</template>
|
||||
59
src/components/ui/dialog/DialogScrollContent.vue
Normal file
59
src/components/ui/dialog/DialogScrollContent.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { X } from "lucide-vue-next"
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
>
|
||||
<DialogContent
|
||||
:class="
|
||||
cn(
|
||||
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
@pointer-down-outside="(event) => {
|
||||
const originalEvent = event.detail.originalEvent;
|
||||
const target = originalEvent.target as HTMLElement;
|
||||
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
23
src/components/ui/dialog/DialogTitle.vue
Normal file
23
src/components/ui/dialog/DialogTitle.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTitleProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DialogTitle, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
data-slot="dialog-title"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-lg leading-none font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
||||
15
src/components/ui/dialog/DialogTrigger.vue
Normal file
15
src/components/ui/dialog/DialogTrigger.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTriggerProps } from "reka-ui"
|
||||
import { DialogTrigger } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger
|
||||
data-slot="dialog-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
||||
10
src/components/ui/dialog/index.ts
Normal file
10
src/components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { default as Dialog } from "./Dialog.vue"
|
||||
export { default as DialogClose } from "./DialogClose.vue"
|
||||
export { default as DialogContent } from "./DialogContent.vue"
|
||||
export { default as DialogDescription } from "./DialogDescription.vue"
|
||||
export { default as DialogFooter } from "./DialogFooter.vue"
|
||||
export { default as DialogHeader } from "./DialogHeader.vue"
|
||||
export { default as DialogOverlay } from "./DialogOverlay.vue"
|
||||
export { default as DialogScrollContent } from "./DialogScrollContent.vue"
|
||||
export { default as DialogTitle } from "./DialogTitle.vue"
|
||||
export { default as DialogTrigger } from "./DialogTrigger.vue"
|
||||
19
src/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
19
src/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui"
|
||||
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>()
|
||||
const emits = defineEmits<DropdownMenuRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="dropdown-menu"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
39
src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
39
src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Check } from "lucide-vue-next"
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuCheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
v-bind="forwarded"
|
||||
:class=" cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<Check class="size-4" />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuCheckboxItem>
|
||||
</template>
|
||||
39
src/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
39
src/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
sideOffset: 4,
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
data-slot="dropdown-menu-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</template>
|
||||
15
src/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
15
src/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuGroupProps } from "reka-ui"
|
||||
import { DropdownMenuGroup } from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuGroup
|
||||
data-slot="dropdown-menu-group"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuGroup>
|
||||
</template>
|
||||
31
src/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
31
src/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DropdownMenuItem, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = withDefaults(defineProps<DropdownMenuItemProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}>(), {
|
||||
variant: "default",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "inset", "variant", "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuItem
|
||||
data-slot="dropdown-menu-item"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
:data-variant="variant"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
23
src/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
23
src/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuLabelProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DropdownMenuLabel, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "inset")
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuLabel>
|
||||
</template>
|
||||
21
src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
21
src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui"
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>()
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuRadioGroup>
|
||||
</template>
|
||||
40
src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
40
src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Circle } from "lucide-vue-next"
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<Circle class="size-2 fill-current" />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuRadioItem>
|
||||
</template>
|
||||
23
src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
23
src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSeparatorProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuSeparator,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSeparator
|
||||
data-slot="dropdown-menu-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
|
||||
/>
|
||||
</template>
|
||||
17
src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
17
src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
18
src/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
18
src/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui"
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>()
|
||||
const emits = defineEmits<DropdownMenuSubEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</DropdownMenuSub>
|
||||
</template>
|
||||
27
src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
27
src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
v-bind="forwarded"
|
||||
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuSubContent>
|
||||
</template>
|
||||
30
src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
30
src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubTriggerProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ChevronRight } from "lucide-vue-next"
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
useForwardProps,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "inset")
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto size-4" />
|
||||
</DropdownMenuSubTrigger>
|
||||
</template>
|
||||
17
src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
17
src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuTriggerProps } from "reka-ui"
|
||||
import { DropdownMenuTrigger, useForwardProps } from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>()
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuTrigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
</template>
|
||||
16
src/components/ui/dropdown-menu/index.ts
Normal file
16
src/components/ui/dropdown-menu/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export { default as DropdownMenu } from "./DropdownMenu.vue"
|
||||
|
||||
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
|
||||
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
|
||||
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
|
||||
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"
|
||||
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"
|
||||
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"
|
||||
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"
|
||||
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"
|
||||
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"
|
||||
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"
|
||||
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"
|
||||
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"
|
||||
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
|
||||
export { DropdownMenuPortal } from "reka-ui"
|
||||
19
src/components/ui/popover/Popover.vue
Normal file
19
src/components/ui/popover/Popover.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverRootEmits, PopoverRootProps } from "reka-ui"
|
||||
import { PopoverRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<PopoverRootProps>()
|
||||
const emits = defineEmits<PopoverRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="popover"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PopoverRoot>
|
||||
</template>
|
||||
15
src/components/ui/popover/PopoverAnchor.vue
Normal file
15
src/components/ui/popover/PopoverAnchor.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverAnchorProps } from "reka-ui"
|
||||
import { PopoverAnchor } from "reka-ui"
|
||||
|
||||
const props = defineProps<PopoverAnchorProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverAnchor
|
||||
data-slot="popover-anchor"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</PopoverAnchor>
|
||||
</template>
|
||||
45
src/components/ui/popover/PopoverContent.vue
Normal file
45
src/components/ui/popover/PopoverContent.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverContentEmits, PopoverContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PopoverContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
align: "center",
|
||||
sideOffset: 4,
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
data-slot="popover-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</template>
|
||||
15
src/components/ui/popover/PopoverTrigger.vue
Normal file
15
src/components/ui/popover/PopoverTrigger.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverTriggerProps } from "reka-ui"
|
||||
import { PopoverTrigger } from "reka-ui"
|
||||
|
||||
const props = defineProps<PopoverTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverTrigger
|
||||
data-slot="popover-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</PopoverTrigger>
|
||||
</template>
|
||||
4
src/components/ui/popover/index.ts
Normal file
4
src/components/ui/popover/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Popover } from "./Popover.vue"
|
||||
export { default as PopoverAnchor } from "./PopoverAnchor.vue"
|
||||
export { default as PopoverContent } from "./PopoverContent.vue"
|
||||
export { default as PopoverTrigger } from "./PopoverTrigger.vue"
|
||||
47
src/hooks/useLocalFonts.ts
Normal file
47
src/hooks/useLocalFonts.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
export function useLocalFonts() {
|
||||
const fonts = ref<Map<string, { name: string, value: string }[]>>(new Map())
|
||||
const disabled = ref(false)
|
||||
const formatFonts = (list: FontData[]): Map<string, { name: string, value: string }[]> => {
|
||||
const res: Map<string, { name: string, value: string }[]> = new Map()
|
||||
for (const item of list) {
|
||||
if (!res.has(item.family)) {
|
||||
res.set(item.family, [])
|
||||
}
|
||||
|
||||
const fontArray = res.get(item.family)
|
||||
if (!Array.isArray(fontArray)) {
|
||||
continue
|
||||
}
|
||||
if (item.family === item.fullName) {
|
||||
fontArray.push({ name: item.style, value: item.fullName })
|
||||
continue
|
||||
}
|
||||
const name = item.fullName.replace(item.family, '').trim()
|
||||
const value = item.fullName
|
||||
fontArray.push({ name, value })
|
||||
}
|
||||
return res
|
||||
}
|
||||
const getFonts = async () => {
|
||||
if (!window.queryLocalFonts) {
|
||||
return
|
||||
}
|
||||
const list = await window.queryLocalFonts()
|
||||
const res = formatFonts(list)
|
||||
fonts.value = res
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!window.queryLocalFonts) {
|
||||
disabled.value = true
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
disabled,
|
||||
fonts,
|
||||
getFonts,
|
||||
}
|
||||
}
|
||||
7
src/lib/utils.ts
Normal file
7
src/lib/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ClassValue } from "clsx"
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
29
src/main.ts
29
src/main.ts
@@ -16,6 +16,35 @@ import './style/style.scss'
|
||||
// 全局svg组件
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
// 在应用初始化时尽早设置主题和字体,避免页面加载时的闪烁
|
||||
(function initializeThemeAndFont() {
|
||||
try {
|
||||
// 从localStorage获取全局配置
|
||||
const globalConfigStr = localStorage.getItem('globalConfig')
|
||||
|
||||
if (globalConfigStr) {
|
||||
const storageData = JSON.parse(globalConfigStr)
|
||||
// 根据persist策略,数据存储在globalConfig属性下
|
||||
const globalConfig = storageData.globalConfig || storageData
|
||||
|
||||
// 设置主题
|
||||
if (globalConfig.theme?.name) {
|
||||
const html = document.documentElement
|
||||
html.setAttribute('data-theme', globalConfig.theme.name)
|
||||
}
|
||||
|
||||
// 设置字体
|
||||
if (globalConfig.theme?.font) {
|
||||
// 更新CSS变量
|
||||
document.documentElement.style.setProperty('--app-font-family', `"${globalConfig.theme.font}", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Failed to set initial theme and font:', e)
|
||||
}
|
||||
})()
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersist)
|
||||
|
||||
@@ -24,6 +24,7 @@ export const useGlobalConfig = defineStore('global', {
|
||||
patternColor: '#1b66c9',
|
||||
patternList: defaultPatternList as number[],
|
||||
background: {}, // 背景颜色或图片
|
||||
font: '',
|
||||
},
|
||||
musicList: defaultMusicList as IMusic[],
|
||||
imageList: defaultImageList as IImage[],
|
||||
@@ -106,6 +107,10 @@ export const useGlobalConfig = defineStore('global', {
|
||||
getBackground(state) {
|
||||
return state.globalConfig.theme.background
|
||||
},
|
||||
// 获取字体
|
||||
getFont(state) {
|
||||
return state.globalConfig.theme.font
|
||||
},
|
||||
// 获取是否显示头像
|
||||
getIsShowAvatar(state) {
|
||||
return state.globalConfig.isShowAvatar
|
||||
@@ -232,6 +237,10 @@ export const useGlobalConfig = defineStore('global', {
|
||||
setBackground(background: any) {
|
||||
this.globalConfig.theme.background = background
|
||||
},
|
||||
// 设置字体
|
||||
setFont(font: any) {
|
||||
this.globalConfig.theme.font = font
|
||||
},
|
||||
// 设置是否显示头像
|
||||
setIsShowAvatar(isShowAvatar: boolean) {
|
||||
this.globalConfig.isShowAvatar = isShowAvatar
|
||||
@@ -256,6 +265,7 @@ export const useGlobalConfig = defineStore('global', {
|
||||
patternColor: '#1b66c9',
|
||||
patternList: defaultPatternList as number[],
|
||||
background: {}, // 背景颜色或图片
|
||||
font: '',
|
||||
},
|
||||
musicList: defaultMusicList as IMusic[],
|
||||
imageList: defaultImageList as IImage[],
|
||||
|
||||
@@ -20,7 +20,6 @@ export const usePersonConfig = defineStore('person', () => {
|
||||
personConfig.value.allPersonList = data
|
||||
})
|
||||
personDb.getAllData('alreadyPersonList').then((data) => {
|
||||
console.log(data)
|
||||
personConfig.value.alreadyPersonList = data
|
||||
})
|
||||
|
||||
|
||||
122
src/style.css
122
src/style.css
@@ -1,4 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes: all;
|
||||
@@ -8,7 +11,6 @@
|
||||
max-width: calc(var(--container-xs) - 90px);
|
||||
}
|
||||
|
||||
|
||||
/* @plugin "@tailwindcss/typography" */
|
||||
|
||||
body,
|
||||
@@ -18,6 +20,7 @@ html {
|
||||
overflow-y: overlay;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
font-family: var(--app-font-family);
|
||||
}
|
||||
|
||||
ul {
|
||||
@@ -62,4 +65,121 @@ ul {
|
||||
/* Opera(Old) */
|
||||
box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
|
||||
/* IE9+, News */
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
3
src/utils/format/tree.ts
Normal file
3
src/utils/format/tree.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
/**
|
||||
* @description 用于将平铺的数组组合到树形数组中
|
||||
*/
|
||||
118
src/views/Config/Global/FaceConfig/components/SelectFont.vue
Normal file
118
src/views/Config/Global/FaceConfig/components/SelectFont.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup lang='ts'>
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import { ChevronRight, ChevronsUpDownIcon } from 'lucide-vue-next'
|
||||
import { PopoverArrow } from 'reka-ui'
|
||||
import { ref } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { useLocalFonts } from '@/hooks/useLocalFonts'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const selectedFont = defineModel('selectedFont', {
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
|
||||
const { getFonts, disabled, fonts } = useLocalFonts()
|
||||
const open = ref(false)
|
||||
const activeKey = ref('')
|
||||
const debouncedActiveKey = refDebounced(activeKey, 20)
|
||||
|
||||
function selectFont(selectedValue: any) {
|
||||
open.value = false
|
||||
activeKey.value = ''
|
||||
selectedFont.value = selectedValue
|
||||
}
|
||||
|
||||
function handelActiveKey(val: string) {
|
||||
activeKey.value = val
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
activeKey.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex justify-center items-center max-w-xs">
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child :disabled="disabled">
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
:aria-expanded="open"
|
||||
class="w-full justify-between truncate bg-transparent hover:bg-transparent hover:text-inherit"
|
||||
@click="getFonts"
|
||||
>
|
||||
<span class="w-7/8 text-left truncate" :style="{ fontFamily: `${selectedFont}` }">
|
||||
{{ selectedFont || "选择字体..." }}
|
||||
</span>
|
||||
<ChevronsUpDownIcon class="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput class="h-9" placeholder="Search framework..." />
|
||||
<CommandList @scroll="handleScroll">
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="[key, value] in fonts"
|
||||
:key="key"
|
||||
:value="key"
|
||||
class="w-full hover:bg-gray-200/50"
|
||||
@select="selectFont(key)"
|
||||
>
|
||||
<Popover :open="debouncedActiveKey === key" class="w-full">
|
||||
<PopoverTrigger class="w-full">
|
||||
<div :style="{ fontFamily: `${key}` }" class="w-full flex justify-between items-center" @mouseleave="handelActiveKey('')" @mouseenter="handelActiveKey(key)">
|
||||
{{ key }}
|
||||
<ChevronRight
|
||||
:class="cn(
|
||||
'ml-auto',
|
||||
)"
|
||||
/>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-2" side="right" @mouseleave="handelActiveKey('')" @mouseenter="handelActiveKey(key)">
|
||||
<PopoverArrow />
|
||||
<Command>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="child in value"
|
||||
:key="child.value"
|
||||
:value="child.value"
|
||||
class="w-full hover:bg-gray-200/50"
|
||||
:style="{ fontFamily: `${key}` }"
|
||||
@select="selectFont(child.value)"
|
||||
>
|
||||
{{ child.name }}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
</style>
|
||||
@@ -11,6 +11,7 @@ import useStore from '@/store'
|
||||
import { themeChange } from '@/utils'
|
||||
import { clearAllDbStore } from '@/utils/localforage'
|
||||
import PatternSetting from './components/PatternSetting.vue'
|
||||
import SelectFont from './components/SelectFont.vue'
|
||||
import 'vue3-colorpicker/style.css'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -18,7 +19,7 @@ const { t } = useI18n()
|
||||
const globalConfig = useStore().globalConfig
|
||||
const personConfig = useStore().personConfig
|
||||
const prizeConfig = useStore().prizeConfig
|
||||
const { getTopTitle: topTitle, getTheme: localTheme, getPatterColor: patternColor, getPatternList: patternList, getCardColor: cardColor, getLuckyColor: luckyCardColor, getTextColor: textColor, getCardSize: cardSize, getTextSize: textSize, getRowCount: rowCount, getIsShowPrizeList: isShowPrizeList, getLanguage: userLanguage, getBackground: backgroundImage, getImageList: imageList, getIsShowAvatar: isShowAvatar,
|
||||
const { getTopTitle: topTitle, getTheme: localTheme, getPatterColor: patternColor, getPatternList: patternList, getCardColor: cardColor, getLuckyColor: luckyCardColor, getTextColor: textColor, getCardSize: cardSize, getTextSize: textSize, getRowCount: rowCount, getIsShowPrizeList: isShowPrizeList, getLanguage: userLanguage, getBackground: backgroundImage, getFont: currentFont, getImageList: imageList, getIsShowAvatar: isShowAvatar,
|
||||
} = storeToRefs(globalConfig)
|
||||
const { getAlreadyPersonList: alreadyPersonList, getNotPersonList: notPersonList } = storeToRefs(personConfig)
|
||||
const colorPickerRef = ref()
|
||||
@@ -42,6 +43,7 @@ const patternColorValue = ref(structuredClone(patternColor.value))
|
||||
const themeList = ref(daisyuiThemes)
|
||||
const daisyuiThemeList = ref<ThemeDaType>(daisyuiThemes)
|
||||
const backgroundImageValue = ref(backgroundImage.value)
|
||||
const currentFontValue = ref(currentFont.value)
|
||||
const formData = ref({
|
||||
rowCount: rowCountValue,
|
||||
})
|
||||
@@ -150,6 +152,10 @@ watch(isShowPrizeListValue, () => {
|
||||
watch(backgroundImageValue, (val) => {
|
||||
globalConfig.setBackground(val)
|
||||
})
|
||||
watch(currentFontValue, (val) => {
|
||||
globalConfig.setFont(val)
|
||||
document.documentElement.style.setProperty('--app-font-family', `"${val}", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`)
|
||||
}, { immediate: true })
|
||||
watch(languageValue, (val: string) => {
|
||||
globalConfig.setLanguage(val)
|
||||
})
|
||||
@@ -225,6 +231,13 @@ onMounted(() => {
|
||||
class="w-full max-w-xs input input-bordered"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label class="w-full max-w-xs form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">字体</span>
|
||||
</div>
|
||||
<SelectFont v-model:selected-font="currentFontValue" />
|
||||
</label>
|
||||
</fieldset>
|
||||
<!-- 布局设置(列数、卡片宽度、卡片高度 -->
|
||||
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs">
|
||||
|
||||
@@ -49,7 +49,6 @@ async function getImageDbStore() {
|
||||
const keys = await imageDbStore.keys()
|
||||
if (keys.length > 0) {
|
||||
imageDbStore.iterate((value: { fileName: string, dataUrl: string }, key: string) => {
|
||||
console.log(value, key)
|
||||
globalConfig.addImage({
|
||||
id: key,
|
||||
name: value.fileName,
|
||||
|
||||
@@ -136,7 +136,6 @@ onMounted(() => {
|
||||
getImageDbStore()
|
||||
})
|
||||
watch(() => prizeList.value, (val: IPrizeConfig[]) => {
|
||||
console.log('prizeList', val)
|
||||
prizeConfig.setPrizeConfig(val)
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
@@ -89,7 +89,6 @@ export function usePrizeConfig() {
|
||||
getImageDbStore()
|
||||
})
|
||||
watch(() => prizeList.value, (val: IPrizeConfig[]) => {
|
||||
console.log('prizeList', val)
|
||||
prizeConfig.setPrizeConfig(val)
|
||||
}, { deep: true })
|
||||
|
||||
|
||||
@@ -1,51 +1,110 @@
|
||||
<script setup lang='ts'>
|
||||
import { Grip } from 'lucide-vue-next'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import { ChevronRight, ChevronsUpDownIcon } from 'lucide-vue-next'
|
||||
import { PopoverArrow } from 'reka-ui'
|
||||
import { ref } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
|
||||
const list = ref([
|
||||
{
|
||||
name: 'Joao',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
name: 'Jean',
|
||||
id: '2',
|
||||
},
|
||||
{
|
||||
name: 'Johanna',
|
||||
id: '3',
|
||||
},
|
||||
{
|
||||
name: 'Juan',
|
||||
id: '4',
|
||||
},
|
||||
])
|
||||
onMounted(() => {
|
||||
import { useLocalFonts } from '@/hooks/useLocalFonts'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
})
|
||||
const { getFonts, disabled, fonts } = useLocalFonts()
|
||||
const open = ref(false)
|
||||
const activeKey = ref('Arial')
|
||||
const debouncedActiveKey = refDebounced(activeKey, 20)
|
||||
const selectedFont = ref('')
|
||||
|
||||
function selectFont(selectedValue: any) {
|
||||
open.value = false
|
||||
activeKey.value = ''
|
||||
selectedFont.value = selectedValue
|
||||
}
|
||||
|
||||
function handelActiveKey(val: string) {
|
||||
activeKey.value = val
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
activeKey.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button class="btn btn-error">
|
||||
打印
|
||||
</button>
|
||||
<VueDraggable
|
||||
v-model="list"
|
||||
:animation="150"
|
||||
handle=".handle"
|
||||
class="flex flex-col gap-2 p-4 w-300px bg-gray-500/5 rounded"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in list"
|
||||
:key="item.id"
|
||||
class="h-50px bg-gray-500/5 px-2 rounded flex items-center justify-between"
|
||||
>
|
||||
<Grip class="handle cursor-move" />
|
||||
<input v-model="item.name" type="text">
|
||||
</div>
|
||||
</VueDraggable>
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child :disabled="disabled">
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
:aria-expanded="open"
|
||||
class="w-[200px] justify-between"
|
||||
@click="getFonts"
|
||||
>
|
||||
{{ selectedFont || "选择字体..." }}
|
||||
<ChevronsUpDownIcon class="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput class="h-9" placeholder="Search framework..." />
|
||||
<CommandList @scroll="handleScroll">
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="[key, value] in fonts"
|
||||
:key="key"
|
||||
:value="key"
|
||||
class="w-full hover:bg-gray-200/50"
|
||||
@select="selectFont(key)"
|
||||
>
|
||||
<Popover :open="debouncedActiveKey === key" class="w-full">
|
||||
<PopoverTrigger class="w-full">
|
||||
<div :style="{ fontFamily: `${key}` }" class="w-full flex justify-between items-center" @mouseleave="handelActiveKey('')" @mouseenter="handelActiveKey(key)">
|
||||
{{ key }}
|
||||
<ChevronRight
|
||||
:class="cn(
|
||||
'ml-auto',
|
||||
)"
|
||||
/>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-2" side="right" @mouseleave="handelActiveKey('')" @mouseenter="handelActiveKey(key)">
|
||||
<PopoverArrow />
|
||||
<Command>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="child in value"
|
||||
:key="child.value"
|
||||
:value="child.value"
|
||||
class="w-full hover:bg-gray-200/50"
|
||||
:style="{ fontFamily: `${key}` }"
|
||||
@select="selectFont(child.value)"
|
||||
>
|
||||
{{ child.name }}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const { t } = useI18n()
|
||||
<template>
|
||||
<div class="absolute z-10 flex flex-col items-center justify-center -translate-x-1/2 left-1/2">
|
||||
<h2
|
||||
class="pt-12 m-0 mb-12 font-mono tracking-wide text-center leading-12 header-title"
|
||||
class="pt-12 m-0 mb-12 tracking-wide text-center leading-12 header-title"
|
||||
:style="{ fontSize: `${textSize * 1.5}px`, color: textColor }"
|
||||
>
|
||||
{{ topTitle }}
|
||||
|
||||
@@ -160,7 +160,6 @@ export function useViewModel() {
|
||||
for (let i = 0; i < objLength; ++i) {
|
||||
const object = objects.value[i]
|
||||
const target = targets[i]
|
||||
// console.log('target', i, target, targets)
|
||||
new TWEEN.Tween(object.position)
|
||||
.to({ x: target.position.x, y: target.position.y, z: target.position.z }, Math.random() * duration + duration)
|
||||
.easing(TWEEN.Easing.Exponential.InOut)
|
||||
|
||||
12
src/window.d.ts
vendored
Normal file
12
src/window.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// src/types/window.d.ts
|
||||
interface FontData {
|
||||
family: string
|
||||
fullName: string
|
||||
postscriptName: string
|
||||
style: string
|
||||
blob: () => Promise<Blob>
|
||||
}
|
||||
|
||||
interface Window {
|
||||
queryLocalFonts?: (options?) => Promise<FontData[]>
|
||||
}
|
||||
Reference in New Issue
Block a user