replace element plus components

This commit is contained in:
pa
2026-01-15 22:38:09 +09:00
committed by Natsumi
parent bdc1d3a347
commit c430ce1b63
46 changed files with 2143 additions and 1752 deletions
+127 -127
View File
@@ -14,19 +14,19 @@
"@electron/rebuild": "^4.0.2", "@electron/rebuild": "^4.0.2",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/noto-sans-jp": "^5.2.9", "@fontsource-variable/noto-sans-jp": "^5.2.10",
"@fontsource-variable/noto-sans-kr": "^5.2.9", "@fontsource-variable/noto-sans-kr": "^5.2.10",
"@fontsource-variable/noto-sans-sc": "^5.2.9", "@fontsource-variable/noto-sans-sc": "^5.2.10",
"@fontsource-variable/noto-sans-tc": "^5.2.9", "@fontsource-variable/noto-sans-tc": "^5.2.10",
"@internationalized/date": "^3.10.1", "@internationalized/date": "^3.10.1",
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6", "@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
"@sentry/vite-plugin": "^4.6.1", "@sentry/vite-plugin": "^4.6.2",
"@sentry/vue": "^10.33.0", "@sentry/vue": "^10.34.0",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@tanstack/vue-virtual": "^3.13.18", "@tanstack/vue-virtual": "^3.13.18",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^25.0.7", "@types/node": "^25.0.8",
"@vee-validate/zod": "^4.15.1", "@vee-validate/zod": "^4.15.1",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3", "@vitejs/plugin-vue-jsx": "^5.1.3",
@@ -46,7 +46,7 @@
"esbuild-jest": "^0.5.0", "esbuild-jest": "^0.5.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-vue": "^9.33.0", "eslint-plugin-vue": "^9.33.0",
"globals": "^17.0.0", "globals": "^17.0.0",
"jest": "^30.2.0", "jest": "^30.2.0",
@@ -54,7 +54,7 @@
"lucide-vue-next": "^0.562.0", "lucide-vue-next": "^0.562.0",
"noty": "^3.2.0-beta-deprecated", "noty": "^3.2.0-beta-deprecated",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"prettier": "^3.7.4", "prettier": "^3.8.0",
"reka-ui": "^2.7.0", "reka-ui": "^2.7.0",
"remixicon": "^4.8.0", "remixicon": "^4.8.0",
"sass-embedded": "^1.97.2", "sass-embedded": "^1.97.2",
@@ -2097,9 +2097,9 @@
} }
}, },
"node_modules/@fontsource-variable/noto-sans-jp": { "node_modules/@fontsource-variable/noto-sans-jp": {
"version": "5.2.9", "version": "5.2.10",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-jp/-/noto-sans-jp-5.2.9.tgz", "resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-jp/-/noto-sans-jp-5.2.10.tgz",
"integrity": "sha512-osPL5f7dvGDjuMuFwDTGPLG37030D8X5zk+3BWea6txAVDFeE/ZIrKW0DY0uSDfRn9+NiKbiFn/2QvZveKXTog==", "integrity": "sha512-m0XfZ38rZtCaGAuAQL0cBPQ6fc/RguYEOqw66zvuLOV9vNRUBf9MFqyoazTu2JVCRIcnkrxbwF29UUaHHgTKdg==",
"dev": true, "dev": true,
"license": "OFL-1.1", "license": "OFL-1.1",
"funding": { "funding": {
@@ -2107,9 +2107,9 @@
} }
}, },
"node_modules/@fontsource-variable/noto-sans-kr": { "node_modules/@fontsource-variable/noto-sans-kr": {
"version": "5.2.9", "version": "5.2.10",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-kr/-/noto-sans-kr-5.2.9.tgz", "resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-kr/-/noto-sans-kr-5.2.10.tgz",
"integrity": "sha512-g1BnJdJbnAgRUP8YxyPIm8npZVUbtt6VgtLnkGR7poa/RVbVGd27i+9138DmwRvtbKhJG1fPLQ/V3BonvFykRQ==", "integrity": "sha512-UZOO7HF44Rt5+7SCeFMHYVgbKu36Jet6IxrAd7jjEkQMVDmeefwd0H8V5pSZydvBOOxClk3V5cQsujJqGHhQMw==",
"dev": true, "dev": true,
"license": "OFL-1.1", "license": "OFL-1.1",
"funding": { "funding": {
@@ -2117,9 +2117,9 @@
} }
}, },
"node_modules/@fontsource-variable/noto-sans-sc": { "node_modules/@fontsource-variable/noto-sans-sc": {
"version": "5.2.9", "version": "5.2.10",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.2.9.tgz", "resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.2.10.tgz",
"integrity": "sha512-ZEEpZlxjYEIVdg85K38mqaoeBcorrN3Z6MaIkwK5w5Dqn/e9v5uVIYr0ukoLsFxaVyEXSi/c3caOeMHjbOMtfA==", "integrity": "sha512-zdk10i5HrDQTXI7ldD61zToX1fsgig8vDTsu7zB48SXOitWfuX0e5viZAwnkHuhwh096PU6X6i1AyAsbBCISpA==",
"dev": true, "dev": true,
"license": "OFL-1.1", "license": "OFL-1.1",
"funding": { "funding": {
@@ -2127,9 +2127,9 @@
} }
}, },
"node_modules/@fontsource-variable/noto-sans-tc": { "node_modules/@fontsource-variable/noto-sans-tc": {
"version": "5.2.9", "version": "5.2.10",
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-tc/-/noto-sans-tc-5.2.9.tgz", "resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-tc/-/noto-sans-tc-5.2.10.tgz",
"integrity": "sha512-GhtbSE8IZTP3vZj7Fu1G/PERxguMe3jryAbHovSd22Rs7aYdbXQD8vBqkTT/tkHIUn6t2IFReTfgKUoQBPCe+w==", "integrity": "sha512-COVssWiIp9VVfdGuHpbrcH8u9H5JK4Lm6U7oee2BD6aLwKzPshj49KGmmQe/r3Fcp4e4SSuCaqyN/BtDKxHOGQ==",
"dev": true, "dev": true,
"license": "OFL-1.1", "license": "OFL-1.1",
"funding": { "funding": {
@@ -4372,63 +4372,63 @@
] ]
}, },
"node_modules/@sentry-internal/browser-utils": { "node_modules/@sentry-internal/browser-utils": {
"version": "10.33.0", "version": "10.34.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.33.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.34.0.tgz",
"integrity": "sha512-nDJFHAfiFifBfJB0OF6DV6BIsIV5uah4lDsV4UBAgPBf+YAHclO10y1gi2U/JMh58c+s4lXi9p+PI1TFXZ0c6w==", "integrity": "sha512-0YNr60rGHyedmwkO0lbDBjNx2KAmT3kWamjaqu7Aw+jsESoPLgt+fzaTVvUBvkftBDui2PeTSzXm/nqzssctYg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/core": "10.33.0" "@sentry/core": "10.34.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/feedback": { "node_modules/@sentry-internal/feedback": {
"version": "10.33.0", "version": "10.34.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.33.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.34.0.tgz",
"integrity": "sha512-sN/VLWtEf0BeV6w6wldIpTxUQxNVc9o9tjLRQa8je1ZV2FCgXA124Iff/zsowsz82dLqtg7qp6GA5zYXVq+JMA==", "integrity": "sha512-wgGnq+iNxsFSOe9WX/FOvtoItSTjgLJJ4dQkVYtcVM6WGBVIg4wgNYfECCnRNztUTPzpZHLjC9r+4Pym451DDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/core": "10.33.0" "@sentry/core": "10.34.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/replay": { "node_modules/@sentry-internal/replay": {
"version": "10.33.0", "version": "10.34.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.33.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.34.0.tgz",
"integrity": "sha512-UOU9PYxuXnPop3HoQ3l4Q7SZUXJC3Vmfm0Adgad8U03UcrThWIHYc5CxECSrVzfDFNOT7w9o7HQgRAgWxBPMXg==", "integrity": "sha512-Vmea0GcOg57z/S1bVSj3saFcRvDqdLzdy4wd9fQMpMgy5OCbTlo7lxVUndKzbcZnanma6zF6VxwnWER1WuN9RA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/browser-utils": "10.33.0", "@sentry-internal/browser-utils": "10.34.0",
"@sentry/core": "10.33.0" "@sentry/core": "10.34.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/replay-canvas": { "node_modules/@sentry-internal/replay-canvas": {
"version": "10.33.0", "version": "10.34.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.33.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.34.0.tgz",
"integrity": "sha512-MTmP6uoAVzw4CCPeqCgCLsRSiOfGLxgyMFjGTCW3E7t62MJ9S0H5sLsQ34sHxXUa1gFU9UNAjEvRRpZ0JvWrPw==", "integrity": "sha512-XWH/9njtgMD+LLWjc4KKgBpb+dTCkoUEIFDxcvzG/87d+jirmzf0+r8EfpLwKG+GrqNiiGRV39zIqu0SfPl+cw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/replay": "10.33.0", "@sentry-internal/replay": "10.34.0",
"@sentry/core": "10.33.0" "@sentry/core": "10.34.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry/babel-plugin-component-annotate": { "node_modules/@sentry/babel-plugin-component-annotate": {
"version": "4.6.1", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.2.tgz",
"integrity": "sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA==", "integrity": "sha512-6VTjLJXtIHKwxMmThtZKwi1+hdklLNzlbYH98NhbH22/Vzb/c6BlSD2b5A0NGN9vFB807rD4x4tuP+Su7BxQXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4436,31 +4436,31 @@
} }
}, },
"node_modules/@sentry/browser": { "node_modules/@sentry/browser": {
"version": "10.33.0", "version": "10.34.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.33.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.34.0.tgz",
"integrity": "sha512-iWiPjik9zetM84jKfk01UveW1J0+X7w8XmJ8+IrhTyNDBVUWCRJWD8FrksiN1dRSg5mFWgfMRzKMz27hAScRwg==", "integrity": "sha512-8WCsAXli5Z+eIN8dMY8KGQjrS3XgUp1np/pjdeWNrVPVR8q8XpS34qc+f+y/LFrYQC9bs2Of5aIBwRtDCIvRsg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/browser-utils": "10.33.0", "@sentry-internal/browser-utils": "10.34.0",
"@sentry-internal/feedback": "10.33.0", "@sentry-internal/feedback": "10.34.0",
"@sentry-internal/replay": "10.33.0", "@sentry-internal/replay": "10.34.0",
"@sentry-internal/replay-canvas": "10.33.0", "@sentry-internal/replay-canvas": "10.34.0",
"@sentry/core": "10.33.0" "@sentry/core": "10.34.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry/bundler-plugin-core": { "node_modules/@sentry/bundler-plugin-core": {
"version": "4.6.1", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.2.tgz",
"integrity": "sha512-WPeRbnMXm927m4Kr69NTArPfI+p5/34FHftdCRI3LFPMyhZDzz6J3wLy4hzaVUgmMf10eLzmq2HGEMvpQmdynA==", "integrity": "sha512-JkOc3JkVzi/fbXsFp8R9uxNKmBrPRaU4Yu4y1i3ihWfugqymsIYaN0ixLENZbGk2j4xGHIk20PAJzBJqBMTHew==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "^7.18.5", "@babel/core": "^7.18.5",
"@sentry/babel-plugin-component-annotate": "4.6.1", "@sentry/babel-plugin-component-annotate": "4.6.2",
"@sentry/cli": "^2.57.0", "@sentry/cli": "^2.57.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"find-up": "^5.0.0", "find-up": "^5.0.0",
@@ -4473,12 +4473,12 @@
} }
}, },
"node_modules/@sentry/cli": { "node_modules/@sentry/cli": {
"version": "2.58.2", "version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.2.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.4.tgz",
"integrity": "sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw==", "integrity": "sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "BSD-3-Clause", "license": "FSL-1.1-MIT",
"dependencies": { "dependencies": {
"https-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
@@ -4493,22 +4493,22 @@
"node": ">= 10" "node": ">= 10"
}, },
"optionalDependencies": { "optionalDependencies": {
"@sentry/cli-darwin": "2.58.2", "@sentry/cli-darwin": "2.58.4",
"@sentry/cli-linux-arm": "2.58.2", "@sentry/cli-linux-arm": "2.58.4",
"@sentry/cli-linux-arm64": "2.58.2", "@sentry/cli-linux-arm64": "2.58.4",
"@sentry/cli-linux-i686": "2.58.2", "@sentry/cli-linux-i686": "2.58.4",
"@sentry/cli-linux-x64": "2.58.2", "@sentry/cli-linux-x64": "2.58.4",
"@sentry/cli-win32-arm64": "2.58.2", "@sentry/cli-win32-arm64": "2.58.4",
"@sentry/cli-win32-i686": "2.58.2", "@sentry/cli-win32-i686": "2.58.4",
"@sentry/cli-win32-x64": "2.58.2" "@sentry/cli-win32-x64": "2.58.4"
} }
}, },
"node_modules/@sentry/cli-darwin": { "node_modules/@sentry/cli-darwin": {
"version": "2.58.2", "version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.2.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.4.tgz",
"integrity": "sha512-MArsb3zLhA2/cbd4rTm09SmTpnEuZCoZOpuZYkrpDw1qzBVJmRFA1W1hGAQ9puzBIk/ubY3EUhhzuU3zN2uD6w==", "integrity": "sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ==",
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "FSL-1.1-MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
@@ -4518,14 +4518,14 @@
} }
}, },
"node_modules/@sentry/cli-linux-arm": { "node_modules/@sentry/cli-linux-arm": {
"version": "2.58.2", "version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.2.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.4.tgz",
"integrity": "sha512-HU9lTCzcHqCz/7Mt5n+cv+nFuJdc1hGD2h35Uo92GgxX3/IujNvOUfF+nMX9j6BXH6hUt73R5c0Ycq9+a3Parg==", "integrity": "sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "FSL-1.1-MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux", "linux",
@@ -4537,14 +4537,14 @@
} }
}, },
"node_modules/@sentry/cli-linux-arm64": { "node_modules/@sentry/cli-linux-arm64": {
"version": "2.58.2", "version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.2.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.4.tgz",
"integrity": "sha512-ay3OeObnbbPrt45cjeUyQjsx5ain1laj1tRszWj37NkKu55NZSp4QCg1gGBZ0gBGhckI9nInEsmKtix00alw2g==", "integrity": "sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "FSL-1.1-MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux", "linux",
@@ -4556,15 +4556,15 @@
} }
}, },
"node_modules/@sentry/cli-linux-i686": { "node_modules/@sentry/cli-linux-i686": {
"version": "2.58.2", "version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.2.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.4.tgz",
"integrity": "sha512-CN9p0nfDFsAT1tTGBbzOUGkIllwS3hygOUyTK7LIm9z+UHw5uNgNVqdM/3Vg+02ymjkjISNB3/+mqEM5osGXdA==", "integrity": "sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA==",
"cpu": [ "cpu": [
"x86", "x86",
"ia32" "ia32"
], ],
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "FSL-1.1-MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux", "linux",
@@ -4576,14 +4576,14 @@
} }
}, },
"node_modules/@sentry/cli-linux-x64": { "node_modules/@sentry/cli-linux-x64": {
"version": "2.58.2", "version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.2.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.4.tgz",
"integrity": "sha512-oX/LLfvWaJO50oBVOn4ZvG2SDWPq0MN8SV9eg5tt2nviq+Ryltfr7Rtoo+HfV+eyOlx1/ZXhq9Wm7OT3cQuz+A==", "integrity": "sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "FSL-1.1-MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux", "linux",
@@ -4595,14 +4595,14 @@
} }
}, },
"node_modules/@sentry/cli-win32-arm64": { "node_modules/@sentry/cli-win32-arm64": {
"version": "2.58.2", "version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.2.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.4.tgz",
"integrity": "sha512-+cl3x2HPVMpoSVGVM1IDWlAEREZrrVQj4xBb0TRKII7g3hUxRsAIcsrr7+tSkie++0FuH4go/b5fGAv51OEF3w==", "integrity": "sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "FSL-1.1-MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@@ -4612,15 +4612,15 @@
} }
}, },
"node_modules/@sentry/cli-win32-i686": { "node_modules/@sentry/cli-win32-i686": {
"version": "2.58.2", "version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.2.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.4.tgz",
"integrity": "sha512-omFVr0FhzJ8oTJSg1Kf+gjLgzpYklY0XPfLxZ5iiMiYUKwF5uo1RJRdkUOiEAv0IqpUKnmKcmVCLaDxsWclB7Q==", "integrity": "sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug==",
"cpu": [ "cpu": [
"x86", "x86",
"ia32" "ia32"
], ],
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "FSL-1.1-MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@@ -4630,14 +4630,14 @@
} }
}, },
"node_modules/@sentry/cli-win32-x64": { "node_modules/@sentry/cli-win32-x64": {
"version": "2.58.2", "version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.2.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.4.tgz",
"integrity": "sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA==", "integrity": "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "FSL-1.1-MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@@ -4647,9 +4647,9 @@
} }
}, },
"node_modules/@sentry/core": { "node_modules/@sentry/core": {
"version": "10.33.0", "version": "10.34.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.33.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.34.0.tgz",
"integrity": "sha512-ehH1VSUclIHZKEZVdv+klofsFIh8FFzqA6AAV23RtLepptzA8wqQzUGraEuSN25sYcNmYJ0jti5U0Ys+WZv5Dw==", "integrity": "sha512-4FFpYBMf0VFdPcsr4grDYDOR87mRu6oCfb51oQjU/Pndmty7UgYo0Bst3LEC/8v0SpytBtzXq+Wx/fkwulBesg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4657,13 +4657,13 @@
} }
}, },
"node_modules/@sentry/vite-plugin": { "node_modules/@sentry/vite-plugin": {
"version": "4.6.1", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.6.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.6.2.tgz",
"integrity": "sha512-Qvys1y3o8/bfL3ikrHnJS9zxdjt0z3POshdBl3967UcflrTqBmnGNkcVk53SlmtJWIfh85fgmrLvGYwZ2YiqNg==", "integrity": "sha512-hK9N50LlTaPlb2P1r87CFupU7MJjvtrp+Js96a2KDdiP8ViWnw4Gsa/OvA0pkj2wAFXFeBQMLS6g/SktTKG54w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/bundler-plugin-core": "4.6.1", "@sentry/bundler-plugin-core": "4.6.2",
"unplugin": "1.0.1" "unplugin": "1.0.1"
}, },
"engines": { "engines": {
@@ -4671,14 +4671,14 @@
} }
}, },
"node_modules/@sentry/vue": { "node_modules/@sentry/vue": {
"version": "10.33.0", "version": "10.34.0",
"resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-10.33.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-10.34.0.tgz",
"integrity": "sha512-CUtoBl62DG8mkoYfgpkw2WdB187XA2CfPj7OJdIzt3lavhpSAPmsY4jUarK2RUJvcowr5zYbEfv50Y0tsQxuGA==", "integrity": "sha512-2c+s8pQKY/MTunwIgTsiMtq4c7cPYyhB1LFOZ/VJSQH8MLD7qzxG7ed8SOJ3NTnGn8a2TpW/vPtc3uh90zLH3Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/browser": "10.33.0", "@sentry/browser": "10.34.0",
"@sentry/core": "10.33.0" "@sentry/core": "10.34.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -5313,9 +5313,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.0.7", "version": "25.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz",
"integrity": "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==", "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -9004,14 +9004,14 @@
} }
}, },
"node_modules/eslint-plugin-prettier": { "node_modules/eslint-plugin-prettier": {
"version": "5.5.4", "version": "5.5.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
"integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"prettier-linter-helpers": "^1.0.0", "prettier-linter-helpers": "^1.0.1",
"synckit": "^0.11.7" "synckit": "^0.11.12"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
@@ -14900,9 +14900,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.7.4", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
@@ -14917,9 +14917,9 @@
} }
}, },
"node_modules/prettier-linter-helpers": { "node_modules/prettier-linter-helpers": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -17080,9 +17080,9 @@
} }
}, },
"node_modules/synckit": { "node_modules/synckit": {
"version": "0.11.11", "version": "0.11.12",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+9 -9
View File
@@ -35,19 +35,19 @@
"@electron/rebuild": "^4.0.2", "@electron/rebuild": "^4.0.2",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/noto-sans-jp": "^5.2.9", "@fontsource-variable/noto-sans-jp": "^5.2.10",
"@fontsource-variable/noto-sans-kr": "^5.2.9", "@fontsource-variable/noto-sans-kr": "^5.2.10",
"@fontsource-variable/noto-sans-sc": "^5.2.9", "@fontsource-variable/noto-sans-sc": "^5.2.10",
"@fontsource-variable/noto-sans-tc": "^5.2.9", "@fontsource-variable/noto-sans-tc": "^5.2.10",
"@internationalized/date": "^3.10.1", "@internationalized/date": "^3.10.1",
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6", "@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
"@sentry/vite-plugin": "^4.6.1", "@sentry/vite-plugin": "^4.6.2",
"@sentry/vue": "^10.33.0", "@sentry/vue": "^10.34.0",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@tanstack/vue-virtual": "^3.13.18", "@tanstack/vue-virtual": "^3.13.18",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^25.0.7", "@types/node": "^25.0.8",
"@vee-validate/zod": "^4.15.1", "@vee-validate/zod": "^4.15.1",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3", "@vitejs/plugin-vue-jsx": "^5.1.3",
@@ -67,7 +67,7 @@
"esbuild-jest": "^0.5.0", "esbuild-jest": "^0.5.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-vue": "^9.33.0", "eslint-plugin-vue": "^9.33.0",
"globals": "^17.0.0", "globals": "^17.0.0",
"jest": "^30.2.0", "jest": "^30.2.0",
@@ -75,7 +75,7 @@
"lucide-vue-next": "^0.562.0", "lucide-vue-next": "^0.562.0",
"noty": "^3.2.0-beta-deprecated", "noty": "^3.2.0-beta-deprecated",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"prettier": "^3.7.4", "prettier": "^3.8.0",
"reka-ui": "^2.7.0", "reka-ui": "^2.7.0",
"remixicon": "^4.8.0", "remixicon": "^4.8.0",
"sass-embedded": "^1.97.2", "sass-embedded": "^1.97.2",
-6
View File
@@ -1,7 +1,5 @@
<template> <template>
<TooltipProvider> <TooltipProvider>
<el-config-provider
:locale="/** @type {import('element-plus/es/locale').Language} */ (messages[locale].elementPlus)">
<MacOSTitleBar></MacOSTitleBar> <MacOSTitleBar></MacOSTitleBar>
<div <div
@@ -20,13 +18,11 @@
<VRCXUpdateDialog></VRCXUpdateDialog> <VRCXUpdateDialog></VRCXUpdateDialog>
</div> </div>
<div id="x-dialog-portal" class="x-dialog-portal"></div> <div id="x-dialog-portal" class="x-dialog-portal"></div>
</el-config-provider>
</TooltipProvider> </TooltipProvider>
</template> </template>
<script setup> <script setup>
import { computed, onBeforeMount, onMounted } from 'vue'; import { computed, onBeforeMount, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { Toaster } from './components/ui/sonner'; import { Toaster } from './components/ui/sonner';
import { TooltipProvider } from './components/ui/tooltip'; import { TooltipProvider } from './components/ui/tooltip';
@@ -45,8 +41,6 @@
const isMacOS = computed(() => navigator.platform.includes('Mac')); const isMacOS = computed(() => navigator.platform.includes('Mac'));
const { locale, messages } = useI18n();
initNoty(); initNoty();
const store = createGlobalStores(); const store = createGlobalStores();
+177
View File
@@ -0,0 +1,177 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { ArrowUp } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
const props = defineProps({
target: { type: [String, Object], default: null },
bottom: { type: Number, default: 20 },
right: { type: Number, default: 20 },
visibilityHeight: { type: Number, default: 200 },
behavior: {
type: String,
default: 'smooth',
validator: (value) => value === 'auto' || value === 'smooth'
},
tooltip: { type: Boolean, default: true },
tooltipText: { type: String, default: 'Back to top' },
teleport: { type: Boolean, default: true }
});
const visible = ref(false);
let containerEl = null;
function resolveTarget() {
if (!props.target) return null;
if (typeof props.target === 'string') {
return document.querySelector(props.target);
}
if (typeof props.target === 'object') {
if ('value' in props.target) {
return props.target.value;
}
if ('$el' in props.target) {
return props.target.$el;
}
}
return props.target;
}
function getScrollTop() {
if (!containerEl || typeof containerEl.scrollTop !== 'number') {
return window.scrollY || document.documentElement.scrollTop || 0;
}
return containerEl.scrollTop || 0;
}
function handleScroll() {
visible.value = getScrollTop() >= props.visibilityHeight;
}
function scrollToTop() {
const behavior = props.behavior === 'auto' ? 'auto' : 'smooth';
if (!containerEl || typeof containerEl.scrollTo !== 'function') {
window.scrollTo({ top: 0, behavior });
return;
}
containerEl.scrollTo({ top: 0, behavior });
}
function bind() {
containerEl = resolveTarget();
const target = containerEl && typeof containerEl.addEventListener === 'function' ? containerEl : window;
target.addEventListener('scroll', handleScroll, { passive: true });
handleScroll();
}
function unbind() {
const target = containerEl || window;
target.removeEventListener('scroll', handleScroll);
}
onMounted(() => {
bind();
});
watch(
() => props.target,
() => {
unbind();
bind();
}
);
onBeforeUnmount(() => {
unbind();
});
const wrapperStyle = computed(
() => `position:fixed; right:${props.right}px; bottom:${props.bottom}px; z-index:50;`
);
</script>
<template>
<Teleport v-if="teleport" to="body">
<Transition name="back-to-top">
<div v-if="visible" :style="wrapperStyle">
<TooltipProvider v-if="tooltip">
<Tooltip>
<TooltipTrigger as-child>
<Button
size="icon"
variant="secondary"
class="rounded-full shadow"
aria-label="Back to top"
@click="scrollToTop">
<ArrowUp class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">
{{ tooltipText }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
v-else
size="icon"
variant="secondary"
class="rounded-full shadow"
aria-label="Back to top"
@click="scrollToTop">
<ArrowUp class="h-4 w-4" />
</Button>
</div>
</Transition>
</Teleport>
<Transition v-else name="back-to-top">
<div v-if="visible" :style="wrapperStyle">
<Button
size="icon"
variant="secondary"
class="rounded-full shadow"
aria-label="Back to top"
@click="scrollToTop">
<ArrowUp class="h-4 w-4" />
</Button>
</div>
</Transition>
</template>
<style scoped>
.back-to-top-enter-active,
.back-to-top-leave-active {
transition:
opacity 160ms ease,
transform 160ms ease;
}
.back-to-top-enter-from,
.back-to-top-leave-to {
opacity: 0;
transform: translateY(6px);
}
.back-to-top-enter-to,
.back-to-top-leave-from {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.back-to-top-enter-active,
.back-to-top-leave-active {
transition: none;
}
}
</style>
+282 -119
View File
@@ -1,154 +1,332 @@
<template> <template>
<Dialog v-model:open="open">
<DialogPortal :to="portalTo">
<RekaDialogOverlay class="fixed inset-0 bg-background/80 backdrop-blur-sm" @click="closeDialog" />
<RekaDialogContent
class="fixed inset-0 p-6 sm:p-10 border-0 bg-transparent shadow-none outline-none"
@open-auto-focus.prevent
@close-auto-focus.prevent>
<div ref="viewerEl" class="relative h-full w-full overflow-hidden select-none">
<!-- toolbar -->
<div <div
v-if="fullscreenImageDialog.visible" class="absolute right-3 top-3 z-10 flex items-center gap-2 rounded-md bg-background/70 backdrop-blur px-2 py-1 border">
class="fullscreen-image-overlay" <Button
:style="{ zIndex: overlayZIndex }" variant="ghost"
@click.self="closeDialog"> size="icon"
<el-image class="h-8 w-8"
v-if="fullscreenImageDialog.imageUrl" :disabled="!imageUrl"
ref="imageRef" @click="copyImageToClipboard(imageUrl)"
class="fullscreen-image" aria-label="Copy">
:src="fullscreenImageDialog.imageUrl" <Copy class="h-4 w-4" />
:preview-src-list="[fullscreenImageDialog.imageUrl]" </Button>
:z-index="100000"
fit="contain" <Button
preview-teleported variant="ghost"
hide-on-click-modal size="icon"
:initial-index="0" class="h-8 w-8"
@load="handleImageLoad" :disabled="!imageUrl"
@close="closeDialog"> @click="downloadAndSaveImage(imageUrl, fullscreenImageDialog.fileName)"
<template #toolbar="{ actions }"> aria-label="Download">
<Copy @click="copyImageToClipboard(fullscreenImageDialog.imageUrl)" class="toolbar-icon" /> <Download class="h-4 w-4" />
<Download </Button>
@click="downloadAndSaveImage(fullscreenImageDialog.imageUrl, fullscreenImageDialog.fileName)"
class="toolbar-icon" /> <div class="mx-1 h-5 w-px bg-border" />
<ZoomOut @click="actions('zoomOut')" class="toolbar-icon" />
<ZoomIn @click="actions('zoomIn')" class="toolbar-icon" /> <Button
<RotateCw @click="actions('clockwise')" class="toolbar-icon" /> variant="ghost"
<RotateCcw @click="actions('anticlockwise')" class="toolbar-icon" /> size="icon"
</template> class="h-8 w-8"
</el-image> @click="zoomOutCenter"
aria-label="Zoom out">
<ZoomOut class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="zoomInCenter" aria-label="Zoom in">
<ZoomIn class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click="rotateCW"
aria-label="Rotate clockwise">
<RotateCw class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click="rotateCCW"
aria-label="Rotate counterclockwise">
<RotateCcw class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="resetTransform" aria-label="Reset">
<RefreshCcw class="h-4 w-4" />
</Button>
<div class="mx-1 h-5 w-px bg-border" />
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeDialog" aria-label="Close">
<X class="h-4 w-4" />
</Button>
</div> </div>
<div
class="h-full w-full flex items-center justify-center"
@wheel="onWheel"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
@pointercancel="onPointerUp">
<img
v-if="imageUrl"
:src="imageUrl"
class="max-h-full max-w-full x-viewer-img"
:style="transformStyle"
draggable="false" />
</div>
</div>
</RekaDialogContent>
</DialogPortal>
</Dialog>
</template> </template>
<script setup> <script setup>
import { Copy, Download, RotateCcw, RotateCw, ZoomIn, ZoomOut } from 'lucide-vue-next'; import { Copy, Download, RefreshCcw, RotateCcw, RotateCw, X, ZoomIn, ZoomOut } from 'lucide-vue-next';
import { nextTick, ref, watch } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { DialogContent as RekaDialogContent, DialogOverlay as RekaDialogOverlay, DialogPortal } from 'reka-ui';
import { Button } from '@/components/ui/button';
import { Dialog } from '@/components/ui/dialog';
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import Noty from 'noty'; import Noty from 'noty';
import { escapeTag, extractFileId } from '../shared/utils'; import { escapeTag, extractFileId } from '../shared/utils';
import { getNextDialogIndex } from '../shared/utils/base/ui';
import { useGalleryStore } from '../stores'; import { useGalleryStore } from '../stores';
const galleryStore = useGalleryStore(); const galleryStore = useGalleryStore();
const { fullscreenImageDialog } = storeToRefs(galleryStore); const { fullscreenImageDialog } = storeToRefs(galleryStore);
const imageRef = ref(); const viewerEl = ref(null);
const overlayZIndex = ref(4000); const portalLayer = acquireModalPortalLayer();
const portalTo = portalLayer.element;
function showPreview() { const scale = ref(1);
nextTick(() => { const rotate = ref(0); // deg
imageRef.value?.showPreview?.(); const tx = ref(0);
const ty = ref(0);
const isDragging = ref(false);
const dragStartX = ref(0);
const dragStartY = ref(0);
const startTx = ref(0);
const startTy = ref(0);
const imageUrl = computed(() => fullscreenImageDialog.value.imageUrl || '');
const open = computed({
get: () => fullscreenImageDialog.value.visible,
set: (v) => {
fullscreenImageDialog.value.visible = v;
}
}); });
function clamp(n, min, max) {
return Math.min(max, Math.max(min, n));
}
function degToRad(deg) {
return (deg * Math.PI) / 180;
} }
function handleImageLoad() { function resetTransform() {
showPreview(); scale.value = 1;
rotate.value = 0;
tx.value = 0;
ty.value = 0;
} }
watch(
() => fullscreenImageDialog.value.visible,
(visible) => {
if (!visible) {
return;
}
overlayZIndex.value = Math.max(getNextDialogIndex(), 4000);
showPreview();
}
);
watch(
() => fullscreenImageDialog.value.imageUrl,
(url) => {
if (!url || !fullscreenImageDialog.value.visible) {
return;
}
showPreview();
}
);
function closeDialog() { function closeDialog() {
fullscreenImageDialog.value.visible = false; open.value = false;
} }
async function copyImageToClipboard(url) { function zoomAtCenter(factor) {
if (!url) { const el = viewerEl.value;
if (!el) {
scale.value = clamp(scale.value * factor, 0.1, 10);
return; return;
} }
scale.value = clamp(scale.value * factor, 0.1, 10);
}
function zoomInCenter() {
zoomAtCenter(1.2);
}
function zoomOutCenter() {
zoomAtCenter(1 / 1.2);
}
function rotateCW() {
rotate.value = (rotate.value + 90) % 360;
}
function rotateCCW() {
rotate.value = (rotate.value - 90 + 360) % 360;
}
function zoomAtPointer(e, factor) {
const el = viewerEl.value;
if (!el) return;
const rect = el.getBoundingClientRect();
// mouse in container space
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
// container center
const cx = rect.width / 2;
const cy = rect.height / 2;
const oldScale = scale.value;
const newScale = clamp(oldScale * factor, 0.1, 10);
const r = degToRad(rotate.value);
const cos = Math.cos(r);
const sin = Math.sin(r);
// vector from transformed center (includes current translation)
const vx = mx - cx - tx.value;
const vy = my - cy - ty.value;
// inverse rotate + unscale => local point
const ux = (vx * cos + vy * sin) / oldScale;
const uy = (-vx * sin + vy * cos) / oldScale;
// forward rotate + scale => new vector
const v2x = (ux * cos - uy * sin) * newScale;
const v2y = (ux * sin + uy * cos) * newScale;
// keep pointer anchored
tx.value = mx - cx - v2x;
ty.value = my - cy - v2y;
scale.value = newScale;
}
function onWheel(e) {
e.preventDefault();
const factor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
zoomAtPointer(e, factor);
}
function onPointerDown(e) {
if (e.button !== 0) return;
isDragging.value = true;
e.currentTarget.setPointerCapture?.(e.pointerId);
dragStartX.value = e.clientX;
dragStartY.value = e.clientY;
startTx.value = tx.value;
startTy.value = ty.value;
}
function onPointerMove(e) {
if (!isDragging.value) return;
const dx = e.clientX - dragStartX.value;
const dy = e.clientY - dragStartY.value;
tx.value = startTx.value + dx;
ty.value = startTy.value + dy;
}
function onPointerUp(e) {
if (!isDragging.value) return;
isDragging.value = false;
e.currentTarget.releasePointerCapture?.(e.pointerId);
}
const transformStyle = computed(() => ({
transform: `translate(${tx.value}px, ${ty.value}px) scale(${scale.value}) rotate(${rotate.value}deg)`,
transformOrigin: 'center center'
}));
watch(
() => open.value,
(v) => {
if (v) {
portalLayer.bringToFront();
resetTransform();
}
}
);
onBeforeUnmount(() => {
portalLayer.release();
});
watch(
() => imageUrl.value,
(url) => {
if (!url || !open.value) return;
resetTransform();
}
);
function onKeydown(e) {
if (!open.value) return;
if (e.key === '+' || e.key === '=') zoomInCenter();
else if (e.key === '-' || e.key === '_') zoomOutCenter();
else if (e.key.toLowerCase() === 'r') rotateCW();
else if (e.key === '0') resetTransform();
}
onMounted(() => window.addEventListener('keydown', onKeydown));
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown));
async function copyImageToClipboard(url) {
if (!url) return;
const msg = toast.info('Downloading image...'); const msg = toast.info('Downloading image...');
try { try {
const response = await webApiService.execute({ const response = await webApiService.execute({ url, method: 'GET' });
url, if (response.status !== 200 || !String(response.data).startsWith('data:image/png')) {
method: 'GET'
});
if (response.status !== 200 || !response.data.startsWith('data:image/png')) {
throw new Error(`Error: ${response.data}`); throw new Error(`Error: ${response.data}`);
} }
await navigator.clipboard.write([ const blob = await (await fetch(response.data)).blob();
new ClipboardItem({ await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
'image/png': await (await fetch(response.data)).blob()
})
]);
toast.success('Image copied to clipboard'); toast.success('Image copied to clipboard');
} catch (error) { } catch (error) {
console.error('Error downloading image:', error); console.error('Error downloading image:', error);
new Noty({ new Noty({ type: 'error', text: escapeTag(`Failed to download image. ${url}`) }).show();
type: 'error',
text: escapeTag(`Failed to download image. ${url}`)
}).show();
} finally { } finally {
toast.dismiss(msg); toast.dismiss(msg);
} }
} }
async function downloadAndSaveImage(url, fileName) { async function downloadAndSaveImage(url, fileName) {
if (!url) { if (!url) return;
return;
}
const msg = toast.info('Downloading image...'); const msg = toast.info('Downloading image...');
try { try {
const response = await webApiService.execute({ const response = await webApiService.execute({ url, method: 'GET' });
url, if (response.status !== 200 || !String(response.data).startsWith('data:image/png')) {
method: 'GET'
});
if (response.status !== 200 || !response.data.startsWith('data:image/png')) {
throw new Error(`Error: ${response.data}`); throw new Error(`Error: ${response.data}`);
} }
const link = document.createElement('a'); const link = document.createElement('a');
link.href = response.data; link.href = response.data;
const fileId = extractFileId(url); const fileId = extractFileId(url);
if (!fileName && fileId) { let name = fileName;
fileName = `${fileId}.png`; if (!name && fileId) name = `${fileId}.png`;
} if (!name) name = `${url.split('/').pop()}.png`;
if (!fileName) { if (!name) name = 'image.png';
fileName = `${url.split('/').pop()}.png`;
} link.setAttribute('download', name);
if (!fileName) {
fileName = 'image.png';
}
link.setAttribute('download', fileName);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
} catch (error) { } catch (error) {
console.error('Error downloading image:', error); console.error('Error downloading image:', error);
new Noty({ new Noty({ type: 'error', text: escapeTag(`Failed to download image. ${url}`) }).show();
type: 'error',
text: escapeTag(`Failed to download image. ${url}`)
}).show();
} finally { } finally {
toast.dismiss(msg); toast.dismiss(msg);
} }
@@ -156,27 +334,12 @@
</script> </script>
<style scoped> <style scoped>
.toolbar-icon:hover { .x-viewer-img {
opacity: 1; will-change: transform;
cursor: grab;
user-select: none;
} }
.toolbar-icon { .x-viewer-img:active {
cursor: pointer; cursor: grabbing;
opacity: 0.8;
}
.fullscreen-image-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
box-sizing: border-box;
}
.fullscreen-image {
max-width: 100%;
max-height: 100%;
}
:deep(.el-image__preview) {
display: none;
} }
</style> </style>
-246
View File
@@ -1,246 +0,0 @@
<template>
<Popover v-model:open="visible">
<PopoverTrigger asChild>
<Button>
{{ t('nav_menu.icon_picker.pick_icon') }}
</Button>
</PopoverTrigger>
<PopoverContent side="bottom" align="start" class="w-155">
<div class="icon-picker">
<InputGroupSearch
v-model="search"
class="icon-picker__search"
:placeholder="t('nav_menu.icon_picker.search_placeholder')" />
<el-scrollbar v-if="filteredCategories.length" height="600px" class="icon-picker__scroll">
<div v-for="category in filteredCategories" :key="category.name" class="icon-picker__category">
<div class="icon-picker__category-title">
{{ category.name }}
</div>
<div class="icon-picker__grid">
<div v-for="group in category.groups" :key="group.id" class="icon-picker__group">
<div class="icon-picker__group-label">
{{ group.label }}
</div>
<div class="icon-picker__variants">
<button
v-for="variant in group.variants"
:key="variant.className"
type="button"
class="icon-picker__variant"
:class="{ 'is-active': variant.className === modelValue }"
:title="group.tooltip"
@click="handleSelect(variant.className)">
<i :class="[variant.className, 'ri-2x']"></i>
</button>
</div>
</div>
</div>
</div>
</el-scrollbar>
<div v-else class="icon-picker__empty">{{ t('nav_menu.icon_picker.no_icon_found') }}</div>
</div>
</PopoverContent>
</Popover>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupSearch } from '@/components/ui/input-group';
import { useI18n } from 'vue-i18n';
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
import remixIconTags from '../shared/constants/remixIconTags.json';
const { t } = useI18n();
defineProps({
modelValue: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:modelValue']);
const visible = ref(false);
const search = ref('');
const parseTags = (tagsText) =>
typeof tagsText === 'string'
? tagsText
.split(',')
.map((tag) => tag.trim())
.filter(Boolean)
: [];
const formatLabel = (baseName) =>
baseName
.split('-')
.filter(Boolean)
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join(' ');
const createGroup = (categoryName, baseName, tagsText) => {
const normalizedTags = parseTags(tagsText);
const label = formatLabel(baseName);
const variants = ['line', 'fill'].map((variant) => ({
className: `ri-${baseName}-${variant}`,
variant
}));
const searchText = [
baseName,
label,
...baseName.split('-'),
...normalizedTags,
...variants.map((variant) => variant.className),
'line',
'fill'
]
.join(' ')
.toLowerCase();
return {
id: `${categoryName}-${baseName}`,
label,
tooltip: normalizedTags.length ? `${label}${normalizedTags.join(', ')}` : label,
variants,
searchable: searchText
};
};
const categories = computed(() =>
Object.entries(remixIconTags)
.filter(([key]) => key !== '_comment')
.map(([name, icons]) => ({
name,
groups: Object.entries(icons || {}).map(([baseName, tags]) => createGroup(name, baseName, tags))
}))
);
const filteredCategories = computed(() => {
const query = search.value.trim().toLowerCase();
if (!query) {
return categories.value;
}
return categories.value
.map((category) => ({
name: category.name,
groups: category.groups.filter((group) => group.searchable.includes(query))
}))
.filter((category) => category.groups.length > 0);
});
const handleSelect = (className) => {
emit('update:modelValue', className);
visible.value = false;
};
watch(visible, (nextVisible) => {
if (!nextVisible) {
search.value = '';
}
});
</script>
<style scoped>
.icon-picker__trigger i {
font-size: 16px;
}
.icon-picker {
display: flex;
flex-direction: column;
gap: 8px;
height: 600px;
width: 100%;
}
.icon-picker__search {
flex-shrink: 0;
}
.icon-picker__scroll {
padding-right: 6px;
}
.icon-picker__category {
margin-bottom: 12px;
}
.icon-picker__category-title {
font-weight: 600;
margin-bottom: 6px;
}
.icon-picker__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.icon-picker__group {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 4px;
align-items: center;
}
.icon-picker__group-label {
font-size: 12px;
font-weight: 600;
text-align: center;
color: var(--el-text-color-primary);
}
.icon-picker__variants {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}
.icon-picker__variant {
border: 1px solid transparent;
background: transparent;
cursor: pointer;
width: 84px;
height: 84px;
border-radius: 10px;
color: var(--el-text-color-regular);
transition:
color 0.2s ease,
background 0.2s ease,
transform 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.icon-picker__variant i {
color: inherit;
}
.icon-picker__variant:hover {
border-color: var(--el-color-primary);
background: var(--el-fill-color-light);
transform: translateY(-1px);
}
.icon-picker__variant.is-active {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
border-color: var(--el-color-primary);
}
.icon-picker__empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
</style>
+3 -3
View File
@@ -2,11 +2,11 @@
<div v-if="isVisible" class="inline-block"> <div v-if="isVisible" class="inline-block">
<TooltipWrapper side="top" :content="t('dialog.user.info.launch_invite_tooltip')" <TooltipWrapper side="top" :content="t('dialog.user.info.launch_invite_tooltip')"
><Button ><Button
class="rounded-full w-6 h-6 text-muted-foreground hover:text-foreground" class="rounded-full w-6 h-6 text-xs text-muted-foreground hover:text-foreground"
size="icon-sm" size="icon-sm"
variant="outline" variant="outline"
@click="confirm" @click="confirm"
><Star /> ><LogIn />
</Button> </Button>
</TooltipWrapper> </TooltipWrapper>
</div> </div>
@@ -14,7 +14,7 @@
<script setup> <script setup>
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Star } from 'lucide-vue-next'; import { LogIn } from 'lucide-vue-next';
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
+28 -1
View File
@@ -3,6 +3,25 @@
<div v-if="!text" class="transparent">-</div> <div v-if="!text" class="transparent">-</div>
<div v-show="text" class="flex items-center"> <div v-show="text" class="flex items-center">
<div v-if="region" :class="['flags', 'mr-1.5', region]"></div> <div v-if="region" :class="['flags', 'mr-1.5', region]"></div>
<template v-if="disableTooltip">
<div
:class="['x-location', { 'x-link': link && location !== 'private' && location !== 'offline' }]"
class="inline-flex min-w-0 flex-nowrap items-center overflow-hidden"
@click="handleShowWorldDialog">
<Loader2 :class="['is-loading']" class="mr-1" v-if="isTraveling" />
<span class="min-w-0 truncate">{{ text }}</span>
<span v-if="showInstanceIdInLocation && instanceName" class="ml-1 whitespace-nowrap">{{
` · #${instanceName}`
}}</span>
<span v-if="groupName" class="ml-0.5 whitespace-nowrap x-link" @click.stop="handleShowGroupDialog">
({{ groupName }})
</span>
</div>
<AlertTriangle v-if="isClosed" :class="['inline-block', 'ml-5']" style="color: lightcoral" />
</template>
<template v-else>
<TooltipWrapper <TooltipWrapper
:content="`${t('dialog.new_instance.instance_id')}: #${instanceName}`" :content="`${t('dialog.new_instance.instance_id')}: #${instanceName}`"
:disabled="!instanceName || showInstanceIdInLocation" :disabled="!instanceName || showInstanceIdInLocation"
@@ -17,7 +36,10 @@
<span v-if="showInstanceIdInLocation && instanceName" class="ml-1 whitespace-nowrap">{{ <span v-if="showInstanceIdInLocation && instanceName" class="ml-1 whitespace-nowrap">{{
` · #${instanceName}` ` · #${instanceName}`
}}</span> }}</span>
<span v-if="groupName" class="ml-0.5 whitespace-nowrap x-link" @click.stop="handleShowGroupDialog"> <span
v-if="groupName"
class="ml-0.5 whitespace-nowrap x-link"
@click.stop="handleShowGroupDialog">
({{ groupName }}) ({{ groupName }})
</span> </span>
</div> </div>
@@ -25,6 +47,7 @@
<TooltipWrapper v-if="isClosed" :content="t('dialog.user.info.instance_closed')"> <TooltipWrapper v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
<AlertTriangle :class="['inline-block', 'ml-5']" style="color: lightcoral" /> <AlertTriangle :class="['inline-block', 'ml-5']" style="color: lightcoral" />
</TooltipWrapper> </TooltipWrapper>
</template>
<Lock v-if="strict" :class="['inline-block', 'ml-5']" /> <Lock v-if="strict" :class="['inline-block', 'ml-5']" />
</div> </div>
</div> </div>
@@ -71,6 +94,10 @@
type: Boolean, type: Boolean,
default: true default: true
}, },
disableTooltip: {
type: Boolean,
default: false
},
isOpenPreviousInstanceInfoDialog: { isOpenPreviousInstanceInfoDialog: {
type: Boolean, type: Boolean,
default: false default: false
+1 -1
View File
@@ -14,7 +14,7 @@
</template> </template>
<script setup> <script setup>
import { Lock, Unlock, AlertTriangle } from 'lucide-vue-next'; import { AlertTriangle, Lock, Unlock } from 'lucide-vue-next';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
+1 -1
View File
@@ -239,13 +239,13 @@
<CustomNavDialog <CustomNavDialog
v-model:visible="customNavDialogVisible" v-model:visible="customNavDialogVisible"
:layout="navLayout" :layout="navLayout"
:default-folder-icon="DEFAULT_FOLDER_ICON"
@save="handleCustomNavSave" @save="handleCustomNavSave"
@reset="handleCustomNavReset" /> @reset="handleCustomNavReset" />
</template> </template>
<script setup> <script setup>
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue'; import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
import { ElMenu, ElMenuItem, ElPopover, ElSubMenu } from 'element-plus';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { dayjs } from 'element-plus'; import { dayjs } from 'element-plus';
+100
View File
@@ -0,0 +1,100 @@
<script setup>
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { computed } from 'vue';
const props = defineProps({
modelValue: { type: String, default: '' },
presets: {
type: Array,
default: () => []
},
disabled: { type: Boolean, default: false },
clearable: { type: Boolean, default: false },
emptyValue: { type: String, default: '' },
cols: { type: Number, default: 6 }
});
const emit = defineEmits(['update:modelValue', 'change']);
function normalizeHex(v) {
const s = String(v || '')
.trim()
.toLowerCase();
if (/^#[0-9a-f]{6}$/.test(s)) return s;
return '#ffffff';
}
const safeValue = computed(() => normalizeHex(props.modelValue));
const displayText = computed(() => (props.modelValue ? String(props.modelValue) : props.emptyValue));
function setColor(color) {
if (props.disabled) return;
emit('update:modelValue', color);
emit('change', color);
}
function onInput(e) {
if (props.disabled) return;
const v = e?.target?.value;
setColor(String(v || ''));
}
function clear() {
if (props.disabled || !props.clearable) return;
emit('update:modelValue', props.emptyValue);
emit('change', props.emptyValue);
}
const gridStyle = computed(() => ({
gridTemplateColumns: `repeat(${Math.max(1, props.cols)}, minmax(0, 1fr))`
}));
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" size="sm" class="flex items-center gap-2 px-2" :disabled="disabled">
<span class="h-4 w-4 rounded border" :style="{ backgroundColor: safeValue }" />
<span class="font-mono text-xs opacity-80">
{{ displayText }}
</span>
<span v-if="clearable && modelValue" class="ml-1 opacity-60"></span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-56 p-3">
<div class="mb-3 grid gap-2" :style="gridStyle">
<button
v-for="color in presets"
:key="color"
type="button"
class="h-6 w-6 rounded border"
:style="{ backgroundColor: color }"
:disabled="disabled"
:aria-disabled="disabled ? 'true' : 'false'"
:class="[
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
safeValue === String(color).toLowerCase() ? 'ring-2 ring-offset-2' : ''
]"
@click="setColor(color)" />
</div>
<input
type="color"
class="h-8 w-full cursor-pointer border-none bg-transparent p-0"
:value="safeValue"
:disabled="disabled"
@input="onInput" />
<div v-if="clearable" class="mt-3 flex justify-end">
<Button variant="ghost" size="sm" :disabled="disabled" @click="clear"> Clear </Button>
</div>
</PopoverContent>
</Popover>
</template>
@@ -12,8 +12,10 @@
style="width: 100%; white-space: initial" style="width: 100%; white-space: initial"
class="my-1" class="my-1"
@click="deleteFavoriteNoConfirm(favoriteDialog.objectId)"> @click="deleteFavoriteNoConfirm(favoriteDialog.objectId)">
<Check />{{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} / <Check />{{ favoriteDialog.currentGroup.displayName }} ({{
{{ favoriteDialog.currentGroup.capacity }}) favoriteDialog.currentGroup.count
}}
/ {{ favoriteDialog.currentGroup.capacity }})
</Button> </Button>
</template> </template>
<template v-else> <template v-else>
@@ -76,10 +78,10 @@
</template> </template>
<script setup> <script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Check } from 'lucide-vue-next'; import { Check } from 'lucide-vue-next';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
+63 -20
View File
@@ -39,10 +39,15 @@
<div class="custom-nav-entry__folder-header"> <div class="custom-nav-entry__folder-header">
<div class="custom-nav-entry__info"> <div class="custom-nav-entry__info">
<i :class="entry.icon || defaultFolderIcon"></i> <i :class="entry.icon || defaultFolderIcon"></i>
<span>{{ entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder') }}</span> <span>{{
entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder')
}}</span>
</div> </div>
<div class="custom-nav-entry__actions"> <div class="custom-nav-entry__actions">
<Button size="icon-sm w-6 h-6 text-xs" variant="outline" @click="openFolderEditor(index)"> <Button
size="icon-sm w-6 h-6 text-xs"
variant="outline"
@click="openFolderEditor(index)">
<i class="ri-edit-box-line"></i> <i class="ri-edit-box-line"></i>
{{ t('nav_menu.custom_nav.edit_folder') }} {{ t('nav_menu.custom_nav.edit_folder') }}
</Button> </Button>
@@ -83,11 +88,11 @@
</template> </template>
</div> </div>
</div> </div>
<el-alert <!-- <el-alert
v-if="invalidFolders.length" v-if="invalidFolders.length"
type="warning" type="warning"
:closable="false" :closable="false"
:title="t('nav_menu.custom_nav.invalid_folder')" /> :title="t('nav_menu.custom_nav.invalid_folder')" /> -->
<DialogFooter> <DialogFooter>
<div class="custom-nav-dialog__footer"> <div class="custom-nav-dialog__footer">
<div class="custom-nav-dialog__footer-left"> <div class="custom-nav-dialog__footer-left">
@@ -112,7 +117,7 @@
</Dialog> </Dialog>
<Dialog v-model:open="folderEditor.visible"> <Dialog v-model:open="folderEditor.visible">
<DialogContent class="folder-editor-dialog"> <DialogContent class="folder-editor-dialog sm:max-w-[50vw]">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{{ {{
@@ -125,9 +130,40 @@
<div class="folder-editor"> <div class="folder-editor">
<div class="folder-editor__form"> <div class="folder-editor__form">
<InputGroupField <InputGroupField
class="col-span-2"
v-model="folderEditor.data.name" v-model="folderEditor.data.name"
:placeholder="t('nav_menu.custom_nav.folder_name_placeholder')" /> :placeholder="t('nav_menu.custom_nav.folder_name_placeholder')" />
<IconPicker v-model="folderEditor.data.icon" class="folder-editor__icon-picker" /> <InputGroupField
class="col-span-2"
v-model="folderEditor.data.icon"
:placeholder="t('nav_menu.custom_nav.folder_icon_placeholder')">
<template #trailing>
<HoverCard>
<HoverCardTrigger as-child>
<InputGroupButton
size="icon-xs"
:aria-label="t('nav_menu.custom_nav.folder_icon_placeholder')">
<LinkIcon class="size-3.5" />
</InputGroupButton>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="end" class="w-80">
<div class="text-sm leading-snug">
<div>
Find the icon you want on this site and paste its class name here, e.g.
<span class="font-mono">ri-arrow-left-up-line</span>
</div>
<div class="mt-2">
<a
class="x-link"
@click.prevent="openExternalLink('https://remixicon.com/')">
https://remixicon.com/
</a>
</div>
</div>
</HoverCardContent>
</HoverCard>
</template>
</InputGroupField>
</div> </div>
<div class="folder-editor__lists"> <div class="folder-editor__lists">
<div class="folder-editor__column"> <div class="folder-editor__column">
@@ -137,8 +173,11 @@
<div v-if="!folderEditorAvailableItems.length" class="folder-editor__empty"> <div v-if="!folderEditorAvailableItems.length" class="folder-editor__empty">
{{ t('nav_menu.custom_nav.folder_empty') }} {{ t('nav_menu.custom_nav.folder_empty') }}
</div> </div>
<el-scrollbar v-else always class="folder-editor__scroll"> <ScrollArea v-else type="always" class="folder-editor__scroll">
<div v-for="item in folderEditorAvailableItems" :key="item.key" class="folder-editor__option"> <div
v-for="item in folderEditorAvailableItems"
:key="item.key"
class="folder-editor__option">
<label class="folder-editor__option-label"> <label class="folder-editor__option-label">
<Checkbox <Checkbox
:model-value="folderEditor.data.items.includes(item.key)" :model-value="folderEditor.data.items.includes(item.key)"
@@ -149,7 +188,7 @@
</span> </span>
</label> </label>
</div> </div>
</el-scrollbar> </ScrollArea>
</div> </div>
<div class="folder-editor__column folder-editor__column--selected"> <div class="folder-editor__column folder-editor__column--selected">
<div class="folder-editor__column-title"> <div class="folder-editor__column-title">
@@ -216,19 +255,23 @@
</template> </template>
<script setup> <script setup>
import { computed, reactive, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, reactive, ref, watch } from 'vue';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { Button } from '@/components/ui/button';
import { Link as LinkIcon } from 'lucide-vue-next';
import { openExternalLink } from '@/shared/utils/common';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { InputGroupButton, InputGroupField } from '../ui/input-group';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { Checkbox } from '../ui/checkbox'; import { Checkbox } from '../ui/checkbox';
import { InputGroupField } from '../ui/input-group'; import { ScrollArea } from '../ui/scroll-area';
import { navDefinitions } from '../../shared/constants/ui.js'; import { navDefinitions } from '../../shared/constants/ui.js';
import IconPicker from '../IconPicker.vue'; // import IconPicker from '../IconPicker.vue';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -240,8 +283,7 @@
default: () => [] default: () => []
}, },
defaultFolderIcon: { defaultFolderIcon: {
type: String, type: String
default: 'ri-menu-fold-line'
} }
}); });
@@ -258,7 +300,7 @@
type: 'folder', type: 'folder',
id: entry.id, id: entry.id,
name: entry.name, name: entry.name,
icon: entry.icon || props.defaultFolderIcon, icon: entry.icon,
items: Array.isArray(entry.items) ? [...entry.items] : [] items: Array.isArray(entry.items) ? [...entry.items] : []
}; };
} }
@@ -358,14 +400,14 @@
folderEditor.data = { folderEditor.data = {
id: entry.id, id: entry.id,
name: entry.name, name: entry.name,
icon: entry.icon || props.defaultFolderIcon, icon: entry.icon,
items: Array.isArray(entry.items) ? [...entry.items] : [] items: Array.isArray(entry.items) ? [...entry.items] : []
}; };
} else { } else {
folderEditor.data = { folderEditor.data = {
id: `custom-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 7)}`, id: `custom-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 7)}`,
name: '', name: '',
icon: props.defaultFolderIcon, icon: '',
items: [] items: []
}; };
} }
@@ -399,6 +441,7 @@
const applyFolderChanges = () => { const applyFolderChanges = () => {
const sanitizedItems = folderEditor.data.items.filter((key) => definitionsMap.value.has(key)); const sanitizedItems = folderEditor.data.items.filter((key) => definitionsMap.value.has(key));
const sanitizedIcon = folderEditor.data.icon?.trim() || '';
const entries = [...localLayout.value]; const entries = [...localLayout.value];
if (folderEditor.isEditing) { if (folderEditor.isEditing) {
@@ -421,7 +464,7 @@
type: 'folder', type: 'folder',
id: folderEditor.data.id, id: folderEditor.data.id,
name: folderEditor.data.name.trim(), name: folderEditor.data.name.trim(),
icon: folderEditor.data.icon || props.defaultFolderIcon, icon: sanitizedIcon,
items: sanitizedItems items: sanitizedItems
}); });
@@ -442,7 +485,7 @@
type: 'folder', type: 'folder',
id: folderEditor.data.id, id: folderEditor.data.id,
name: folderEditor.data.name.trim(), name: folderEditor.data.name.trim(),
icon: folderEditor.data.icon || props.defaultFolderIcon, icon: sanitizedIcon,
items: sanitizedItems items: sanitizedItems
}); });
+1 -1
View File
@@ -80,9 +80,9 @@
</template> </template>
<script setup> <script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, watch } from 'vue'; import { computed, watch } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Check as CheckIcon } from 'lucide-vue-next'; import { Check as CheckIcon } from 'lucide-vue-next';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
+9 -2
View File
@@ -129,8 +129,14 @@
</template> </template>
<script setup> <script setup>
import { computed, onBeforeUnmount, ref, watch } from 'vue'; import {
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -138,6 +144,7 @@
DropdownMenuTrigger DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field'; import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { AlertTriangle, Copy, MoreHorizontal } from 'lucide-vue-next'; import { AlertTriangle, Copy, MoreHorizontal } from 'lucide-vue-next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ButtonGroup } from '@/components/ui/button-group'; import { ButtonGroup } from '@/components/ui/button-group';
@@ -54,9 +54,9 @@
</template> </template>
<script setup> <script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, watch } from 'vue'; import { computed, watch } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
+25 -6
View File
@@ -66,14 +66,20 @@
<ToggleGroupItem <ToggleGroupItem
value="members" value="members"
:disabled=" :disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create') !hasGroupPermission(
newInstanceDialog.groupRef,
'group-instance-open-create'
)
" "
>{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem >{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem
> >
<ToggleGroupItem <ToggleGroupItem
value="plus" value="plus"
:disabled=" :disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create') !hasGroupPermission(
newInstanceDialog.groupRef,
'group-instance-plus-create'
)
" "
>{{ t('dialog.new_instance.group_access_type_plus') }}</ToggleGroupItem >{{ t('dialog.new_instance.group_access_type_plus') }}</ToggleGroupItem
> >
@@ -132,7 +138,10 @@
<Checkbox <Checkbox
v-model="newInstanceDialog.ageGate" v-model="newInstanceDialog.ageGate"
:disabled=" :disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-age-gated-create') !hasGroupPermission(
newInstanceDialog.groupRef,
'group-instance-age-gated-create'
)
" "
@update:modelValue="buildInstance" /> @update:modelValue="buildInstance" />
</FieldContent> </FieldContent>
@@ -177,14 +186,17 @@
</Field> </Field>
<Field <Field
v-if=" v-if="
newInstanceDialog.accessType === 'group' && newInstanceDialog.groupAccessType === 'members' newInstanceDialog.accessType === 'group' &&
newInstanceDialog.groupAccessType === 'members'
" "
class="items-start"> class="items-start">
<FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel> <FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel>
<FieldContent> <FieldContent>
<Select <Select
multiple multiple
:model-value="Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []" :model-value="
Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []
"
@update:modelValue="handleRoleIdsChange"> @update:modelValue="handleRoleIdsChange">
<SelectTrigger size="sm" class="w-full"> <SelectTrigger size="sm" class="w-full">
<SelectValue> <SelectValue>
@@ -514,8 +526,15 @@
</template> </template>
<script setup> <script setup>
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field'; import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Check as CheckIcon } from 'lucide-vue-next'; import { Check as CheckIcon } from 'lucide-vue-next';
+1 -1
View File
@@ -73,10 +73,10 @@
</template> </template>
<script setup> <script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Check as CheckIcon } from 'lucide-vue-next'; import { Check as CheckIcon } from 'lucide-vue-next';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -64,8 +64,7 @@
</template> </template>
<Location <Location
:location="userDialog.ref.location" :location="userDialog.ref.location"
:traveling="userDialog.ref.travelingToLocation" :traveling="userDialog.ref.travelingToLocation" />
style="display: block; margin-top: 5px" />
</div> </div>
<div class="x-friend-list" style="flex: 1; margin-top: 10px; max-height: 150px"> <div class="x-friend-list" style="flex: 1; margin-top: 10px; max-height: 150px">
<div <div
+1 -1
View File
@@ -81,10 +81,10 @@
</template> </template>
<script setup> <script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field'; import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { AlertCircle } from 'lucide-vue-next'; import { AlertCircle } from 'lucide-vue-next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
@@ -56,13 +56,13 @@
<AlertDialogPortal :to="portalTo"> <AlertDialogPortal :to="portalTo">
<AlertDialogOverlay <AlertDialogOverlay
data-slot="alert-dialog-overlay" data-slot="alert-dialog-overlay"
class="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" /> class="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-11000 bg-black/80" />
<AlertDialogContent <AlertDialogContent
data-slot="alert-dialog-content" data-slot="alert-dialog-content"
v-bind="{ ...$attrs, ...forwarded }" v-bind="{ ...$attrs, ...forwarded }"
:class=" :class="
cn( 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', '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-11000 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 props.class
) )
"> ">
@@ -51,7 +51,7 @@
v-bind="{ ...$attrs, ...forwarded }" v-bind="{ ...$attrs, ...forwarded }"
:class=" :class="
cn( 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', '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-12000 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 props.class
) )
"> ">
@@ -46,7 +46,7 @@
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( 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-32 origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', '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-12000 min-w-32 origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
props.class props.class
) )
"> ">
@@ -43,7 +43,7 @@
v-bind="{ ...$attrs, ...forwardedProps }" v-bind="{ ...$attrs, ...forwardedProps }"
:class=" :class="
cn( 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-64 rounded-md border p-4 shadow-md outline-hidden', '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-12000 w-64 rounded-md border p-4 shadow-md outline-hidden',
props.class props.class
) )
"> ">
+1 -1
View File
@@ -52,7 +52,7 @@
v-bind="{ ...$attrs, ...forwarded }" v-bind="{ ...$attrs, ...forwarded }"
:class=" :class="
cn( 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', '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-12000 w-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
props.class props.class
) )
"> ">
@@ -0,0 +1,44 @@
<script setup>
defineOptions({ inheritAttrs: false });
import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport } from 'reka-ui';
import { ref, useAttrs } from 'vue';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
import ScrollBar from './ScrollBar.vue';
const props = defineProps({
type: { type: null, required: false },
dir: { type: null, required: false },
scrollHideDelay: { type: Number, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const attrs = useAttrs();
const viewportEl = ref(null);
function setViewportEl(el) {
viewportEl.value = el?.$el ?? el ?? null;
}
defineExpose({ viewportEl, update: () => {} });
</script>
<template>
<ScrollAreaRoot data-slot="scroll-area" v-bind="delegatedProps" :class="cn('relative', props.class)">
<ScrollAreaViewport
data-slot="scroll-area-viewport"
:ref="setViewportEl"
v-bind="attrs"
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1">
<slot />
</ScrollAreaViewport>
<ScrollBar />
<ScrollAreaCorner />
</ScrollAreaRoot>
</template>
@@ -0,0 +1,37 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
orientation: { type: String, required: false, default: "vertical" },
forceMount: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
</script>
<template>
<ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
v-bind="delegatedProps"
:class="
cn(
'flex touch-none p-px transition-colors select-none',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent',
props.class,
)
"
>
<ScrollAreaThumb
data-slot="scroll-area-thumb"
class="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaScrollbar>
</template>
+2
View File
@@ -0,0 +1,2 @@
export { default as ScrollArea } from "./ScrollArea.vue";
export { default as ScrollBar } from "./ScrollBar.vue";
+1 -1
View File
@@ -49,7 +49,7 @@
v-bind="{ ...$attrs, ...forwarded }" v-bind="{ ...$attrs, ...forwarded }"
:class=" :class="
cn( 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 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md', '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 relative z-12000 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper' && position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class props.class
+17 -10
View File
@@ -20,17 +20,20 @@
ariaLabel: { type: String, default: '' }, ariaLabel: { type: String, default: '' },
variant: { type: String, default: 'fit' }, variant: { type: String, default: 'fit' },
unmountOnHide: { type: Boolean, default: false } unmountOnHide: { type: Boolean, default: false },
fill: { type: Boolean, default: false }
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const { modelValue, defaultValue, items, ariaLabel, variant, unmountOnHide } = toRefs(props); const { modelValue, defaultValue, items, ariaLabel, variant, unmountOnHide, fill } = toRefs(props);
const itemsList = computed(() => (Array.isArray(items.value) ? items.value : []));
const resolvedDefault = computed(() => { const resolvedDefault = computed(() => {
return defaultValue.value ?? items.value?.[0]?.value; return defaultValue.value ?? itemsList.value?.[0]?.value;
}); });
const isValueValid = (value) => items.value?.some((item) => item?.value === value); const isValueValid = (value) => itemsList.value.some((item) => item?.value === value);
const innerValue = ref(isValueValid(modelValue.value) ? modelValue.value : resolvedDefault.value); const innerValue = ref(isValueValid(modelValue.value) ? modelValue.value : resolvedDefault.value);
@@ -40,12 +43,13 @@
} }
}); });
watch([items, defaultValue], () => { watch([itemsList, defaultValue], () => {
if (!isValueValid(innerValue.value)) { if (!isValueValid(innerValue.value)) {
innerValue.value = resolvedDefault.value; innerValue.value = resolvedDefault.value;
return; return;
} }
if (!isValueValid(modelValue.value)) {
if (modelValue.value !== undefined && modelValue.value !== null && !isValueValid(modelValue.value)) {
innerValue.value = resolvedDefault.value; innerValue.value = resolvedDefault.value;
} }
}); });
@@ -79,7 +83,7 @@
<TabsRoot <TabsRoot
:model-value="innerValue" :model-value="innerValue"
:default-value="resolvedDefault" :default-value="resolvedDefault"
class="w-full" :class="['w-full', fill ? 'flex min-h-0 flex-col' : '']"
:unmount-on-hide="unmountOnHide" :unmount-on-hide="unmountOnHide"
@update:modelValue="onValueChange"> @update:modelValue="onValueChange">
<TabsList :class="listClass" :aria-label="ariaLabel || undefined"> <TabsList :class="listClass" :aria-label="ariaLabel || undefined">
@@ -89,7 +93,7 @@
</TabsIndicator> </TabsIndicator>
<TabsTrigger <TabsTrigger
v-for="it in items" v-for="it in itemsList"
:key="it.value" :key="it.value"
:value="it.value" :value="it.value"
:disabled="it.disabled" :disabled="it.disabled"
@@ -99,10 +103,13 @@
</TabsList> </TabsList>
<TabsContent <TabsContent
v-for="it in items" v-for="it in itemsList"
:key="it.value" :key="it.value"
:value="it.value" :value="it.value"
class="pt-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background"> :class="[
'pt-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background',
fill ? 'min-h-0 flex-1' : ''
]">
<slot :name="it.value" /> <slot :name="it.value" />
</TabsContent> </TabsContent>
</TabsRoot> </TabsRoot>
+2 -2
View File
@@ -40,14 +40,14 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="{ ...forwarded, ...$attrs }" v-bind="{ ...forwarded, ...$attrs }"
:class=" :class="
cn( cn(
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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-fit rounded-md px-3 py-1.5 text-xs text-balance', 'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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-12000 w-fit rounded-md px-3 py-1.5 text-xs text-balance',
props.class props.class
) )
"> ">
<slot /> <slot />
<TooltipArrow <TooltipArrow
class="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]" /> class="bg-foreground fill-foreground z-12000 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]" />
</TooltipContent> </TooltipContent>
</TooltipPortal> </TooltipPortal>
</template> </template>
+6 -1
View File
@@ -1,7 +1,7 @@
const MODAL_PORTAL_ROOT_ID = 'vrcx-modal-portal-root'; const MODAL_PORTAL_ROOT_ID = 'vrcx-modal-portal-root';
const APP_PORTAL_ROOT_ID = 'x-dialog-portal'; const APP_PORTAL_ROOT_ID = 'x-dialog-portal';
const BASE_Z_INDEX = 50; const BASE_Z_INDEX = 10000;
const Z_STEP = 10; const Z_STEP = 10;
let nextLayerIndex = 0; let nextLayerIndex = 0;
@@ -15,11 +15,15 @@ function ensureModalPortalRoot() {
if (root) { if (root) {
root.style.position ||= 'relative'; root.style.position ||= 'relative';
root.style.isolation ||= 'isolate'; root.style.isolation ||= 'isolate';
root.style.zIndex ||= String(BASE_Z_INDEX);
return root; return root;
} }
root = document.getElementById(MODAL_PORTAL_ROOT_ID); root = document.getElementById(MODAL_PORTAL_ROOT_ID);
if (root) { if (root) {
root.style.position ||= 'relative';
root.style.isolation ||= 'isolate';
root.style.zIndex ||= String(BASE_Z_INDEX);
return root; return root;
} }
@@ -27,6 +31,7 @@ function ensureModalPortalRoot() {
root.id = MODAL_PORTAL_ROOT_ID; root.id = MODAL_PORTAL_ROOT_ID;
root.style.position = 'relative'; root.style.position = 'relative';
root.style.isolation = 'isolate'; root.style.isolation = 'isolate';
root.style.zIndex = String(BASE_Z_INDEX);
document.body.appendChild(root); document.body.appendChild(root);
return root; return root;
} }
+1 -28
View File
@@ -1,27 +1,3 @@
const elementPlusStrings = {
// Vite does not support dynamic imports to `node_modules`.
// https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
cs: () => import('element-plus/es/locale/lang/cs'),
en: () => import('element-plus/es/locale/lang/en'),
es: () => import('element-plus/es/locale/lang/es'),
fr: () => import('element-plus/es/locale/lang/fr'),
hu: () => import('element-plus/es/locale/lang/hu'),
ja: () => import('element-plus/es/locale/lang/ja'),
ko: () => import('element-plus/es/locale/lang/ko'),
pl: () => import('element-plus/es/locale/lang/pl'),
pt: () => import('element-plus/es/locale/lang/pt'),
ru: () => import('element-plus/es/locale/lang/ru'),
th: () => import('element-plus/es/locale/lang/th'),
vi: () => import('element-plus/es/locale/lang/vi'),
'zh-CN': () => import('element-plus/es/locale/lang/zh-cn'),
'zh-TW': () => import('element-plus/es/locale/lang/zh-tw')
};
async function getElementPlusStrings(code) {
const loader = elementPlusStrings[code] || elementPlusStrings.en;
return (await loader().catch(() => elementPlusStrings.en())).default;
}
const localizedStringsUrls = import.meta.glob('./*.json', { const localizedStringsUrls = import.meta.glob('./*.json', {
eager: true, eager: true,
query: '?url', query: '?url',
@@ -50,10 +26,7 @@ async function getLocalizedStrings(code) {
} }
} }
return { return localizedStrings;
...localizedStrings,
elementPlus: await getElementPlusStrings(code)
};
} }
const languageNames = import.meta.glob('./*.json', { const languageNames = import.meta.glob('./*.json', {
+5 -3
View File
@@ -403,13 +403,15 @@ function openExternalLink(link) {
confirmText: 'Open', confirmText: 'Open',
cancelText: 'Copy' cancelText: 'Copy'
}) })
// TODO: beforeClose alert dialog .then(({ ok, reason }) => {
.then(({ ok }) => { if (reason === 'cancel') {
if (!ok) {
copyToClipboard(link, 'Link copied to clipboard!'); copyToClipboard(link, 'Link copied to clipboard!');
return; return;
} }
if (ok) {
AppApi.OpenLink(link); AppApi.OpenLink(link);
return;
}
}); });
} }
+3 -1
View File
@@ -8,7 +8,7 @@
<MutualFriends /> <MutualFriends />
</template> </template>
</TabsUnderline> </TabsUnderline>
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop> <BackToTop target="#chart" :right="30" :bottom="30" />
</div> </div>
</template> </template>
@@ -18,6 +18,8 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BackToTop from '@/components/BackToTop.vue';
import { useChartsStore } from '../../stores'; import { useChartsStore } from '../../stores';
const InstanceActivity = defineAsyncComponent(() => import('./components/InstanceActivity.vue')); const InstanceActivity = defineAsyncComponent(() => import('./components/InstanceActivity.vue'));
@@ -1,5 +1,6 @@
<template> <template>
<div ref="instanceActivityRef" class="pt-12"> <div ref="instanceActivityRef" class="pt-12">
<BackToTop :target="instanceActivityRef" :right="30" :bottom="30" :teleport="false" />
<div class="options-container instance-activity" style="margin-top: 0"> <div class="options-container instance-activity" style="margin-top: 0">
<div> <div>
<span>{{ t('view.charts.instance_activity.header') }}</span> <span>{{ t('view.charts.instance_activity.header') }}</span>
@@ -171,6 +172,7 @@
import { toDate } from 'reka-ui/date'; import { toDate } from 'reka-ui/date';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BackToTop from '@/components/BackToTop.vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Popover, PopoverContent, PopoverTrigger } from '../../../components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '../../../components/ui/popover';
@@ -204,7 +206,7 @@
function setInstanceActivityHeight() { function setInstanceActivityHeight() {
if (instanceActivityRef.value) { if (instanceActivityRef.value) {
const availableHeight = window.innerHeight - 100; const availableHeight = window.innerHeight - 110;
instanceActivityRef.value.style.height = `${availableHeight}px`; instanceActivityRef.value.style.height = `${availableHeight}px`;
instanceActivityRef.value.style.overflowY = 'auto'; instanceActivityRef.value.style.overflowY = 'auto';
} }
+6 -5
View File
@@ -146,7 +146,7 @@
<DropdownMenuSubContent <DropdownMenuSubContent
side="right" side="right"
align="start" align="start"
class="w-[180px] p-1 rounded-lg"> class="w-45 p-1 rounded-lg">
<div class="group-visibility-menu"> <div class="group-visibility-menu">
<button <button
v-for="visibility in avatarGroupVisibilityOptions" v-for="visibility in avatarGroupVisibilityOptions"
@@ -476,7 +476,7 @@
</div> </div>
</template> </template>
<template v-else-if="activeLocalGroupName"> <template v-else-if="activeLocalGroupName">
<el-scrollbar <ScrollArea
ref="localAvatarScrollbarRef" ref="localAvatarScrollbarRef"
class="favorites-content__scroll" class="favorites-content__scroll"
@scroll="handleLocalAvatarScroll"> @scroll="handleLocalAvatarScroll">
@@ -495,7 +495,7 @@
</div> </div>
</template> </template>
<div v-else class="favorites-empty">No Data</div> <div v-else class="favorites-empty">No Data</div>
</el-scrollbar> </ScrollArea>
</template> </template>
<template v-else-if="isHistorySelected"> <template v-else-if="isHistorySelected">
<div class="favorites-content__scroll favorites-content__scroll--native"> <div class="favorites-content__scroll favorites-content__scroll--native">
@@ -529,6 +529,7 @@
import { ArrowUpDown, Check, Ellipsis, Loader, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next'; import { ArrowUpDown, Check, Ellipsis, Loader, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next';
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group'; import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
@@ -1101,7 +1102,7 @@
if (!isLocalGroupSelected.value || isSearchActive.value) { if (!isLocalGroupSelected.value || isSearchActive.value) {
return; return;
} }
const wrap = localAvatarScrollbarRef.value?.wrapRef; const wrap = localAvatarScrollbarRef.value?.viewportEl?.value;
if (!wrap) { if (!wrap) {
return; return;
} }
@@ -1135,7 +1136,7 @@
if (!isLocalGroupSelected.value || isSearchActive.value) { if (!isLocalGroupSelected.value || isSearchActive.value) {
return; return;
} }
const wrap = localAvatarScrollbarRef.value?.wrapRef; const wrap = localAvatarScrollbarRef.value?.viewportEl?.value;
if (!wrap) { if (!wrap) {
return; return;
} }
+5 -4
View File
@@ -404,7 +404,7 @@
</template> </template>
<div v-else class="favorites-empty">No Data</div> <div v-else class="favorites-empty">No Data</div>
</div> </div>
<el-scrollbar <ScrollArea
v-else-if="activeLocalGroupName && isLocalGroupSelected" v-else-if="activeLocalGroupName && isLocalGroupSelected"
ref="localFavoritesScrollbarRef" ref="localFavoritesScrollbarRef"
class="favorites-content__scroll" class="favorites-content__scroll"
@@ -424,7 +424,7 @@
</div> </div>
</template> </template>
<div v-else class="favorites-empty">No Data</div> <div v-else class="favorites-empty">No Data</div>
</el-scrollbar> </ScrollArea>
<div v-else class="favorites-empty">No Data</div> <div v-else class="favorites-empty">No Data</div>
</template> </template>
</div> </div>
@@ -440,6 +440,7 @@
import { ArrowUpDown, Ellipsis, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next'; import { ArrowUpDown, Ellipsis, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next';
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group'; import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
@@ -1004,7 +1005,7 @@
if (!isLocalGroupSelected.value || isSearchActive.value) { if (!isLocalGroupSelected.value || isSearchActive.value) {
return; return;
} }
const wrap = localFavoritesScrollbarRef.value?.wrapRef; const wrap = localFavoritesScrollbarRef.value?.viewportEl?.value;
if (!wrap) { if (!wrap) {
return; return;
} }
@@ -1097,7 +1098,7 @@
if (!isLocalGroupSelected.value || isSearchActive.value) { if (!isLocalGroupSelected.value || isSearchActive.value) {
return; return;
} }
const wrap = localFavoritesScrollbarRef.value?.wrapRef; const wrap = localFavoritesScrollbarRef.value?.viewportEl?.value;
if (!wrap) { if (!wrap) {
return; return;
} }
@@ -25,8 +25,8 @@
:rows="10" :rows="10"
style="margin-top: 10px" style="margin-top: 10px"
input-class="resize-none" /> input-class="resize-none" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div> <div>
<div class="mb-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Select <Select
:model-value="worldImportFavoriteGroupSelection" :model-value="worldImportFavoriteGroupSelection"
+7 -9
View File
@@ -306,6 +306,7 @@ export const columns = [
location={original.location} location={original.location}
hint={original.worldName} hint={original.worldName}
grouphint={original.groupName} grouphint={original.groupName}
disableTooltip
/> />
</div> </div>
) : null; ) : null;
@@ -318,6 +319,7 @@ export const columns = [
location={original.location} location={original.location}
hint={original.worldName} hint={original.worldName}
grouphint={original.groupName} grouphint={original.groupName}
disableTooltip
/> />
</div> </div>
) : null; ) : null;
@@ -350,7 +352,7 @@ export const columns = [
} }
return ( return (
<span class="block w-full min-w-0 truncate"> <div class="block w-full min-w-0 truncate">
<i <i
class={[ class={[
'x-user-status', 'x-user-status',
@@ -359,7 +361,7 @@ export const columns = [
]} ]}
></i> ></i>
<span>{original.statusDescription}</span> <span>{original.statusDescription}</span>
</span> </div>
); );
} }
@@ -379,13 +381,9 @@ export const columns = [
if (type === 'Bio') { if (type === 'Bio') {
return ( return (
<span <div class="block w-full min-w-0 truncate">
class="block w-full min-w-0 truncate" {original.bio}
innerHTML={formatDifference( </div>
original.previousBio,
original.bio
)}
></span>
); );
} }
@@ -58,7 +58,7 @@
<div v-else class="friend-view__toolbar friend-view__toolbar--loading"> <div v-else class="friend-view__toolbar friend-view__toolbar--loading">
<span class="friend-view__loading-text">{{ t('view.friends_locations.loading_more') }}</span> <span class="friend-view__loading-text">{{ t('view.friends_locations.loading_more') }}</span>
</div> </div>
<el-scrollbar v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll" @scroll="handleScroll"> <ScrollArea v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll" @scroll="handleScroll">
<template v-if="isSameInstanceView"> <template v-if="isSameInstanceView">
<div v-if="visibleSameInstanceGroups.length" class="friend-view__instances"> <div v-if="visibleSameInstanceGroups.length" class="friend-view__instances">
<section <section
@@ -151,7 +151,7 @@
<Loader2 class="friend-view__loading-icon" :size="18" /> <Loader2 class="friend-view__loading-icon" :size="18" />
<span>{{ t('view.friends_locations.loading_more') }}</span> <span>{{ t('view.friends_locations.loading_more') }}</span>
</div> </div>
</el-scrollbar> </ScrollArea>
<div v-else class="friend-view__initial-loading"> <div v-else class="friend-view__initial-loading">
<Loader2 class="friend-view__loading-icon" :size="22" /> <Loader2 class="friend-view__loading-icon" :size="22" />
</div> </div>
@@ -164,6 +164,7 @@
import { Loader2, Settings } from 'lucide-vue-next'; import { Loader2, Settings } from 'lucide-vue-next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { InputGroupSearch } from '@/components/ui/input-group'; import { InputGroupSearch } from '@/components/ui/input-group';
import { ScrollArea } from '@/components/ui/scroll-area';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -262,7 +263,7 @@
let cleanupResize; let cleanupResize;
const updateGridWidth = () => { const updateGridWidth = () => {
const wrap = scrollbarRef.value?.wrapRef; const wrap = scrollbarRef.value?.viewportEl?.value;
if (!wrap) { if (!wrap) {
return; return;
} }
@@ -276,7 +277,7 @@
cleanupResize = undefined; cleanupResize = undefined;
} }
const wrap = scrollbarRef.value?.wrapRef; const wrap = scrollbarRef.value?.viewportEl?.value;
if (!wrap) { if (!wrap) {
return; return;
} }
@@ -559,7 +560,7 @@
return; return;
} }
const wrap = scrollbarRef.value?.wrapRef; const wrap = scrollbarRef.value?.viewportEl?.value;
if (!wrap) { if (!wrap) {
return; return;
@@ -590,7 +591,7 @@
function maybeFillViewport() { function maybeFillViewport() {
nextTick(() => { nextTick(() => {
const wrap = scrollbarRef.value?.wrapRef; const wrap = scrollbarRef.value?.viewportEl?.value;
if (!wrap) { if (!wrap) {
return; return;
} }
@@ -634,7 +635,6 @@
return; return;
} }
nextTick(() => { nextTick(() => {
scrollbarRef.value?.update?.();
updateGridWidth(); updateGridWidth();
maybeFillViewport(); maybeFillViewport();
}); });
@@ -697,7 +697,6 @@
settingsReady.value = true; settingsReady.value = true;
nextTick(() => { nextTick(() => {
setupResizeHandling(); setupResizeHandling();
scrollbarRef.value?.update?.();
updateGridWidth(); updateGridWidth();
maybeFillViewport(); maybeFillViewport();
}); });
@@ -352,66 +352,52 @@
@change="updateTrustColor('', '', true)"></simple-switch> @change="updateTrustColor('', '', true)"></simple-switch>
<div> <div>
<div> <div>
<el-color-picker <PresetColorPicker
:model-value="trustColor.untrusted" :model-value="trustColor.untrusted"
size="small" :presets="['#CCCCCC']"
:predefine="['#CCCCCC']" @change="updateTrustColor('untrusted', $event)" />
@change="updateTrustColor('untrusted', $event)">
</el-color-picker>
<span class="color-picker x-tag-untrusted">Visitor</span> <span class="color-picker x-tag-untrusted">Visitor</span>
</div> </div>
<div> <div>
<el-color-picker <PresetColorPicker
:model-value="trustColor.basic" :model-value="trustColor.basic"
size="small" :presets="['#1778ff']"
:predefine="['#1778ff']" @change="updateTrustColor('basic', $event)" />
@change="updateTrustColor('basic', $event)">
</el-color-picker>
<span class="color-picker x-tag-basic">New User</span> <span class="color-picker x-tag-basic">New User</span>
</div> </div>
<div> <div>
<el-color-picker <PresetColorPicker
:model-value="trustColor.known" :model-value="trustColor.known"
size="small" :presets="['#2bcf5c']"
:predefine="['#2bcf5c']" @change="updateTrustColor('known', $event)" />
@change="updateTrustColor('known', $event)">
</el-color-picker>
<span class="color-picker x-tag-known">User</span> <span class="color-picker x-tag-known">User</span>
</div> </div>
<div> <div>
<el-color-picker <PresetColorPicker
:model-value="trustColor.trusted" :model-value="trustColor.trusted"
size="small" :presets="['#ff7b42']"
:predefine="['#ff7b42']" @change="updateTrustColor('trusted', $event)" />
@change="updateTrustColor('trusted', $event)">
</el-color-picker>
<span class="color-picker x-tag-trusted">Known User</span> <span class="color-picker x-tag-trusted">Known User</span>
</div> </div>
<div> <div>
<el-color-picker <PresetColorPicker
:model-value="trustColor.veteran" :model-value="trustColor.veteran"
size="small" :presets="['#b18fff', '#8143e6', '#ff69b4', '#b52626', '#ffd000', '#abcdef']"
:predefine="['#b18fff', '#8143e6', '#ff69b4', '#b52626', '#ffd000', '#abcdef']" @change="updateTrustColor('veteran', $event)" />
@change="updateTrustColor('veteran', $event)">
</el-color-picker>
<span class="color-picker x-tag-veteran">Trusted User</span> <span class="color-picker x-tag-veteran">Trusted User</span>
</div> </div>
<div> <div>
<el-color-picker <PresetColorPicker
:model-value="trustColor.vip" :model-value="trustColor.vip"
size="small" :presets="['#ff2626']"
:predefine="['#ff2626']" @change="updateTrustColor('vip', $event)" />
@change="updateTrustColor('vip', $event)">
</el-color-picker>
<span class="color-picker x-tag-vip">VRChat Team</span> <span class="color-picker x-tag-vip">VRChat Team</span>
</div> </div>
<div> <div>
<el-color-picker <PresetColorPicker
:model-value="trustColor.troll" :model-value="trustColor.troll"
size="small" :presets="['#782f2f']"
:predefine="['#782f2f']" @change="updateTrustColor('troll', $event)" />
@change="updateTrustColor('troll', $event)">
</el-color-picker>
<span class="color-picker x-tag-troll">Nuisance</span> <span class="color-picker x-tag-troll">Nuisance</span>
</div> </div>
</div> </div>
@@ -445,6 +431,8 @@
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import PresetColorPicker from '@/components/PresetColorPicker.vue';
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '../../../../stores'; import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '../../../../stores';
import { getLanguageName, languageCodes } from '../../../../localization'; import { getLanguageName, languageCodes } from '../../../../localization';
import { THEME_CONFIG } from '../../../../shared/constants'; import { THEME_CONFIG } from '../../../../shared/constants';
+33 -13
View File
@@ -4,10 +4,7 @@
<div style="flex: 1; padding: 10px; padding-left: 0"> <div style="flex: 1; padding: 10px; padding-left: 0">
<Popover v-model:open="isQuickSearchOpen"> <Popover v-model:open="isQuickSearchOpen">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Input <Input v-model="quickSearchQuery" :placeholder="t('side_panel.search_placeholder')" />
v-model="quickSearchQuery"
:placeholder="t('side_panel.search_placeholder')"
@focus="handleQuickSearchFocus" />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
side="bottom" side="bottom"
@@ -78,6 +75,7 @@
:items="sidebarTabs" :items="sidebarTabs"
:unmount-on-hide="false" :unmount-on-hide="false"
variant="equal" variant="equal"
fill
class="zero-margin-tabs" class="zero-margin-tabs"
style="height: calc(100% - 70px); margin-top: 5px"> style="height: calc(100% - 70px); margin-top: 5px">
<template #label-friends> <template #label-friends>
@@ -89,14 +87,19 @@
<span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span> <span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span>
</template> </template>
<template #friends> <template #friends>
<div class="el-tabs__content"> <div class="h-full overflow-hidden">
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop> <ScrollArea ref="friendsScrollAreaRef" class="h-full">
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" /> <FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
</ScrollArea>
<BackToTop :target="friendsScrollTarget" :bottom="20" :right="20" :teleport="false" />
</div> </div>
</template> </template>
<template #groups> <template #groups>
<div class="el-tabs__content"> <div class="h-full overflow-hidden">
<ScrollArea ref="groupsScrollAreaRef" class="h-full">
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" /> <GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
</ScrollArea>
<BackToTop :target="groupsScrollTarget" :bottom="20" :right="20" :teleport="false" />
</div> </div>
</template> </template>
</TabsUnderline> </TabsUnderline>
@@ -104,16 +107,19 @@
</template> </template>
<script setup> <script setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { RefreshCw } from 'lucide-vue-next'; import { RefreshCw } from 'lucide-vue-next';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { TabsUnderline } from '@/components/ui/tabs'; import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BackToTop from '@/components/BackToTop.vue';
import { useFriendStore, useGroupStore, useSearchStore } from '../../stores'; import { useFriendStore, useGroupStore, useSearchStore } from '../../stores';
import { userImage } from '../../shared/utils'; import { userImage } from '../../shared/utils';
@@ -134,6 +140,25 @@
const quickSearchQuery = ref(''); const quickSearchQuery = ref('');
const isQuickSearchOpen = ref(false); const isQuickSearchOpen = ref(false);
const friendsScrollAreaRef = ref(null);
const groupsScrollAreaRef = ref(null);
const friendsScrollTarget = ref(null);
const groupsScrollTarget = ref(null);
function resolveScrollViewport(scrollAreaComponentRef) {
// Our ScrollArea renders a DOM element root; the viewport is marked by data-slot.
const rootEl = scrollAreaComponentRef?.$el ?? null;
if (!rootEl || typeof rootEl.querySelector !== 'function') return null;
return rootEl.querySelector('[data-slot="scroll-area-viewport"]');
}
onMounted(async () => {
// Ensure child components are mounted before querying their DOM.
await nextTick();
friendsScrollTarget.value = resolveScrollViewport(friendsScrollAreaRef.value);
groupsScrollTarget.value = resolveScrollViewport(groupsScrollAreaRef.value);
});
watch( watch(
quickSearchQuery, quickSearchQuery,
(value) => { (value) => {
@@ -142,11 +167,6 @@
{ immediate: true } { immediate: true }
); );
function handleQuickSearchFocus() {
isQuickSearchOpen.value = true;
quickSearchRemoteMethod(String(quickSearchQuery.value ?? ''));
}
function handleQuickSearchSelect(value) { function handleQuickSearchSelect(value) {
if (!value) { if (!value) {
return; return;
+2 -2
View File
@@ -45,7 +45,7 @@
</Button> </Button>
</template> </template>
<div v-else class="skeleton" aria-busy="true" aria-label="Loading"> <!-- <div v-else class="skeleton" aria-busy="true" aria-label="Loading">
<div> <div>
<Skeleton class="h-10 w-10 rounded-full" /> <Skeleton class="h-10 w-10 rounded-full" />
<div> <div>
@@ -53,7 +53,7 @@
<Skeleton class="mt-1.5 h-3 w-full" /> <Skeleton class="mt-1.5 h-3 w-full" />
</div> </div>
</div> </div>
</div> </div> -->
</div> </div>
</template> </template>
@@ -42,11 +42,7 @@
const timeZone = getLocalTimeZone(); const timeZone = getLocalTimeZone();
// JSDoc casts: this project can end up with nominal-type mismatches for DateValue
// due to duplicate @internationalized/date copies in tooling.
/** @type {import('vue').Ref<any>} */
const internalValue = ref(fromDate(props.modelValue ?? new Date(), timeZone)); const internalValue = ref(fromDate(props.modelValue ?? new Date(), timeZone));
/** @type {import('vue').Ref<any>} */
const placeholder = ref(fromDate(props.modelValue ?? new Date(), timeZone)); const placeholder = ref(fromDate(props.modelValue ?? new Date(), timeZone));
watch( watch(
@@ -147,6 +143,10 @@
:class="hasFollowingFor(weekDate) ? 'has-following' : 'no-following'"> :class="hasFollowingFor(weekDate) ? 'has-following' : 'no-following'">
{{ eventCountFor(weekDate) }} {{ eventCountFor(weekDate) }}
</div> </div>
<!-- <div
v-if="eventCountFor(weekDate) > 0"
class="calendar-event-dot"
aria-hidden="true" /> -->
</div> </div>
</div> </div>
</CalendarCellTrigger> </CalendarCellTrigger>
@@ -165,7 +165,6 @@
width: 100%; width: 100%;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: center;
} }
.date { .date {
@@ -193,21 +192,20 @@
.calendar-event-badge { .calendar-event-badge {
position: absolute; position: absolute;
top: 2px; top: -4px;
right: 2px; right: -4px;
min-width: 18px; min-width: 14px;
height: 18px; height: 14px;
border-radius: 9px; border-radius: 9px;
color: var(--el-color-white, #fff); color: var(--el-color-white, #fff);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 11px; font-size: 10px;
font-weight: bold;
box-shadow: var(--el-box-shadow-lighter); box-shadow: var(--el-box-shadow-lighter);
z-index: 10; z-index: 10;
padding: 0 5px; padding: 0 5px;
line-height: 18px; line-height: 14px;
} }
.calendar-event-badge.has-following { .calendar-event-badge.has-following {
@@ -217,4 +215,18 @@
.calendar-event-badge.no-following { .calendar-event-badge.no-following {
background-color: var(--group-calendar-badge-normal, var(--el-color-primary)); background-color: var(--group-calendar-badge-normal, var(--el-color-primary));
} }
.calendar-event-dot {
position: absolute;
left: 50%;
bottom: 4px;
transform: translateX(-50%);
width: 6px;
height: 6px;
border-radius: 9999px;
background-color: var(--group-calendar-event-dot, #ef4444);
box-shadow: var(--el-box-shadow-lighter);
z-index: 5;
pointer-events: none;
}
</style> </style>
+34 -21
View File
@@ -1,6 +1,6 @@
<template> <template>
<Dialog :open="visible" @update:open="(open) => (open ? null : closeDialog())"> <Dialog :open="visible" @update:open="(open) => (open ? null : closeDialog())">
<DialogContent class="x-dialog w-[90vw] max-w-[90vw] sm:max-w-[70vw] h-[60vh] overflow-hidden"> <DialogContent class="x-dialog sm:max-w-[50vw] h-[60vh] overflow-hidden">
<DialogHeader> <DialogHeader>
<div class="dialog-title-container"> <div class="dialog-title-container">
<DialogTitle>{{ t('dialog.group_calendar.header') }}</DialogTitle> <DialogTitle>{{ t('dialog.group_calendar.header') }}</DialogTitle>
@@ -18,17 +18,13 @@
</div> </div>
</DialogHeader> </DialogHeader>
<div class="top-content"> <div class="top-content">
<transition name="el-fade-in-linear" mode="out-in">
<div v-if="viewMode === 'timeline'" key="timeline" class="timeline-view"> <div v-if="viewMode === 'timeline'" key="timeline" class="timeline-view">
<div class="timeline-container"> <div class="timeline-container">
<el-timeline v-if="groupedTimelineEvents.length"> <div v-if="groupedTimelineEvents.length" class="timeline-list">
<el-timeline-item <div v-for="(timeGroup, key) of groupedTimelineEvents" :key="key" class="timeline-group">
v-for="(timeGroup, key) of groupedTimelineEvents" <div class="timeline-timestamp">
:key="key" {{ dayjs(timeGroup.startsAt).format('MM-DD ddd') }} {{ timeGroup.startTime }}
:timestamp=" </div>
dayjs(timeGroup.startsAt).format('MM-DD ddd') + ' ' + timeGroup.startTime
"
placement="top">
<div class="time-group-container"> <div class="time-group-container">
<GroupCalendarEventCard <GroupCalendarEventCard
v-for="value in timeGroup.events" v-for="value in timeGroup.events"
@@ -40,9 +36,9 @@
@update-following-calendar-data="updateFollowingCalendarData" @update-following-calendar-data="updateFollowingCalendarData"
@click-action="showGroupDialog(value.ownerId)" /> @click-action="showGroupDialog(value.ownerId)" />
</div> </div>
</el-timeline-item> </div>
</el-timeline> </div>
<div v-else>{{ t('dialog.group_calendar.no_events') }}</div> <div v-else class="timeline-empty">{{ t('dialog.group_calendar.no_events') }}</div>
</div> </div>
<div class="calendar-container"> <div class="calendar-container">
@@ -93,7 +89,6 @@
</div> </div>
</div> </div>
</div> </div>
</transition>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -334,7 +329,8 @@
.sort((a, b) => dayjs(a.startsAt).diff(dayjs(b.startsAt))); .sort((a, b) => dayjs(a.startsAt).diff(dayjs(b.startsAt)));
}); });
const formatDateKey = (date) => formatDateFilter(date, 'date'); // Use a stable key for calendar maps (independent of locale/appearance date formatting).
const formatDateKey = (date) => dayjs(date).format('YYYY-MM-DD');
function getGroupNameFromCache(groupId) { function getGroupNameFromCache(groupId) {
if (!groupNamesCache.has(groupId)) { if (!groupNamesCache.has(groupId)) {
@@ -462,18 +458,36 @@
overflow: hidden; overflow: hidden;
.timeline-view { .timeline-view {
.timeline-container { .timeline-container {
:deep(.el-timeline) {
width: 100%;
height: 100%;
min-width: 200px; min-width: 200px;
padding-left: 4px; padding-left: 4px;
padding-right: 16px; padding-right: 16px;
margin-left: 10px; margin-left: 10px;
margin-right: 6px; margin-right: 6px;
overflow: auto; overflow: auto;
.el-timeline-item {
padding: 0 20px 20px 10px; .timeline-list {
display: flex;
flex-direction: column;
gap: 14px;
} }
.timeline-group {
padding: 0 20px 6px 10px;
}
.timeline-timestamp {
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-secondary);
margin-bottom: 8px;
}
.timeline-empty {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
} }
.time-group-container { .time-group-container {
display: flex; display: flex;
@@ -571,7 +585,6 @@
height: 100%; height: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;
} }
.calendar-container { .calendar-container {
width: 609px; width: 609px;