mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
Compare commits
26 Commits
refactor/n
...
feat/build
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d8d3ef62 | ||
|
|
5d4a16fe8f | ||
|
|
8a3c1b3ba8 | ||
|
|
59b257a50f | ||
|
|
4f8fd18a39 | ||
|
|
95eb841110 | ||
|
|
d06b0db923 | ||
|
|
1c4dc154e8 | ||
|
|
1f67f63513 | ||
|
|
761b3964a9 | ||
|
|
b9a4d4a864 | ||
|
|
c82e3d8427 | ||
|
|
3211312b0e | ||
|
|
fb575acc4f | ||
|
|
4a0b481a2d | ||
|
|
cd15e12cdd | ||
|
|
0e20fd0ab0 | ||
|
|
f44b4a87e9 | ||
|
|
4986f4251d | ||
|
|
8213738e26 | ||
|
|
f32493e728 | ||
|
|
78ce64e357 | ||
|
|
bb8c4a133e | ||
|
|
c3222cc6c4 | ||
|
|
71621072f8 | ||
|
|
d4a8edab49 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -333,7 +333,9 @@ jobs:
|
||||
echo VITE_CONNECT=${{ secrets.VITE_CONNECT }} >> .env
|
||||
echo VITE_UNRAID_NET=${{ secrets.VITE_UNRAID_NET }} >> .env
|
||||
echo VITE_CALLBACK_KEY=${{ secrets.VITE_CALLBACK_KEY }} >> .env
|
||||
cat .env
|
||||
|
||||
touch .env.production
|
||||
echo NUXT_UI_PRO_LICENSE=${{ secrets.NUXT_UI_PRO_LICENSE }} >> .env.production
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -118,3 +118,6 @@ api/dev/Unraid.net/myservers.cfg
|
||||
|
||||
# local Mise settings
|
||||
.mise.toml
|
||||
|
||||
# environment variables
|
||||
web/.env.production
|
||||
|
||||
@@ -161,3 +161,4 @@ Enables GraphQL playground at `http://tower.local/graphql`
|
||||
- Never use the `any` type. Always prefer proper typing
|
||||
- Avoid using casting whenever possible, prefer proper typing from the start
|
||||
- **IMPORTANT:** cache-manager v7 expects TTL values in **milliseconds**, not seconds. Always use milliseconds when setting cache TTL (e.g., 600000 for 10 minutes, not 600)
|
||||
- Use the nuxt UI library in VUE MODE NOT IN NUXT MODE
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"@nestjs/core": "11.1.6",
|
||||
"@nestjs/graphql": "13.1.0",
|
||||
"nest-authz": "2.17.0",
|
||||
"typescript": "5.9.2"
|
||||
"typescript": "5.9.2",
|
||||
"pify": "^6.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "11.1.6",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"graphql-ws": "6.0.6",
|
||||
"lodash-es": "4.17.21",
|
||||
"nest-authz": "2.17.0",
|
||||
"pify": "^6.1.0",
|
||||
"rimraf": "6.0.1",
|
||||
"type-fest": "4.41.0",
|
||||
"typescript": "5.9.2",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Menu="UNRAID-OS"
|
||||
Title="Detail Layout"
|
||||
Icon="icon-u-globe"
|
||||
Tag="globe"
|
||||
---
|
||||
<unraid-detail-test />
|
||||
334
pnpm-lock.yaml
generated
334
pnpm-lock.yaml
generated
@@ -712,6 +712,9 @@ importers:
|
||||
nest-authz:
|
||||
specifier: 2.17.0
|
||||
version: 2.17.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)
|
||||
pify:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
typescript:
|
||||
specifier: 5.9.2
|
||||
version: 5.9.2
|
||||
@@ -782,6 +785,9 @@ importers:
|
||||
nest-authz:
|
||||
specifier: 2.17.0
|
||||
version: 2.17.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)
|
||||
pify:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
rimraf:
|
||||
specifier: 6.0.1
|
||||
version: 6.0.1
|
||||
@@ -1071,8 +1077,8 @@ importers:
|
||||
specifier: 3.6.0
|
||||
version: 3.6.0(@jsonforms/core@3.6.0)(@jsonforms/vue@3.6.0(@jsonforms/core@3.6.0)(vue@3.5.20(typescript@5.9.2)))(ajv@8.17.1)(dayjs@1.11.14)(lodash@4.17.21)(maska@2.1.11)(vue@3.5.20(typescript@5.9.2))(vuetify@3.9.6)
|
||||
'@nuxt/ui':
|
||||
specifier: 3.3.2
|
||||
version: 3.3.2(@babel/parser@7.28.3)(@netlify/blobs@9.1.2)(change-case@5.4.4)(db0@0.3.2)(embla-carousel@8.6.0)(focus-trap@7.6.5)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.20(typescript@5.9.2)))(vue@3.5.20(typescript@5.9.2))(zod@3.25.76)
|
||||
specifier: 4.0.0-alpha.0
|
||||
version: 4.0.0-alpha.0(@babel/parser@7.28.3)(@netlify/blobs@9.1.2)(change-case@5.4.4)(db0@0.3.2)(embla-carousel@8.6.0)(focus-trap@7.6.5)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.20(typescript@5.9.2)))(vue@3.5.20(typescript@5.9.2))(zod@3.25.76)
|
||||
'@nuxtjs/color-mode':
|
||||
specifier: 3.5.2
|
||||
version: 3.5.2(magicast@0.3.5)
|
||||
@@ -1315,6 +1321,31 @@ packages:
|
||||
'@adobe/css-tools@4.4.3':
|
||||
resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==}
|
||||
|
||||
'@ai-sdk/gateway@1.0.15':
|
||||
resolution: {integrity: sha512-xySXoQ29+KbGuGfmDnABx+O6vc7Gj7qugmj1kGpn0rW0rQNn6UKUuvscKMzWyv1Uv05GyC1vqHq8ZhEOLfXscQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.7':
|
||||
resolution: {integrity: sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/vue@2.0.26':
|
||||
resolution: {integrity: sha512-QNaG+kbIZMN8xW5JMlDSCPVtnDm3SP7g5i8/yRJGI4skEVVWiscRqEfleLeBWNrUtZbHSxaV1+4EqFJAP70/dg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
vue: ^3.3.4
|
||||
peerDependenciesMeta:
|
||||
vue:
|
||||
optional: true
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1329,8 +1360,8 @@ packages:
|
||||
'@antfu/utils@0.7.10':
|
||||
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
|
||||
|
||||
'@antfu/utils@8.1.1':
|
||||
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
|
||||
'@antfu/utils@9.2.0':
|
||||
resolution: {integrity: sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw==}
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@14.1.1':
|
||||
resolution: {integrity: sha512-uGF1YGOzzD50L7HLNWclXmsEhQflw8/zZHIz0/AzkJrKL5r9PceUipZxR/cp/8veTk4TVfdDJLyIwXLjaP5ePg==}
|
||||
@@ -1365,7 +1396,6 @@ packages:
|
||||
|
||||
'@apollo/server-gateway-interface@1.1.1':
|
||||
resolution: {integrity: sha512-pGwCl/po6+rxRmDMFgozKQo2pbsSwE91TpsDBAOgf74CRDPXHHtM88wbwjab0wMMZh95QfR45GGyDIdhY24bkQ==}
|
||||
deprecated: '@apollo/server-gateway-interface v1 is part of Apollo Server v4, which is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v2 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details.'
|
||||
peerDependencies:
|
||||
graphql: 16.11.0
|
||||
|
||||
@@ -1379,7 +1409,6 @@ packages:
|
||||
'@apollo/server@4.12.2':
|
||||
resolution: {integrity: sha512-jKRlf+sBMMdKYrjMoiWKne42Eb6paBfDOr08KJnUaeaiyWFj+/040FjVPQI7YGLfdwnYIsl1NUUqS2UdgezJDg==}
|
||||
engines: {node: '>=14.16.0'}
|
||||
deprecated: Apollo Server v4 is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v5 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details.
|
||||
peerDependencies:
|
||||
graphql: 16.11.0
|
||||
|
||||
@@ -3054,14 +3083,14 @@ packages:
|
||||
prettier-plugin-ember-template-tag:
|
||||
optional: true
|
||||
|
||||
'@iconify/collections@1.0.569':
|
||||
resolution: {integrity: sha512-PclOVcAlvv55Fv5kRJmxk/KMoFLNBMLh0q9LDMlonIPJMUu958VsNw7F7CVurfyEbCf/54i7eF+q6LHqJxeQvg==}
|
||||
'@iconify/collections@1.0.588':
|
||||
resolution: {integrity: sha512-K6jijh3aEZ937R+ES5Swd62NOCZ868PNCyHNg+R7c9Kn9yurtuiLM/zkpN8KxRwVvTX8w83EkBqhUjqo+wFgDw==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
'@iconify/utils@2.3.0':
|
||||
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
||||
'@iconify/utils@3.0.1':
|
||||
resolution: {integrity: sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==}
|
||||
|
||||
'@iconify/vue@5.0.0':
|
||||
resolution: {integrity: sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==}
|
||||
@@ -3745,8 +3774,8 @@ packages:
|
||||
'@nuxt/fonts@0.11.4':
|
||||
resolution: {integrity: sha512-GbLavsC+9FejVwY+KU4/wonJsKhcwOZx/eo4EuV57C4osnF/AtEmev8xqI0DNlebMEhEGZbu1MGwDDDYbeR7Bw==}
|
||||
|
||||
'@nuxt/icon@1.15.0':
|
||||
resolution: {integrity: sha512-kA0rxqr1B601zNJNcOXera8CyYcxUCEcT7dXEC7rwAz71PRCN5emf7G656eKEQgtqrD4JSj6NQqWDgrmFcf/GQ==}
|
||||
'@nuxt/icon@2.0.0':
|
||||
resolution: {integrity: sha512-sy8+zkKMYp+H09S0cuTteL3zPTmktqzYPpPXV9ZkLNjrQsaPH08n7s/9wjr+C/K/w2R3u18E3+P1VIQi3xaq1A==}
|
||||
|
||||
'@nuxt/kit@3.17.5':
|
||||
resolution: {integrity: sha512-NdCepmA+S/SzgcaL3oYUeSlXGYO6BXGr9K/m1D0t0O9rApF8CSq/QQ+ja5KYaYMO1kZAEWH4s2XVcE3uPrrAVg==}
|
||||
@@ -3818,8 +3847,8 @@ packages:
|
||||
vitest:
|
||||
optional: true
|
||||
|
||||
'@nuxt/ui@3.3.2':
|
||||
resolution: {integrity: sha512-LN8axCK/0zCqWC/m0nN5R4vQyGmv6Viu9K1ZyzApgAg4vsyRYKXLtr2ta/vXv2y4/CtKfncry1zs/IfsktDyuw==}
|
||||
'@nuxt/ui@4.0.0-alpha.0':
|
||||
resolution: {integrity: sha512-Gvjfoyw2VkyovMddhUhu+ixHpcCHb/MDlqlcYt29knxvVcJqMGNW/BvSgcmAVrttNb9xUnL6rvg0bneFXU48Gg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@inertiajs/vue3': ^2.0.7
|
||||
@@ -3858,6 +3887,10 @@ packages:
|
||||
'@one-ini/wasm@0.1.1':
|
||||
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
|
||||
|
||||
'@opentelemetry/api@1.9.0':
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@originjs/vite-plugin-commonjs@1.0.3':
|
||||
resolution: {integrity: sha512-KuEXeGPptM2lyxdIEJ4R11+5ztipHoE7hy8ClZt3PYaOVQ/pyngd2alaSrPnwyFeOW1UagRBaQ752aA1dTMdOQ==}
|
||||
|
||||
@@ -5997,6 +6030,12 @@ packages:
|
||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ai@5.0.26:
|
||||
resolution: {integrity: sha512-bGNtG+nYQ2U+5mzuLbxIg9WxGQJ2u5jv2gYgP8C+CJ1YI4qqIjvjOgGEZWzvNet8jiOGIlqstsht9aQefKzmBw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4
|
||||
|
||||
ajv-errors@3.0.0:
|
||||
resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==}
|
||||
peerDependencies:
|
||||
@@ -6520,6 +6559,9 @@ packages:
|
||||
caniuse-lite@1.0.30001727:
|
||||
resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
|
||||
|
||||
caniuse-lite@1.0.30001731:
|
||||
resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==}
|
||||
|
||||
capital-case@1.0.4:
|
||||
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
|
||||
|
||||
@@ -7443,10 +7485,6 @@ packages:
|
||||
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv@16.5.0:
|
||||
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv@16.6.1:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -7477,8 +7515,8 @@ packages:
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
electron-to-chromium@1.5.191:
|
||||
resolution: {integrity: sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==}
|
||||
electron-to-chromium@1.5.192:
|
||||
resolution: {integrity: sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==}
|
||||
|
||||
embla-carousel-auto-height@8.6.0:
|
||||
resolution: {integrity: sha512-/HrJQOEM6aol/oF33gd2QlINcXy3e19fJWvHDuHWp2bpyTa+2dm9tVVJak30m2Qy6QyQ6Fc8DkImtv7pxWOJUQ==}
|
||||
@@ -7515,8 +7553,8 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.2.37
|
||||
|
||||
embla-carousel-wheel-gestures@8.0.2:
|
||||
resolution: {integrity: sha512-gtE8xHRwMGsfsMAgco/QoYhvcxNoMLmFF0DaWH7FXJJWk8RlEZyiZHZRZL6TZVCgooo9/hKyYWITLaSZLIvkbQ==}
|
||||
embla-carousel-wheel-gestures@8.1.0:
|
||||
resolution: {integrity: sha512-J68jkYrxbWDmXOm2n2YHl+uMEXzkGSKjWmjaEgL9xVvPb3HqVmg6rJSKfI3sqIDVvm7mkeTy87wtG/5263XqHQ==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
embla-carousel: ^8.0.0 || ~8.0.0-rc03
|
||||
@@ -8089,6 +8127,10 @@ packages:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
|
||||
eventsource-parser@3.0.5:
|
||||
resolution: {integrity: sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
execa@5.1.1:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8238,14 +8280,6 @@ packages:
|
||||
fd-slicer@1.1.0:
|
||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||
|
||||
fdir@6.4.6:
|
||||
resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -8398,6 +8432,20 @@ packages:
|
||||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
framer-motion@12.23.12:
|
||||
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fresh@0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -8774,6 +8822,9 @@ packages:
|
||||
hex-to-rgba@2.0.1:
|
||||
resolution: {integrity: sha512-5XqPJBpsEUMsseJUi2w2Hl7cHFFi3+OO10M2pzAvKB1zL6fc+koGMhmBqoDOCB4GemiRM/zvDMRIhVw6EkB8dQ==}
|
||||
|
||||
hey-listen@1.0.8:
|
||||
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
||||
|
||||
highlight.js@11.11.1:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -9437,6 +9488,9 @@ packages:
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-schema@0.4.0:
|
||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -10038,6 +10092,18 @@ packages:
|
||||
module-details-from-path@1.0.3:
|
||||
resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==}
|
||||
|
||||
motion-dom@12.23.12:
|
||||
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
|
||||
|
||||
motion-utils@12.23.6:
|
||||
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||
|
||||
motion-v@1.7.0:
|
||||
resolution: {integrity: sha512-5oPDF5GBpcRnIZuce7Wap09S8afH4JeBWD3VbMRg4hZKk0olQnTFuHjgQUGMpX3V1WXrZgyveoF02W51XMxx9w==}
|
||||
peerDependencies:
|
||||
'@vueuse/core': '>=10.0.0'
|
||||
vue: '>=3.0.0'
|
||||
|
||||
mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -10730,6 +10796,10 @@ packages:
|
||||
resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pify@6.1.0:
|
||||
resolution: {integrity: sha512-KocF8ve28eFjjuBKKGvzOBGzG8ew2OqOOSxTTZhirkzH7h3BI1vyzqlR0qbfcDBve1Yzo3FVlWUAtCRrbVN8Fw==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
pinia@3.0.3:
|
||||
resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
|
||||
peerDependencies:
|
||||
@@ -12108,6 +12178,11 @@ packages:
|
||||
swap-case@2.0.2:
|
||||
resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==}
|
||||
|
||||
swrv@1.1.0:
|
||||
resolution: {integrity: sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==}
|
||||
peerDependencies:
|
||||
vue: '>=3.2.26 < 4'
|
||||
|
||||
symbol-observable@1.2.0:
|
||||
resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -13441,6 +13516,33 @@ snapshots:
|
||||
|
||||
'@adobe/css-tools@4.4.3': {}
|
||||
|
||||
'@ai-sdk/gateway@1.0.15(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.7(zod@3.25.76)
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.7(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@standard-schema/spec': 1.0.0
|
||||
eventsource-parser: 3.0.5
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/vue@2.0.26(vue@3.5.20(typescript@5.9.2))(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 3.0.7(zod@3.25.76)
|
||||
ai: 5.0.26(zod@3.25.76)
|
||||
swrv: 1.1.0(vue@3.5.20(typescript@5.9.2))
|
||||
optionalDependencies:
|
||||
vue: 3.5.20(typescript@5.9.2)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
@@ -13455,7 +13557,7 @@ snapshots:
|
||||
|
||||
'@antfu/utils@0.7.10': {}
|
||||
|
||||
'@antfu/utils@8.1.1': {}
|
||||
'@antfu/utils@9.2.0': {}
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@14.1.1':
|
||||
dependencies:
|
||||
@@ -13610,7 +13712,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/core': 7.28.0
|
||||
'@babel/generator': 7.28.0
|
||||
'@babel/parser': 7.28.0
|
||||
'@babel/parser': 7.28.3
|
||||
'@babel/runtime': 7.27.6
|
||||
'@babel/traverse': 7.28.0
|
||||
'@babel/types': 7.28.0
|
||||
@@ -13676,7 +13778,7 @@ snapshots:
|
||||
'@babel/helper-compilation-targets': 7.27.2
|
||||
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4)
|
||||
'@babel/helpers': 7.27.6
|
||||
'@babel/parser': 7.28.0
|
||||
'@babel/parser': 7.28.3
|
||||
'@babel/template': 7.27.2
|
||||
'@babel/traverse': 7.28.0
|
||||
'@babel/types': 7.28.0
|
||||
@@ -15211,7 +15313,7 @@ snapshots:
|
||||
'@whatwg-node/fetch': 0.10.8
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
dotenv: 16.5.0
|
||||
dotenv: 16.6.1
|
||||
graphql: 16.11.0
|
||||
graphql-request: 6.1.0(graphql@16.11.0)
|
||||
http-proxy-agent: 7.0.2
|
||||
@@ -15365,21 +15467,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@iconify/collections@1.0.569':
|
||||
'@iconify/collections@1.0.588':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@iconify/utils@2.3.0':
|
||||
'@iconify/utils@3.0.1':
|
||||
dependencies:
|
||||
'@antfu/install-pkg': 1.1.0
|
||||
'@antfu/utils': 8.1.1
|
||||
'@antfu/utils': 9.2.0
|
||||
'@iconify/types': 2.0.0
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
globals: 15.15.0
|
||||
kolorist: 1.8.0
|
||||
local-pkg: 1.1.1
|
||||
local-pkg: 1.1.2
|
||||
mlly: 1.7.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -16241,7 +16343,7 @@ snapshots:
|
||||
|
||||
'@nuxt/fonts@0.11.4(@netlify/blobs@9.1.2)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))':
|
||||
dependencies:
|
||||
'@nuxt/devtools-kit': 2.6.2(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))
|
||||
'@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))
|
||||
'@nuxt/kit': 3.18.1(magicast@0.3.5)
|
||||
consola: 3.4.2
|
||||
css-tree: 3.1.0
|
||||
@@ -16259,7 +16361,7 @@ snapshots:
|
||||
tinyglobby: 0.2.14
|
||||
ufo: 1.6.1
|
||||
unifont: 0.4.1
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
unstorage: 1.16.1(@netlify/blobs@9.1.2)(db0@0.3.2)(ioredis@5.6.1)
|
||||
transitivePeerDependencies:
|
||||
- '@azure/app-configuration'
|
||||
@@ -16284,16 +16386,16 @@ snapshots:
|
||||
- uploadthing
|
||||
- vite
|
||||
|
||||
'@nuxt/icon@1.15.0(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.20(typescript@5.9.2))':
|
||||
'@nuxt/icon@2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.20(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@iconify/collections': 1.0.569
|
||||
'@iconify/collections': 1.0.588
|
||||
'@iconify/types': 2.0.0
|
||||
'@iconify/utils': 2.3.0
|
||||
'@iconify/utils': 3.0.1
|
||||
'@iconify/vue': 5.0.0(vue@3.5.20(typescript@5.9.2))
|
||||
'@nuxt/devtools-kit': 2.6.2(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))
|
||||
'@nuxt/kit': 3.18.1(magicast@0.3.5)
|
||||
'@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))
|
||||
'@nuxt/kit': 4.0.3(magicast@0.3.5)
|
||||
consola: 3.4.2
|
||||
local-pkg: 1.1.1
|
||||
local-pkg: 1.1.2
|
||||
mlly: 1.7.4
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
@@ -16401,7 +16503,7 @@ snapshots:
|
||||
mlly: 1.7.4
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
pkg-types: 2.2.0
|
||||
pkg-types: 2.3.0
|
||||
scule: 1.3.0
|
||||
semver: 7.7.2
|
||||
std-env: 3.9.0
|
||||
@@ -16428,7 +16530,7 @@ snapshots:
|
||||
|
||||
'@nuxt/schema@4.0.3':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.18
|
||||
'@vue/shared': 3.5.20
|
||||
consola: 3.4.2
|
||||
defu: 6.1.4
|
||||
pathe: 2.0.3
|
||||
@@ -16489,13 +16591,14 @@ snapshots:
|
||||
- magicast
|
||||
- typescript
|
||||
|
||||
'@nuxt/ui@3.3.2(@babel/parser@7.28.3)(@netlify/blobs@9.1.2)(change-case@5.4.4)(db0@0.3.2)(embla-carousel@8.6.0)(focus-trap@7.6.5)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.20(typescript@5.9.2)))(vue@3.5.20(typescript@5.9.2))(zod@3.25.76)':
|
||||
'@nuxt/ui@4.0.0-alpha.0(@babel/parser@7.28.3)(@netlify/blobs@9.1.2)(change-case@5.4.4)(db0@0.3.2)(embla-carousel@8.6.0)(focus-trap@7.6.5)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.20(typescript@5.9.2)))(vue@3.5.20(typescript@5.9.2))(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/vue': 2.0.26(vue@3.5.20(typescript@5.9.2))(zod@3.25.76)
|
||||
'@iconify/vue': 5.0.0(vue@3.5.20(typescript@5.9.2))
|
||||
'@internationalized/date': 3.8.2
|
||||
'@internationalized/number': 3.6.5
|
||||
'@nuxt/fonts': 0.11.4(@netlify/blobs@9.1.2)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))
|
||||
'@nuxt/icon': 1.15.0(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.20(typescript@5.9.2))
|
||||
'@nuxt/icon': 2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.20(typescript@5.9.2))
|
||||
'@nuxt/kit': 4.0.3(magicast@0.3.5)
|
||||
'@nuxt/schema': 4.0.3
|
||||
'@nuxtjs/color-mode': 3.5.2(magicast@0.3.5)
|
||||
@@ -16515,12 +16618,13 @@ snapshots:
|
||||
embla-carousel-class-names: 8.6.0(embla-carousel@8.6.0)
|
||||
embla-carousel-fade: 8.6.0(embla-carousel@8.6.0)
|
||||
embla-carousel-vue: 8.6.0(vue@3.5.20(typescript@5.9.2))
|
||||
embla-carousel-wheel-gestures: 8.0.2(embla-carousel@8.6.0)
|
||||
embla-carousel-wheel-gestures: 8.1.0(embla-carousel@8.6.0)
|
||||
fuse.js: 7.1.0
|
||||
hookable: 5.5.3
|
||||
knitwork: 1.2.0
|
||||
magic-string: 0.30.17
|
||||
mlly: 1.7.4
|
||||
motion-v: 1.7.0(@vueuse/core@13.8.0(vue@3.5.20(typescript@5.9.2)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vue@3.5.20(typescript@5.9.2))
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
reka-ui: 2.4.1(typescript@5.9.2)(vue@3.5.20(typescript@5.9.2))
|
||||
@@ -16530,7 +16634,7 @@ snapshots:
|
||||
tailwindcss: 4.1.12
|
||||
tinyglobby: 0.2.14
|
||||
typescript: 5.9.2
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
unplugin-auto-import: 19.3.0(@nuxt/kit@4.0.3(magicast@0.3.5))(@vueuse/core@13.8.0(vue@3.5.20(typescript@5.9.2)))
|
||||
unplugin-vue-components: 28.8.0(@babel/parser@7.28.3)(@nuxt/kit@4.0.3(magicast@0.3.5))(vue@3.5.20(typescript@5.9.2))
|
||||
vaul-vue: 0.4.1(reka-ui@2.4.1(typescript@5.9.2)(vue@3.5.20(typescript@5.9.2)))(vue@3.5.20(typescript@5.9.2))
|
||||
@@ -16548,6 +16652,7 @@ snapshots:
|
||||
- '@babel/parser'
|
||||
- '@capacitor/preferences'
|
||||
- '@deno/kv'
|
||||
- '@emotion/is-prop-valid'
|
||||
- '@netlify/blobs'
|
||||
- '@planetscale/database'
|
||||
- '@upstash/redis'
|
||||
@@ -16569,6 +16674,8 @@ snapshots:
|
||||
- magicast
|
||||
- nprogress
|
||||
- qrcode
|
||||
- react
|
||||
- react-dom
|
||||
- sortablejs
|
||||
- supports-color
|
||||
- universal-cookie
|
||||
@@ -16647,6 +16754,8 @@ snapshots:
|
||||
|
||||
'@one-ini/wasm@0.1.1': {}
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@originjs/vite-plugin-commonjs@1.0.3':
|
||||
dependencies:
|
||||
esbuild: 0.14.54
|
||||
@@ -17048,7 +17157,7 @@ snapshots:
|
||||
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
|
||||
commondir: 1.0.1
|
||||
estree-walker: 2.0.2
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
is-reference: 1.2.1
|
||||
magic-string: 0.30.17
|
||||
picomatch: 4.0.3
|
||||
@@ -18312,7 +18421,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@vue/compiler-sfc': 3.5.18
|
||||
ast-kit: 2.1.1
|
||||
local-pkg: 1.1.1
|
||||
local-pkg: 1.1.2
|
||||
magic-string-ast: 1.0.0
|
||||
unplugin-utils: 0.2.4
|
||||
optionalDependencies:
|
||||
@@ -18343,7 +18452,7 @@ snapshots:
|
||||
'@babel/types': 7.28.0
|
||||
'@vue/babel-helper-vue-transform-on': 1.4.0
|
||||
'@vue/babel-plugin-resolve-type': 1.4.0(@babel/core@7.28.0)
|
||||
'@vue/shared': 3.5.18
|
||||
'@vue/shared': 3.5.20
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.0
|
||||
transitivePeerDependencies:
|
||||
@@ -18362,7 +18471,7 @@ snapshots:
|
||||
|
||||
'@vue/compiler-core@3.5.17':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.0
|
||||
'@babel/parser': 7.28.3
|
||||
'@vue/shared': 3.5.17
|
||||
entities: 4.5.0
|
||||
estree-walker: 2.0.2
|
||||
@@ -18532,7 +18641,7 @@ snapshots:
|
||||
'@volar/language-core': 2.4.22
|
||||
'@vue/compiler-dom': 3.5.18
|
||||
'@vue/compiler-vue2': 2.7.16
|
||||
'@vue/shared': 3.5.18
|
||||
'@vue/shared': 3.5.20
|
||||
alien-signals: 1.0.13
|
||||
minimatch: 9.0.5
|
||||
muggle-string: 0.4.1
|
||||
@@ -18545,7 +18654,7 @@ snapshots:
|
||||
'@volar/language-core': 2.4.22
|
||||
'@vue/compiler-dom': 3.5.18
|
||||
'@vue/compiler-vue2': 2.7.16
|
||||
'@vue/shared': 3.5.18
|
||||
'@vue/shared': 3.5.20
|
||||
alien-signals: 2.0.5
|
||||
muggle-string: 0.4.1
|
||||
path-browserify: 1.0.1
|
||||
@@ -18783,6 +18892,14 @@ snapshots:
|
||||
clean-stack: 2.2.0
|
||||
indent-string: 4.0.0
|
||||
|
||||
ai@5.0.26(zod@3.25.76):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 1.0.15(zod@3.25.76)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.7(zod@3.25.76)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
zod: 3.25.76
|
||||
|
||||
ajv-errors@3.0.0(ajv@8.17.1):
|
||||
dependencies:
|
||||
ajv: 8.17.1
|
||||
@@ -19209,8 +19326,8 @@ snapshots:
|
||||
|
||||
browserslist@4.25.1:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001727
|
||||
electron-to-chromium: 1.5.191
|
||||
caniuse-lite: 1.0.30001731
|
||||
electron-to-chromium: 1.5.192
|
||||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.3(browserslist@4.25.1)
|
||||
|
||||
@@ -19269,7 +19386,7 @@ snapshots:
|
||||
chokidar: 4.0.3
|
||||
confbox: 0.2.2
|
||||
defu: 6.1.4
|
||||
dotenv: 16.5.0
|
||||
dotenv: 16.6.1
|
||||
exsolve: 1.0.7
|
||||
giget: 2.0.0
|
||||
jiti: 2.5.1
|
||||
@@ -19293,7 +19410,7 @@ snapshots:
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
perfect-debounce: 1.0.0
|
||||
pkg-types: 2.2.0
|
||||
pkg-types: 2.3.0
|
||||
rc9: 2.1.2
|
||||
optionalDependencies:
|
||||
magicast: 0.3.5
|
||||
@@ -19379,6 +19496,8 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001727: {}
|
||||
|
||||
caniuse-lite@1.0.30001731: {}
|
||||
|
||||
capital-case@1.0.4:
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
@@ -19700,7 +19819,7 @@ snapshots:
|
||||
|
||||
constantinople@4.0.1:
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.0
|
||||
'@babel/parser': 7.28.3
|
||||
'@babel/types': 7.28.0
|
||||
|
||||
content-disposition@0.5.4:
|
||||
@@ -20329,12 +20448,10 @@ snapshots:
|
||||
|
||||
dotenv-expand@12.0.1:
|
||||
dependencies:
|
||||
dotenv: 16.5.0
|
||||
dotenv: 16.6.1
|
||||
|
||||
dotenv@16.4.7: {}
|
||||
|
||||
dotenv@16.5.0: {}
|
||||
|
||||
dotenv@16.6.1: {}
|
||||
|
||||
dotenv@17.2.1: {}
|
||||
@@ -20360,7 +20477,7 @@ snapshots:
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
electron-to-chromium@1.5.191: {}
|
||||
electron-to-chromium@1.5.192: {}
|
||||
|
||||
embla-carousel-auto-height@8.6.0(embla-carousel@8.6.0):
|
||||
dependencies:
|
||||
@@ -20392,7 +20509,7 @@ snapshots:
|
||||
embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0)
|
||||
vue: 3.5.20(typescript@5.9.2)
|
||||
|
||||
embla-carousel-wheel-gestures@8.0.2(embla-carousel@8.6.0):
|
||||
embla-carousel-wheel-gestures@8.1.0(embla-carousel@8.6.0):
|
||||
dependencies:
|
||||
embla-carousel: 8.6.0
|
||||
wheel-gestures: 2.2.48
|
||||
@@ -21145,6 +21262,8 @@ snapshots:
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
eventsource-parser@3.0.5: {}
|
||||
|
||||
execa@5.1.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
@@ -21396,14 +21515,6 @@ snapshots:
|
||||
dependencies:
|
||||
pend: 1.2.0
|
||||
|
||||
fdir@6.4.6(picomatch@4.0.2):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
fdir@6.4.6(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
@@ -21522,7 +21633,7 @@ snapshots:
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
ufo: 1.6.1
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
@@ -21577,6 +21688,15 @@ snapshots:
|
||||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
framer-motion@12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
motion-dom: 12.23.12
|
||||
motion-utils: 12.23.6
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
fresh@0.5.2: {}
|
||||
|
||||
fresh@2.0.0: {}
|
||||
@@ -21993,6 +22113,8 @@ snapshots:
|
||||
|
||||
hex-to-rgba@2.0.1: {}
|
||||
|
||||
hey-listen@1.0.8: {}
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
@@ -22141,7 +22263,7 @@ snapshots:
|
||||
exsolve: 1.0.7
|
||||
mocked-exports: 0.1.1
|
||||
pathe: 2.0.3
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
unplugin-utils: 0.2.4
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
@@ -22671,6 +22793,8 @@ snapshots:
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-schema@0.4.0: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json-stringify-safe@5.0.1: {}
|
||||
@@ -23036,7 +23160,7 @@ snapshots:
|
||||
regexp-tree: 0.1.27
|
||||
type-level-regexp: 0.1.17
|
||||
ufo: 1.6.1
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
|
||||
magic-string-ast@1.0.0:
|
||||
dependencies:
|
||||
@@ -23227,6 +23351,24 @@ snapshots:
|
||||
|
||||
module-details-from-path@1.0.3: {}
|
||||
|
||||
motion-dom@12.23.12:
|
||||
dependencies:
|
||||
motion-utils: 12.23.6
|
||||
|
||||
motion-utils@12.23.6: {}
|
||||
|
||||
motion-v@1.7.0(@vueuse/core@13.8.0(vue@3.5.20(typescript@5.9.2)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vue@3.5.20(typescript@5.9.2)):
|
||||
dependencies:
|
||||
'@vueuse/core': 13.8.0(vue@3.5.20(typescript@5.9.2))
|
||||
framer-motion: 12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
hey-listen: 1.0.8
|
||||
motion-dom: 12.23.12
|
||||
vue: 3.5.20(typescript@5.9.2)
|
||||
transitivePeerDependencies:
|
||||
- '@emotion/is-prop-valid'
|
||||
- react
|
||||
- react-dom
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
@@ -23473,7 +23615,7 @@ snapshots:
|
||||
|
||||
node-source-walk@7.0.1:
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.0
|
||||
'@babel/parser': 7.28.3
|
||||
|
||||
node-window-polyfill@1.0.4:
|
||||
dependencies:
|
||||
@@ -24166,6 +24308,8 @@ snapshots:
|
||||
|
||||
pify@3.0.0: {}
|
||||
|
||||
pify@6.1.0: {}
|
||||
|
||||
pinia@3.0.3(typescript@5.9.2)(vue@3.5.20(typescript@5.9.2)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.2
|
||||
@@ -25810,6 +25954,10 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
swrv@1.1.0(vue@3.5.20(typescript@5.9.2)):
|
||||
dependencies:
|
||||
vue: 3.5.20(typescript@5.9.2)
|
||||
|
||||
symbol-observable@1.2.0: {}
|
||||
|
||||
symbol-observable@4.0.0: {}
|
||||
@@ -25930,8 +26078,8 @@ snapshots:
|
||||
|
||||
tinyglobby@0.2.14:
|
||||
dependencies:
|
||||
fdir: 6.4.6(picomatch@4.0.2)
|
||||
picomatch: 4.0.2
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tinypool@1.1.1: {}
|
||||
|
||||
@@ -26170,7 +26318,7 @@ snapshots:
|
||||
acorn: 8.15.0
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
|
||||
undefsafe@2.0.5: {}
|
||||
|
||||
@@ -26216,16 +26364,16 @@ snapshots:
|
||||
acorn: 8.15.0
|
||||
escape-string-regexp: 5.0.0
|
||||
estree-walker: 3.0.3
|
||||
local-pkg: 1.1.1
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.17
|
||||
mlly: 1.7.4
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
pkg-types: 2.2.0
|
||||
pkg-types: 2.3.0
|
||||
scule: 1.3.0
|
||||
strip-literal: 3.0.0
|
||||
tinyglobby: 0.2.14
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
unplugin-utils: 0.2.4
|
||||
|
||||
unimport@5.2.0:
|
||||
@@ -26242,7 +26390,7 @@ snapshots:
|
||||
scule: 1.3.0
|
||||
strip-literal: 3.0.0
|
||||
tinyglobby: 0.2.14
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
unplugin-utils: 0.2.4
|
||||
|
||||
union@0.5.0:
|
||||
@@ -26261,11 +26409,11 @@ snapshots:
|
||||
|
||||
unplugin-auto-import@19.3.0(@nuxt/kit@4.0.3(magicast@0.3.5))(@vueuse/core@13.8.0(vue@3.5.20(typescript@5.9.2))):
|
||||
dependencies:
|
||||
local-pkg: 1.1.1
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.17
|
||||
picomatch: 4.0.3
|
||||
unimport: 4.2.0
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
unplugin-utils: 0.2.4
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 4.0.3(magicast@0.3.5)
|
||||
@@ -26294,11 +26442,11 @@ snapshots:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
local-pkg: 1.1.1
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.17
|
||||
mlly: 1.7.4
|
||||
tinyglobby: 0.2.14
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
unplugin-utils: 0.2.4
|
||||
vue: 3.5.20(typescript@5.9.2)
|
||||
optionalDependencies:
|
||||
@@ -26323,7 +26471,7 @@ snapshots:
|
||||
picomatch: 4.0.3
|
||||
scule: 1.3.0
|
||||
tinyglobby: 0.2.14
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.8
|
||||
unplugin-utils: 0.2.4
|
||||
yaml: 2.8.0
|
||||
optionalDependencies:
|
||||
@@ -27043,7 +27191,7 @@ snapshots:
|
||||
|
||||
with@7.0.2:
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.0
|
||||
'@babel/parser': 7.28.3
|
||||
'@babel/types': 7.28.0
|
||||
assert-never: 1.4.0
|
||||
babel-walk: 3.0.0-canary-5
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
VITE_ACCOUNT=http://localhost:5555
|
||||
VITE_ACCOUNT=https://account.unraid.net
|
||||
VITE_CONNECT=https://connect.myunraid.net
|
||||
VITE_UNRAID_NET=https://unraid.ddev.site
|
||||
VITE_OS_RELEASES="https://releases.unraid.net/os"
|
||||
@@ -14,3 +14,5 @@ VITE_WEBGUI=http://localhost:3001
|
||||
VITE_CSRF_TOKEN="0000000000000000"
|
||||
# Flag for mocking a user session during development via an unsecure cookie
|
||||
VITE_MOCK_USER_SESSION=true
|
||||
# Get the license key from one of the devs
|
||||
NUXT_UI_PRO_LICENSE=
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
VITE_ACCOUNT=https://account.unraid.net
|
||||
VITE_CONNECT=https://connect.myunraid.net
|
||||
VITE_UNRAID_NET=https://unraid.net
|
||||
VITE_CALLBACK_KEY=Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB
|
||||
3
web/.gitignore
vendored
3
web/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.env.*
|
||||
!.env.staging
|
||||
!.env.production
|
||||
!.env.example
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
export default {
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'primary',
|
||||
},
|
||||
primary: 'blue',
|
||||
neutral: 'gray'
|
||||
}
|
||||
},
|
||||
toaster: {
|
||||
position: 'bottom-right' as const,
|
||||
expand: true,
|
||||
duration: 5000
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { NuxtLayout, NuxtPage, UApp } from '#components';
|
||||
import { devConfig } from '~/helpers/env';
|
||||
|
||||
onMounted(() => {
|
||||
@@ -28,9 +27,9 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<div>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UApp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@import 'tailwindcss/utilities.css';
|
||||
@import 'tw-animate-css';
|
||||
@import '../../@tailwind-shared/index.css';
|
||||
|
||||
@import '@nuxt/ui';
|
||||
|
||||
/* Scan unraid-ui package from linked directory for class usage */
|
||||
|
||||
62
web/auto-imports.d.ts
vendored
Normal file
62
web/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const avatarGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['avatarGroupInjectionKey']
|
||||
const defineLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['defineLocale']
|
||||
const defineShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['defineShortcuts']
|
||||
const extendLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['extendLocale']
|
||||
const extractShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts']
|
||||
const fieldGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey']
|
||||
const formBusInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey']
|
||||
const formFieldInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey']
|
||||
const formInputsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey']
|
||||
const formLoadingInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey']
|
||||
const formOptionsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey']
|
||||
const inputIdInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey']
|
||||
const kbdKeysMap: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap']
|
||||
const localeContextInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey']
|
||||
const portalTargetInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey']
|
||||
const useAppConfig: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig']
|
||||
const useAvatarGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup']
|
||||
const useComponentIcons: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons']
|
||||
const useContentSearch: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js')['useContentSearch']
|
||||
const useFieldGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['useFieldGroup']
|
||||
const useFileUpload: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js')['useFileUpload']
|
||||
const useFormField: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['useFormField']
|
||||
const useKbd: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['useKbd']
|
||||
const useLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['useLocale']
|
||||
const useOverlay: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js')['useOverlay']
|
||||
const usePortal: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['usePortal']
|
||||
const useResizable: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js')['useResizable']
|
||||
const useScrollspy: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js')['useScrollspy']
|
||||
const useToast: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js')['useToast']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d')
|
||||
// @ts-ignore
|
||||
export type { UseComponentIconsProps } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d')
|
||||
// @ts-ignore
|
||||
export type { UseFileUploadOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d')
|
||||
// @ts-ignore
|
||||
export type { KbdKey, KbdKeySpecific } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d')
|
||||
// @ts-ignore
|
||||
export type { OverlayOptions, Overlay } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d')
|
||||
// @ts-ignore
|
||||
export type { UseResizableProps, UseResizableReturn } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d')
|
||||
// @ts-ignore
|
||||
export type { Toast } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d')
|
||||
}
|
||||
25
web/components.d.ts
vendored
Normal file
25
web/components.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
|
||||
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
|
||||
UCard: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
|
||||
UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
|
||||
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
|
||||
UDropdownMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
|
||||
UFormField: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
|
||||
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
|
||||
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||
UNavigationMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
|
||||
USelectMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
||||
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
|
||||
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
||||
}
|
||||
}
|
||||
59
web/components/Docker/Console.vue
Normal file
59
web/components/Docker/Console.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface Props {
|
||||
item: {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
};
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const command = ref('');
|
||||
const output = ref<string[]>([
|
||||
`root@${props.item.id}:/# echo "Welcome to ${props.item.label}"`,
|
||||
`Welcome to ${props.item.label}`,
|
||||
`root@${props.item.id}:/#`,
|
||||
]);
|
||||
|
||||
const executeCommand = () => {
|
||||
if (command.value.trim()) {
|
||||
output.value.push(`root@${props.item.id}:/# ${command.value}`);
|
||||
output.value.push(`${command.value}: command executed`);
|
||||
output.value.push(`root@${props.item.id}:/#`);
|
||||
command.value = '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center sm:mx-4">
|
||||
<h3 class="text-lg font-medium">Terminal</h3>
|
||||
<div class="flex gap-2">
|
||||
<UButton size="sm" color="primary" variant="outline" icon="i-lucide-maximize-2">
|
||||
<span class="hidden sm:inline">Fullscreen</span>
|
||||
</UButton>
|
||||
<UButton size="sm" color="primary" variant="outline" icon="i-lucide-refresh-cw">
|
||||
<span class="hidden sm:inline">Restart</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-black text-green-400 p-4 rounded-lg font-mono text-sm h-96 overflow-y-auto sm:mx-4">
|
||||
<div v-for="(line, index) in output" :key="index">
|
||||
{{ line }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span>root@{{ item.id }}:/# </span>
|
||||
<input
|
||||
v-model="command"
|
||||
class="bg-transparent outline-none flex-1 ml-1"
|
||||
type="text"
|
||||
@keyup.enter="executeCommand"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
126
web/components/Docker/Edit.vue
Normal file
126
web/components/Docker/Edit.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface Props {
|
||||
item: {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
};
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const config = ref({
|
||||
name: props.item.label,
|
||||
image: 'ghcr.io/imagegenius/immich:latest',
|
||||
network: 'bridge',
|
||||
restartPolicy: 'unless-stopped',
|
||||
cpuLimit: '',
|
||||
memoryLimit: '',
|
||||
ports: [{ container: '7878', host: '7878', protocol: 'tcp' }],
|
||||
volumes: [
|
||||
{ container: '/config', host: '/mnt/user/appdata/immich' },
|
||||
{ container: '/media', host: '/mnt/user/media' },
|
||||
],
|
||||
environment: [
|
||||
{ key: 'PUID', value: '99' },
|
||||
{ key: 'PGID', value: '100' },
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center sm:mx-4">
|
||||
<h3 class="text-lg font-medium">Container Configuration</h3>
|
||||
<div class="flex gap-2">
|
||||
<UButton color="primary" variant="outline">Cancel</UButton>
|
||||
<UButton color="primary">Save Changes</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UCard class="sm:mx-4">
|
||||
<template #header>
|
||||
<h4 class="font-medium">Basic Settings</h4>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<UFormField label="Container Name">
|
||||
<UInput v-model="config.name" />
|
||||
</UFormField>
|
||||
<UFormField label="Image">
|
||||
<UInput v-model="config.image" />
|
||||
</UFormField>
|
||||
<UFormField label="Network Mode">
|
||||
<USelectMenu v-model="config.network" :options="['bridge', 'host', 'none', 'custom']" />
|
||||
</UFormField>
|
||||
<UFormField label="Restart Policy">
|
||||
<USelectMenu
|
||||
v-model="config.restartPolicy"
|
||||
:options="['no', 'always', 'unless-stopped', 'on-failure']"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="sm:mx-4">
|
||||
<template #header>
|
||||
<h4 class="font-medium">Resource Limits</h4>
|
||||
</template>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormField label="CPU Limit">
|
||||
<UInput v-model="config.cpuLimit" placeholder="e.g., 0.5 or 2" />
|
||||
</UFormField>
|
||||
<UFormField label="Memory Limit">
|
||||
<UInput v-model="config.memoryLimit" placeholder="e.g., 512m or 2g" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="sm:mx-4">
|
||||
<template #header>
|
||||
<h4 class="font-medium">Port Mappings</h4>
|
||||
</template>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(port, index) in config.ports" :key="index" class="flex gap-2 items-center">
|
||||
<UInput v-model="port.host" placeholder="Host Port" class="flex-1" />
|
||||
<UIcon name="i-lucide-arrow-right" class="text-gray-400" />
|
||||
<UInput v-model="port.container" placeholder="Container Port" class="flex-1" />
|
||||
<USelectMenu v-model="port.protocol" :options="['tcp', 'udp']" class="w-24" />
|
||||
<UButton icon="i-lucide-trash-2" color="primary" variant="ghost" size="sm" />
|
||||
</div>
|
||||
<UButton icon="i-lucide-plus" size="sm" variant="outline">Add Port</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="sm:mx-4">
|
||||
<template #header>
|
||||
<h4 class="font-medium">Volume Mappings</h4>
|
||||
</template>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(volume, index) in config.volumes" :key="index" class="flex gap-2 items-center">
|
||||
<UInput v-model="volume.host" placeholder="Host Path" class="flex-1" />
|
||||
<UIcon name="i-lucide-arrow-right" class="text-gray-400" />
|
||||
<UInput v-model="volume.container" placeholder="Container Path" class="flex-1" />
|
||||
<UButton icon="i-lucide-trash-2" color="primary" variant="ghost" size="sm" />
|
||||
</div>
|
||||
<UButton icon="i-lucide-plus" size="sm" variant="outline">Add Volume</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h4 class="font-medium">Environment Variables</h4>
|
||||
</template>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(env, index) in config.environment" :key="index" class="flex gap-2 items-center">
|
||||
<UInput v-model="env.key" placeholder="Variable Name" class="flex-1" />
|
||||
<UInput v-model="env.value" placeholder="Value" class="flex-1" />
|
||||
<UButton icon="i-lucide-trash-2" color="primary" variant="ghost" size="sm" />
|
||||
</div>
|
||||
<UButton icon="i-lucide-plus" size="sm" variant="outline">Add Variable</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
63
web/components/Docker/HeaderContent.vue
Normal file
63
web/components/Docker/HeaderContent.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
autostartValue?: boolean;
|
||||
showAutostart?: boolean;
|
||||
manageActions?: Array<Array<{ label: string; icon: string; onClick?: () => void }>>;
|
||||
}
|
||||
|
||||
const _props = withDefaults(defineProps<Props>(), {
|
||||
autostartValue: true,
|
||||
showAutostart: true,
|
||||
manageActions: () => [
|
||||
[
|
||||
{ label: 'Start', icon: 'i-lucide-play' },
|
||||
{ label: 'Stop', icon: 'i-lucide-square' },
|
||||
{ label: 'Pause', icon: 'i-lucide-pause' },
|
||||
{ label: 'Restart', icon: 'i-lucide-refresh-cw' },
|
||||
],
|
||||
[
|
||||
{ label: 'Update', icon: 'i-lucide-download' },
|
||||
{ label: 'Force Update', icon: 'i-lucide-download-cloud' },
|
||||
{ label: 'Remove', icon: 'i-lucide-trash-2' },
|
||||
],
|
||||
[
|
||||
{ label: 'Docker Allocations', icon: 'i-lucide-hard-drive' },
|
||||
],
|
||||
[
|
||||
{ label: 'Project Page', icon: 'i-lucide-external-link' },
|
||||
{ label: 'Support', icon: 'i-lucide-help-circle' },
|
||||
{ label: 'More Info', icon: 'i-lucide-info' },
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:autostart': [value: boolean];
|
||||
manageAction: [action: string];
|
||||
}>();
|
||||
|
||||
const handleManageAction = (action: { label: string; icon: string }) => {
|
||||
emit('manageAction', action.label);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-3 sm:gap-6">
|
||||
<div v-if="showAutostart" class="flex items-center gap-2 sm:gap-3">
|
||||
<span class="text-xs sm:text-sm font-medium">Autostart</span>
|
||||
<USwitch :model-value="autostartValue" @update:model-value="$emit('update:autostart', $event)" />
|
||||
</div>
|
||||
|
||||
<UDropdownMenu
|
||||
:items="manageActions.map(group => group.map(action => ({
|
||||
...action,
|
||||
onSelect: () => handleManageAction(action)
|
||||
})))"
|
||||
size="md"
|
||||
>
|
||||
<UButton variant="subtle" color="primary" size="sm" trailing-icon="i-lucide-chevron-down">
|
||||
Manage
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</template>
|
||||
34
web/components/Docker/Logs.vue
Normal file
34
web/components/Docker/Logs.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
item: {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
};
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const sampleLogs = [
|
||||
{ timestamp: '2024-01-22 10:15:23', message: `Starting ${props.item.label}...` },
|
||||
{ timestamp: '2024-01-22 10:15:24', message: 'Container initialized successfully' },
|
||||
{ timestamp: '2024-01-22 10:15:25', message: 'Listening on configured port' },
|
||||
{ timestamp: '2024-01-22 10:15:26', message: 'Health check passed' },
|
||||
{ timestamp: '2024-01-22 10:15:27', message: 'Ready to accept connections' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center sm:mx-4">
|
||||
<h3 class="text-lg font-medium">Container Logs</h3>
|
||||
<UButton size="sm" color="primary" variant="outline" icon="i-lucide-download"> Export </UButton>
|
||||
</div>
|
||||
<div class="bg-gray-900 text-gray-100 p-4 rounded-lg font-mono sm:mx-4 text-sm overflow-x-auto">
|
||||
<div v-for="(log, index) in sampleLogs" :key="index" class="whitespace-nowrap">
|
||||
<span class="text-gray-500">[{{ log.timestamp }}]</span> {{ log.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
66
web/components/Docker/Overview.vue
Normal file
66
web/components/Docker/Overview.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
interface ContainerDetails {
|
||||
network: string;
|
||||
lanIpPort: string;
|
||||
containerIp: string;
|
||||
uptime: string;
|
||||
containerPort: string;
|
||||
creationDate: string;
|
||||
containerId: string;
|
||||
maintainer: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item: {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
};
|
||||
details?: ContainerDetails;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="details" class="space-y-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:mx-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Network:</p>
|
||||
<p class="mt-1">{{ details.network }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">LAN IP:Port</p>
|
||||
<p class="mt-1">{{ details.lanIpPort }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Container IP:</p>
|
||||
<p class="mt-1">{{ details.containerIp }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Uptime:</p>
|
||||
<p class="mt-1">{{ details.uptime }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Container Port:</p>
|
||||
<p class="mt-1">{{ details.containerPort }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Creation Date:</p>
|
||||
<p class="mt-1">{{ details.creationDate }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Container Id:</p>
|
||||
<p class="mt-1 font-mono text-sm">{{ details.containerId }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Maintainer:</p>
|
||||
<p class="mt-1 text-sm">{{ details.maintainer }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-500 dark:text-gray-400 sm:mx-4">
|
||||
No details available for {{ item.label }}
|
||||
</div>
|
||||
</template>
|
||||
56
web/components/Docker/Preview.vue
Normal file
56
web/components/Docker/Preview.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
item: {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
};
|
||||
port?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const previewUrl = props.port ? `http://localhost:${props.port}` : null;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2 sm:mx-4">
|
||||
<h3 class="text-lg font-medium">Web Preview</h3>
|
||||
<div class="flex gap-2">
|
||||
<UButton
|
||||
v-if="previewUrl"
|
||||
size="sm"
|
||||
color="primary"
|
||||
variant="outline"
|
||||
icon="i-lucide-external-link"
|
||||
:to="previewUrl"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="hidden sm:inline">Open in new tab</span>
|
||||
</UButton>
|
||||
<UButton size="sm" color="primary" variant="outline" icon="i-lucide-refresh-cw">
|
||||
<span class="hidden sm:inline">Refresh</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden sm:mx-4">
|
||||
<div v-if="previewUrl" class="bg-gray-100 dark:bg-gray-800 px-4 py-2 flex items-center gap-2">
|
||||
<UIcon name="i-lucide-lock" class="w-4 h-4 text-gray-500" />
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ previewUrl }}</span>
|
||||
</div>
|
||||
<div class="p-8 text-center h-96 flex items-center justify-center">
|
||||
<div v-if="previewUrl" class="text-gray-500 dark:text-gray-400">
|
||||
<UIcon name="i-lucide-globe" class="w-16 h-16 mx-auto mb-4" />
|
||||
<p>Web interface preview for {{ item.label }}</p>
|
||||
<p class="text-sm mt-2">Container must be running and accessible on port {{ port }}</p>
|
||||
</div>
|
||||
<div v-else class="text-gray-500 dark:text-gray-400">
|
||||
<UIcon name="i-lucide-alert-circle" class="w-16 h-16 mx-auto mb-4" />
|
||||
<p>No web interface available for {{ item.label }}</p>
|
||||
<p class="text-sm mt-2">This container does not expose a web interface</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
191
web/components/LayoutViews/Card/Card.vue
Normal file
191
web/components/LayoutViews/Card/Card.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import CardGrid from './CardGrid.vue';
|
||||
import CardHeader from './CardHeader.vue';
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
slot?: string;
|
||||
status?: {
|
||||
label: string;
|
||||
dotColor: string;
|
||||
}[];
|
||||
children?: Item[];
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
export interface TabItem {
|
||||
key: string;
|
||||
label: string;
|
||||
component?: Component;
|
||||
props?: Record<string, unknown>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items?: Item[];
|
||||
tabs?: TabItem[];
|
||||
defaultItemId?: string;
|
||||
defaultTabKey?: string;
|
||||
navigationLabel?: string;
|
||||
showFilter?: boolean;
|
||||
showGrouping?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
tabs: () => [],
|
||||
defaultItemId: undefined,
|
||||
defaultTabKey: undefined,
|
||||
navigationLabel: 'Docker Overview',
|
||||
showFilter: true,
|
||||
showGrouping: true,
|
||||
});
|
||||
|
||||
const selectedItemId = ref(props.defaultItemId || props.items[0]?.id || '');
|
||||
const selectedItems = ref<string[]>([]);
|
||||
const expandedGroups = ref<Record<string, boolean>>({});
|
||||
const filterQuery = ref('');
|
||||
const groupBy = ref<string>('none');
|
||||
const autostartStates = ref<Record<string, boolean>>({});
|
||||
const runningStates = ref<Record<string, boolean>>({});
|
||||
|
||||
// Initialize expanded state for groups
|
||||
const initializeExpandedState = () => {
|
||||
props.items.forEach((item) => {
|
||||
if (item.isGroup) {
|
||||
expandedGroups.value[item.id] = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
initializeExpandedState();
|
||||
|
||||
watch(
|
||||
() => props.items,
|
||||
() => {
|
||||
initializeExpandedState();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
if (!filterQuery.value) return props.items;
|
||||
|
||||
const query = filterQuery.value.toLowerCase();
|
||||
return props.items.filter((item) => {
|
||||
const matchesItem = item.label.toLowerCase().includes(query);
|
||||
const matchesChildren = item.children?.some((child) => child.label.toLowerCase().includes(query));
|
||||
return matchesItem || matchesChildren;
|
||||
});
|
||||
});
|
||||
|
||||
const groupedItems = computed(() => {
|
||||
if (groupBy.value === 'none') {
|
||||
return filteredItems.value;
|
||||
}
|
||||
|
||||
// For now, return items as-is since grouping logic depends on data structure
|
||||
return filteredItems.value;
|
||||
});
|
||||
|
||||
// Reusable function to collect all selectable items
|
||||
const collectSelectableItems = (items: Item[]): string[] => {
|
||||
const selectableItems: string[] = [];
|
||||
|
||||
const collect = (items: Item[]) => {
|
||||
for (const item of items) {
|
||||
if (!item.isGroup) {
|
||||
selectableItems.push(item.id);
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
collect(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
collect(items);
|
||||
|
||||
return selectableItems;
|
||||
};
|
||||
|
||||
const selectAllItems = () => {
|
||||
selectedItems.value = [...collectSelectableItems(props.items)];
|
||||
};
|
||||
|
||||
const clearAllSelections = () => {
|
||||
selectedItems.value = [];
|
||||
};
|
||||
|
||||
const handleAddAction = () => {
|
||||
console.log('Add action triggered');
|
||||
};
|
||||
|
||||
const handleManageSelectedAction = (action: string) => {
|
||||
console.log('Manage selected action:', action);
|
||||
};
|
||||
|
||||
const handleItemSelect = (itemId: string) => {
|
||||
selectedItemId.value = itemId;
|
||||
};
|
||||
|
||||
const handleItemsSelectionUpdate = (items: string[]) => {
|
||||
selectedItems.value = items;
|
||||
};
|
||||
|
||||
const handleAutostartUpdate = (itemId: string, value: boolean) => {
|
||||
console.log('Autostart update for item:', itemId, 'value:', value);
|
||||
autostartStates.value[itemId] = value;
|
||||
};
|
||||
|
||||
const handleToggleRunning = (itemId: string) => {
|
||||
// TODO: Wire up to actual docker/VM start/stop API
|
||||
const currentState = runningStates.value[itemId] || false;
|
||||
runningStates.value[itemId] = !currentState;
|
||||
console.log('Toggle running for item:', itemId, 'new state:', !currentState);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header -->
|
||||
<CardHeader
|
||||
:title="navigationLabel"
|
||||
:filter-query="filterQuery"
|
||||
:group-by="groupBy"
|
||||
:selected-items="selectedItems"
|
||||
:show-filter="showFilter"
|
||||
:show-grouping="showGrouping"
|
||||
@update:filter-query="filterQuery = $event"
|
||||
@update:group-by="groupBy = $event"
|
||||
@add="handleAddAction"
|
||||
@select-all="selectAllItems"
|
||||
@clear-all="clearAllSelections"
|
||||
@manage-action="handleManageSelectedAction"
|
||||
/>
|
||||
|
||||
<!-- Card Grid -->
|
||||
<div class="flex-1 overflow-auto w-full">
|
||||
<CardGrid
|
||||
:items="groupedItems"
|
||||
:selected-items="selectedItems"
|
||||
:selected-item-id="selectedItemId"
|
||||
:expanded-groups="expandedGroups"
|
||||
:autostart-states="autostartStates"
|
||||
:running-states="runningStates"
|
||||
@update:selected-items="handleItemsSelectionUpdate"
|
||||
@update:expanded-groups="expandedGroups = $event"
|
||||
@item-select="handleItemSelect"
|
||||
@update:autostart="handleAutostartUpdate"
|
||||
@toggle-running="handleToggleRunning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
120
web/components/LayoutViews/Card/CardGrid.vue
Normal file
120
web/components/LayoutViews/Card/CardGrid.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { Item } from './Card.vue';
|
||||
|
||||
import CardGroupHeader from './CardGroupHeader.vue';
|
||||
import CardItem from './CardItem.vue';
|
||||
|
||||
interface Props {
|
||||
items: Item[];
|
||||
selectedItems: string[];
|
||||
selectedItemId?: string;
|
||||
expandedGroups: Record<string, boolean>;
|
||||
autostartStates: Record<string, boolean>;
|
||||
runningStates: Record<string, boolean>;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:selectedItems': [items: string[]];
|
||||
'item-select': [itemId: string];
|
||||
'update:expandedGroups': [groups: Record<string, boolean>];
|
||||
'update:autostart': [itemId: string, value: boolean];
|
||||
'toggle-running': [itemId: string];
|
||||
}>();
|
||||
|
||||
const flattenedItems = computed(() => {
|
||||
const result: Array<Item & { isGroupChild?: boolean; parentGroup?: string }> = [];
|
||||
|
||||
for (const item of props.items) {
|
||||
if (item.isGroup && item.children) {
|
||||
// Add group header
|
||||
result.push(item);
|
||||
// Add children only if group is expanded
|
||||
if (props.expandedGroups[item.id]) {
|
||||
for (const child of item.children) {
|
||||
result.push({
|
||||
...child,
|
||||
isGroupChild: true,
|
||||
parentGroup: item.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const toggleItemSelection = (itemId: string) => {
|
||||
const newItems = [...props.selectedItems];
|
||||
const index = newItems.indexOf(itemId);
|
||||
|
||||
if (index > -1) {
|
||||
newItems.splice(index, 1);
|
||||
} else {
|
||||
newItems.push(itemId);
|
||||
}
|
||||
|
||||
emit('update:selectedItems', newItems);
|
||||
};
|
||||
|
||||
const isItemSelected = (itemId: string) => {
|
||||
return props.selectedItems.includes(itemId);
|
||||
};
|
||||
|
||||
const handleItemClick = (itemId: string) => {
|
||||
emit('item-select', itemId);
|
||||
};
|
||||
|
||||
const toggleGroupExpansion = (groupId: string) => {
|
||||
const newGroups = { ...props.expandedGroups };
|
||||
newGroups[groupId] = !newGroups[groupId];
|
||||
emit('update:expandedGroups', newGroups);
|
||||
};
|
||||
|
||||
const handleAutostartUpdate = (itemId: string, value: boolean) => {
|
||||
emit('update:autostart', itemId, value);
|
||||
};
|
||||
|
||||
const handleToggleRunning = (itemId: string) => {
|
||||
emit('toggle-running', itemId);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 w-full">
|
||||
<div class="space-y-4 w-full max-w-full">
|
||||
<template v-for="item in flattenedItems" :key="item.id">
|
||||
<!-- Group Header -->
|
||||
<CardGroupHeader
|
||||
v-if="item.isGroup"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
:badge="item.badge"
|
||||
:is-expanded="expandedGroups[item.id]"
|
||||
@toggle="toggleGroupExpansion(item.id)"
|
||||
/>
|
||||
|
||||
<!-- Regular Card Item -->
|
||||
<CardItem
|
||||
v-else
|
||||
:item="item"
|
||||
:is-selected="isItemSelected(item.id)"
|
||||
:is-active="selectedItemId === item.id"
|
||||
:is-group-child="item.isGroupChild"
|
||||
:autostart-value="autostartStates[item.id] || false"
|
||||
:is-running="runningStates[item.id] || false"
|
||||
@toggle-selection="toggleItemSelection"
|
||||
@click="handleItemClick"
|
||||
@update:autostart="handleAutostartUpdate(item.id, $event)"
|
||||
@toggle-running="handleToggleRunning"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
53
web/components/LayoutViews/Card/CardGroupHeader.vue
Normal file
53
web/components/LayoutViews/Card/CardGroupHeader.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCard
|
||||
class="w-full cursor-pointer transition-all duration-200 hover:shadow-md"
|
||||
@click="$emit('toggle')"
|
||||
>
|
||||
<div class="flex items-center gap-4 py-2">
|
||||
<!-- Icon -->
|
||||
<div class="flex-shrink-0 pl-2">
|
||||
<UIcon v-if="icon" :name="icon" class="h-8 w-8" />
|
||||
<div v-else class="h-8 w-8 rounded flex items-center justify-center">
|
||||
<UIcon name="i-lucide-folder" class="h-5 w-5 text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-lg font-semibold truncate">
|
||||
{{ label }}
|
||||
</h2>
|
||||
<UBadge v-if="badge" size="sm" :label="String(badge)" variant="subtle" />
|
||||
<!-- Edit icon -->
|
||||
<UIcon
|
||||
name="i-lucide-pencil"
|
||||
class="h-4 w-4 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expansion Arrow on right side -->
|
||||
<div class="flex-shrink-0 pr-2">
|
||||
<UIcon
|
||||
name="i-lucide-chevron-right"
|
||||
:class="[
|
||||
'h-5 w-5 text-gray-500 transform transition-transform duration-200',
|
||||
isExpanded ? 'rotate-90' : 'rotate-0',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
134
web/components/LayoutViews/Card/CardHeader.vue
Normal file
134
web/components/LayoutViews/Card/CardHeader.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
filterQuery?: string;
|
||||
groupBy?: string;
|
||||
selectedItems?: string[];
|
||||
showFilter?: boolean;
|
||||
showGrouping?: boolean;
|
||||
manageActions?: Array<Array<{ label: string; icon: string; onClick?: () => void }>>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: 'Docker Overview',
|
||||
filterQuery: '',
|
||||
groupBy: 'none',
|
||||
selectedItems: () => [],
|
||||
showFilter: true,
|
||||
showGrouping: true,
|
||||
manageActions: () => [
|
||||
[
|
||||
{ label: 'Sort Alpha Asc', icon: 'i-lucide-arrow-up-a-z' },
|
||||
{ label: 'Sort Alpha Dec', icon: 'i-lucide-arrow-down-z-a' },
|
||||
],
|
||||
[
|
||||
{ label: 'Start Selected', icon: 'i-lucide-play' },
|
||||
{ label: 'Stop Selected', icon: 'i-lucide-square' },
|
||||
{ label: 'Pause Selected', icon: 'i-lucide-pause' },
|
||||
{ label: 'Restart Selected', icon: 'i-lucide-refresh-cw' },
|
||||
{ label: 'Autostart Selected', icon: 'i-lucide-timer' },
|
||||
],
|
||||
[
|
||||
{ label: 'Check for Updates', icon: 'i-lucide-refresh-ccw' },
|
||||
{ label: 'Update Selected', icon: 'i-lucide-download' },
|
||||
{ label: 'Remove Selected', icon: 'i-lucide-trash-2' },
|
||||
],
|
||||
[{ label: 'Add Container', icon: 'i-lucide-plus' }],
|
||||
],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:filterQuery': [query: string];
|
||||
'update:groupBy': [groupBy: string];
|
||||
add: [];
|
||||
selectAll: [];
|
||||
clearAll: [];
|
||||
manageAction: [action: string];
|
||||
}>();
|
||||
|
||||
const selectedCount = computed(() => props.selectedItems?.length || 0);
|
||||
|
||||
const handleManageAction = (action: { label: string; icon: string }) => {
|
||||
emit('manageAction', action.label);
|
||||
};
|
||||
|
||||
const dropdownItems = computed(() =>
|
||||
props.manageActions.map((group) =>
|
||||
group.map((action) => ({
|
||||
label: action.label,
|
||||
icon: action.icon,
|
||||
onSelect: () => handleManageAction(action),
|
||||
}))
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950">
|
||||
<div class="p-4 space-y-4">
|
||||
<!-- Title Row -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ title }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Controls Row -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||
<!-- Left Side: Filter and Configure View -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Filter Input with Configure View -->
|
||||
<div v-if="showFilter" class="flex items-center gap-2 max-w-sm">
|
||||
<UInput
|
||||
:model-value="filterQuery"
|
||||
placeholder="Filter"
|
||||
icon="i-lucide-search"
|
||||
size="md"
|
||||
class="flex-1"
|
||||
@update:model-value="$emit('update:filterQuery', $event)"
|
||||
/>
|
||||
|
||||
<!-- Configure View Button -->
|
||||
<UButton
|
||||
v-if="showGrouping"
|
||||
color="primary"
|
||||
variant="outline"
|
||||
>
|
||||
Configure View
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: All action buttons -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Select All / Clear All -->
|
||||
<UButton
|
||||
variant="link"
|
||||
color="primary"
|
||||
size="sm"
|
||||
@click="selectedCount > 0 ? $emit('clearAll') : $emit('selectAll')"
|
||||
>
|
||||
{{ selectedCount > 0 ? 'Clear all' : 'Select all' }}
|
||||
</UButton>
|
||||
|
||||
<!-- Manage Selected Dropdown -->
|
||||
<UDropdownMenu :items="dropdownItems" size="md">
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="primary"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
:disabled="selectedCount === 0"
|
||||
>
|
||||
Manage Selected ({{ selectedCount }})
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
|
||||
<!-- Add Folder Button -->
|
||||
<UButton icon="i-lucide-plus" color="primary" variant="solid" @click="$emit('add')">
|
||||
Add Folder
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
136
web/components/LayoutViews/Card/CardItem.vue
Normal file
136
web/components/LayoutViews/Card/CardItem.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import type { Item } from './Card.vue';
|
||||
|
||||
interface Props {
|
||||
item: Item;
|
||||
isSelected: boolean;
|
||||
isActive?: boolean;
|
||||
isGroupChild?: boolean;
|
||||
autostartValue?: boolean;
|
||||
isRunning?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autostartValue: false,
|
||||
isRunning: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleSelection: [itemId: string];
|
||||
click: [itemId: string];
|
||||
'update:autostart': [value: boolean];
|
||||
toggleRunning: [itemId: string];
|
||||
}>();
|
||||
|
||||
const handleCardClick = () => {
|
||||
emit('click', props.item.id);
|
||||
};
|
||||
|
||||
const handleCheckboxClick = (_value: boolean | 'indeterminate') => {
|
||||
emit('toggleSelection', props.item.id);
|
||||
};
|
||||
|
||||
const handleToggleRunning = () => {
|
||||
// TODO: Wire up to actual start/stop functionality for docker containers and VMs
|
||||
console.log('Toggle running state for:', props.item.id, 'Current state:', props.isRunning);
|
||||
emit('toggleRunning', props.item.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="isGroupChild ? 'ml-4' : ''">
|
||||
<UCard
|
||||
:class="[
|
||||
'w-full cursor-pointer transition-all duration-200 hover:shadow-md group',
|
||||
isActive ? 'ring-2 ring-primary-500' : '',
|
||||
]"
|
||||
@click="handleCardClick"
|
||||
>
|
||||
<div class="flex items-center gap-4 py-2">
|
||||
<!-- Selection Checkbox -->
|
||||
<div class="flex-shrink-0 pl-2 flex items-center">
|
||||
<UCheckbox :model-value="isSelected" @update:model-value="handleCheckboxClick" @click.stop />
|
||||
</div>
|
||||
|
||||
<!-- Icon -->
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<UIcon v-if="item.icon" :name="item.icon" class="h-8 w-8" />
|
||||
<div v-else class="h-8 w-8 rounded flex items-center justify-center">
|
||||
<UIcon name="i-lucide-box" class="h-5 w-5 text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Play/Stop Button -->
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<button
|
||||
class="p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors flex items-center justify-center"
|
||||
:aria-label="isRunning ? 'Stop' : 'Start'"
|
||||
@click.stop="handleToggleRunning"
|
||||
>
|
||||
<svg
|
||||
v-if="!isRunning"
|
||||
class="w-4 h-4 fill-green-500 hover:fill-green-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="w-4 h-4 fill-red-500 hover:fill-red-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="6" y="6" width="12" height="12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 min-w-0 flex items-center">
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<h3 class="font-semibold truncate">
|
||||
{{ item.label }}
|
||||
</h3>
|
||||
<UBadge v-if="item.badge" size="xs" :label="String(item.badge)" />
|
||||
<div v-if="item.status && item.status.length > 0" class="flex flex-wrap gap-2 ml-4">
|
||||
<UBadge
|
||||
v-for="(statusItem, index) in item.status"
|
||||
:key="index"
|
||||
variant="subtle"
|
||||
color="neutral"
|
||||
size="sm"
|
||||
class="flex items-center"
|
||||
>
|
||||
<div :class="['h-2 w-2 rounded-full mr-2', statusItem.dotColor]" />
|
||||
{{ statusItem.label }}
|
||||
</UBadge>
|
||||
<div class="text-sm ml-4">Uptime: 10 hours</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side content -->
|
||||
<div class="flex items-center gap-4 flex-shrink-0 pr-2">
|
||||
<!-- Action Buttons - only visible on hover -->
|
||||
<div class="hidden group-hover:flex items-center gap-2">
|
||||
<UButton color="primary" variant="outline" size="sm" @click.stop> Manage </UButton>
|
||||
<UButton color="primary" variant="solid" size="sm" @click.stop> Visit </UButton>
|
||||
</div>
|
||||
|
||||
<!-- Autostart Toggle - only visible on hover -->
|
||||
<div class="hidden group-hover:flex items-center gap-2">
|
||||
<span class="text-sm font-medium">Autostart</span>
|
||||
<USwitch
|
||||
:model-value="autostartValue"
|
||||
@update:model-value="$emit('update:autostart', $event)"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
185
web/components/LayoutViews/Detail/Detail.vue
Normal file
185
web/components/LayoutViews/Detail/Detail.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import HeaderContent from '../../Docker/HeaderContent.vue';
|
||||
import DetailContentHeader from './DetailContentHeader.vue';
|
||||
import DetailLeftNavigation from './DetailLeftNavigation.vue';
|
||||
import DetailRightContent from './DetailRightContent.vue';
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
slot?: string;
|
||||
status?: {
|
||||
label: string;
|
||||
dotColor: string;
|
||||
}[];
|
||||
children?: Item[];
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
export interface TabItem {
|
||||
key: string;
|
||||
label: string;
|
||||
component?: Component;
|
||||
props?: Record<string, unknown>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items?: Item[];
|
||||
tabs?: TabItem[];
|
||||
defaultItemId?: string;
|
||||
defaultTabKey?: string;
|
||||
navigationLabel?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
tabs: () => [],
|
||||
defaultItemId: undefined,
|
||||
defaultTabKey: undefined,
|
||||
navigationLabel: 'Select Item',
|
||||
});
|
||||
|
||||
const selectedItemId = ref(props.defaultItemId || props.items[0]?.id || '');
|
||||
const selectedTab = ref(props.defaultTabKey || '0');
|
||||
const selectedItems = ref<string[]>([]);
|
||||
const expandedGroups = ref<Record<string, boolean>>({});
|
||||
const autostartEnabled = ref(true);
|
||||
|
||||
// Initialize expanded state for groups
|
||||
const initializeExpandedState = () => {
|
||||
props.items.forEach((item) => {
|
||||
if (item.isGroup) {
|
||||
expandedGroups.value[item.id] = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
initializeExpandedState();
|
||||
|
||||
watch(
|
||||
() => props.items,
|
||||
() => {
|
||||
initializeExpandedState();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const selectedItem = computed(() => {
|
||||
const topLevel = props.items.find((item) => item.id === selectedItemId.value);
|
||||
|
||||
if (topLevel) return topLevel;
|
||||
|
||||
for (const item of props.items) {
|
||||
if (item.children) {
|
||||
const nested = item.children.find((child) => child.id === selectedItemId.value);
|
||||
|
||||
if (nested) return nested;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// Reusable function to collect all selectable items
|
||||
const collectSelectableItems = (items: Item[]): string[] => {
|
||||
const selectableItems: string[] = [];
|
||||
|
||||
const collect = (items: Item[]) => {
|
||||
for (const item of items) {
|
||||
if (!item.isGroup) {
|
||||
selectableItems.push(item.id);
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
collect(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
collect(items);
|
||||
|
||||
return selectableItems;
|
||||
};
|
||||
|
||||
const selectAllItems = () => {
|
||||
selectedItems.value = [...collectSelectableItems(props.items)];
|
||||
};
|
||||
|
||||
const clearAllSelections = () => {
|
||||
selectedItems.value = [];
|
||||
};
|
||||
|
||||
const handleAddAction = () => {
|
||||
console.log('Add action triggered');
|
||||
};
|
||||
|
||||
const handleManageSelectedAction = (action: string) => {
|
||||
console.log('Manage selected action:', action);
|
||||
};
|
||||
|
||||
const handleManageItemAction = (action: string) => {
|
||||
console.log('Manage item action:', action);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col p-4 lg:flex-row h-full">
|
||||
<!-- Navigation -->
|
||||
<DetailLeftNavigation
|
||||
:items="items"
|
||||
:selected-id="selectedItemId"
|
||||
:selected-items="selectedItems"
|
||||
:expanded-groups="expandedGroups"
|
||||
:navigation-label="navigationLabel"
|
||||
@update:selected-id="selectedItemId = $event"
|
||||
@update:selected-items="selectedItems = $event"
|
||||
@update:expanded-groups="expandedGroups = $event"
|
||||
@add="handleAddAction"
|
||||
@select-all="selectAllItems"
|
||||
@clear-all="clearAllSelections"
|
||||
@manage-action="handleManageSelectedAction"
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<DetailRightContent
|
||||
:selected-item="selectedItem"
|
||||
:tabs="tabs"
|
||||
:selected-tab="selectedTab"
|
||||
@update:selected-tab="selectedTab = $event"
|
||||
>
|
||||
<template #header="{ item }">
|
||||
<DetailContentHeader :icon="item.icon" :title="item.label">
|
||||
<template #right-content>
|
||||
<template v-if="item.status && item.status.length > 0">
|
||||
<UBadge
|
||||
v-for="(statusItem, index) in item.status"
|
||||
:key="index"
|
||||
variant="subtle"
|
||||
color="neutral"
|
||||
size="sm"
|
||||
>
|
||||
<div :class="['h-2 w-2 rounded-full mr-2', statusItem.dotColor]" />
|
||||
{{ statusItem.label }}
|
||||
</UBadge>
|
||||
</template>
|
||||
</template>
|
||||
<template #controls>
|
||||
<HeaderContent
|
||||
:autostart-value="autostartEnabled"
|
||||
@update:autostart="autostartEnabled = $event"
|
||||
@manage-action="handleManageItemAction"
|
||||
/>
|
||||
</template>
|
||||
</DetailContentHeader>
|
||||
</template>
|
||||
</DetailRightContent>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
28
web/components/LayoutViews/Detail/DetailContentHeader.vue
Normal file
28
web/components/LayoutViews/Detail/DetailContentHeader.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
icon?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const _props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 sm:gap-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-2 sm:gap-4 lg:mb-0">
|
||||
<UIcon v-if="icon" :name="icon" class="h-8 w-8 flex-shrink-0" />
|
||||
<h1 class="text-2xl font-semibold truncate leading-none">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<slot name="right-content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center flex-shrink-0">
|
||||
<slot name="controls" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
357
web/components/LayoutViews/Detail/DetailLeftNavigation.vue
Normal file
357
web/components/LayoutViews/Detail/DetailLeftNavigation.vue
Normal file
@@ -0,0 +1,357 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import type { Item } from './Detail.vue';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
items: Item[];
|
||||
selectedId: string;
|
||||
selectedItems: string[];
|
||||
expandedGroups: Record<string, boolean>;
|
||||
showHeader?: boolean;
|
||||
manageActions?: Array<Array<{ label: string; icon: string; onClick?: () => void }>>;
|
||||
navigationLabel?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: 'Service Name',
|
||||
showHeader: true,
|
||||
manageActions: () => [
|
||||
[
|
||||
{ label: 'Sort Alpha Asc', icon: 'i-lucide-arrow-up-a-z' },
|
||||
{ label: 'Sort Alpha Dec', icon: 'i-lucide-arrow-down-z-a' },
|
||||
],
|
||||
[
|
||||
{ label: 'Start Selected', icon: 'i-lucide-play' },
|
||||
{ label: 'Stop Selected', icon: 'i-lucide-square' },
|
||||
{ label: 'Pause Selected', icon: 'i-lucide-pause' },
|
||||
{ label: 'Restart Selected', icon: 'i-lucide-refresh-cw' },
|
||||
{ label: 'Autostart Selected', icon: 'i-lucide-timer' },
|
||||
],
|
||||
[
|
||||
{ label: 'Check for Updates', icon: 'i-lucide-refresh-ccw' },
|
||||
{ label: 'Update Selected', icon: 'i-lucide-download' },
|
||||
{ label: 'Remove Selected', icon: 'i-lucide-trash-2' },
|
||||
],
|
||||
[{ label: 'Add Container', icon: 'i-lucide-plus' }],
|
||||
],
|
||||
navigationLabel: 'Select Item',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:selectedId': [id: string];
|
||||
'update:selectedItems': [items: string[]];
|
||||
'update:expandedGroups': [groups: Record<string, boolean>];
|
||||
add: [];
|
||||
selectAll: [];
|
||||
clearAll: [];
|
||||
manageAction: [action: string];
|
||||
}>();
|
||||
|
||||
// Internal drawer state for mobile
|
||||
const sidebarOpen = ref(false);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
sidebarOpen.value = !sidebarOpen.value;
|
||||
};
|
||||
|
||||
const navigationMenuItems = computed(() =>
|
||||
props.items.map((item) => ({
|
||||
label: item.label,
|
||||
icon: item.icon,
|
||||
id: item.id,
|
||||
badge: String(item.badge || ''),
|
||||
slot: item.slot,
|
||||
...(item.isGroup ? {} : { onClick: () => selectNavigationItem(item.id) }),
|
||||
isGroup: item.isGroup,
|
||||
status: item.status,
|
||||
...(item.isGroup ? {} : { to: '#' }),
|
||||
defaultOpen: item.isGroup ? true : undefined,
|
||||
children: item.children?.map((child) => ({
|
||||
label: child.label,
|
||||
icon: child.icon,
|
||||
id: child.id,
|
||||
badge: String(child.badge || ''),
|
||||
slot: child.slot,
|
||||
onClick: () => selectNavigationItem(child.id),
|
||||
status: child.status,
|
||||
to: '#',
|
||||
})),
|
||||
}))
|
||||
);
|
||||
|
||||
interface NavigationMenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge: string;
|
||||
slot?: string;
|
||||
onClick?: () => void;
|
||||
isGroup?: boolean;
|
||||
status?: {
|
||||
label: string;
|
||||
dotColor: string;
|
||||
}[];
|
||||
children?: NavigationMenuItem[];
|
||||
to?: string;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
const allItemsWithSlots = computed(() => {
|
||||
const items: NavigationMenuItem[] = [];
|
||||
const collectItems = (navItems: NavigationMenuItem[]) => {
|
||||
for (const item of navItems) {
|
||||
if (item.slot) {
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
collectItems(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
collectItems(navigationMenuItems.value);
|
||||
return items;
|
||||
});
|
||||
|
||||
const allItemsSelected = computed(() => {
|
||||
const allSelectableItems: string[] = [];
|
||||
const collectSelectableItems = (items: Item[]) => {
|
||||
for (const item of items) {
|
||||
if (!item.isGroup) {
|
||||
allSelectableItems.push(item.id);
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
collectSelectableItems(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
collectSelectableItems(props.items);
|
||||
|
||||
return (
|
||||
allSelectableItems.length > 0 && allSelectableItems.every((id) => props.selectedItems.includes(id))
|
||||
);
|
||||
});
|
||||
|
||||
const selectedItemsCount = computed(() => props.selectedItems.length);
|
||||
|
||||
const selectNavigationItem = (id: string) => {
|
||||
const actualItem =
|
||||
props.items.find((item) => item.id === id) ||
|
||||
props.items.flatMap((item) => item.children || []).find((child) => child.id === id);
|
||||
|
||||
if (actualItem && !actualItem.isGroup) {
|
||||
emit('update:selectedId', id);
|
||||
sidebarOpen.value = false; // Close drawer on mobile when item is selected
|
||||
}
|
||||
};
|
||||
|
||||
const toggleItemSelection = (itemId: string) => {
|
||||
const newItems = [...props.selectedItems];
|
||||
const index = newItems.indexOf(itemId);
|
||||
|
||||
if (index > -1) {
|
||||
newItems.splice(index, 1);
|
||||
} else {
|
||||
newItems.push(itemId);
|
||||
}
|
||||
|
||||
emit('update:selectedItems', newItems);
|
||||
};
|
||||
|
||||
const isItemSelected = (itemId: string) => {
|
||||
return props.selectedItems.includes(itemId);
|
||||
};
|
||||
|
||||
const toggleGroupExpansion = (groupId: string) => {
|
||||
const newGroups = { ...props.expandedGroups };
|
||||
|
||||
newGroups[groupId] = !newGroups[groupId];
|
||||
emit('update:expandedGroups', newGroups);
|
||||
};
|
||||
|
||||
const handleManageAction = (action: { label: string; icon: string }) => {
|
||||
emit('manageAction', action.label);
|
||||
};
|
||||
|
||||
const dropdownItems = computed(() =>
|
||||
props.manageActions.map((group) =>
|
||||
group.map((action) => ({
|
||||
label: action.label,
|
||||
icon: action.icon,
|
||||
onSelect: () => handleManageAction(action),
|
||||
}))
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Desktop navigation -->
|
||||
<div class="hidden lg:block lg:mr-16">
|
||||
<div class="h-full overflow-y-auto overflow-x-hidden">
|
||||
<!-- Navigation Header -->
|
||||
<div v-if="showHeader" class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-lg font-semibold">{{ title }}</h2>
|
||||
<UButton
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
square
|
||||
@click="$emit('add')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
||||
<UButton
|
||||
variant="link"
|
||||
color="primary"
|
||||
size="sm"
|
||||
:label="allItemsSelected ? 'Clear all' : 'Select all'"
|
||||
@click="allItemsSelected ? $emit('clearAll') : $emit('selectAll')"
|
||||
/>
|
||||
|
||||
<UDropdownMenu :items="dropdownItems" size="md">
|
||||
<UButton
|
||||
variant="subtle"
|
||||
color="primary"
|
||||
size="sm"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
:disabled="selectedItemsCount === 0"
|
||||
class="w-full sm:w-auto"
|
||||
>
|
||||
<span class="sm:hidden">Manage ({{ selectedItemsCount }})</span>
|
||||
<span class="hidden sm:inline">Manage Selected ({{ selectedItemsCount }})</span>
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Menu -->
|
||||
<UNavigationMenu :items="navigationMenuItems" orientation="vertical">
|
||||
<template v-for="item in allItemsWithSlots" :key="`slot-${item.id}`" #[item.slot!]>
|
||||
<div
|
||||
class="flex items-center gap-3 mb-2 min-w-0"
|
||||
@click="
|
||||
item.children && item.children.length > 0 ? toggleGroupExpansion(item.id) : undefined
|
||||
"
|
||||
>
|
||||
<UCheckbox
|
||||
:model-value="isItemSelected(item.id)"
|
||||
class="flex-shrink-0"
|
||||
@update:model-value="toggleItemSelection(item.id)"
|
||||
@click.stop
|
||||
/>
|
||||
<UIcon v-if="item.icon" :name="item.icon" class="h-5 w-5 flex-shrink-0" />
|
||||
<span class="truncate flex-1 min-w-0">{{ item.label }}</span>
|
||||
<UBadge v-if="item.badge" size="xs" :label="String(item.badge)" class="flex-shrink-0" />
|
||||
|
||||
<UIcon
|
||||
v-if="item.children?.length"
|
||||
name="i-lucide-chevron-down"
|
||||
:class="[
|
||||
'h-5 w-5 text-gray-400 transition-transform duration-200 flex-shrink-0',
|
||||
expandedGroups[item.id] ? 'rotate-180' : 'rotate-0',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UNavigationMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile UDrawer -->
|
||||
<div class="lg:hidden">
|
||||
<div class="m-4">
|
||||
<UButton color="primary" size="md" class="w-full justify-center" @click="toggleSidebar">
|
||||
{{ navigationLabel }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UDrawer v-model:open="sidebarOpen" direction="left" size="md">
|
||||
<template #content>
|
||||
<div class="h-full overflow-y-auto overflow-x-hidden p-4">
|
||||
<!-- Navigation Header -->
|
||||
<div v-if="showHeader" class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-lg font-semibold">{{ title }}</h2>
|
||||
<UButton
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
square
|
||||
@click="$emit('add')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
||||
<UButton
|
||||
variant="link"
|
||||
color="primary"
|
||||
size="sm"
|
||||
:label="allItemsSelected ? 'Clear all' : 'Select all'"
|
||||
@click="allItemsSelected ? $emit('clearAll') : $emit('selectAll')"
|
||||
/>
|
||||
|
||||
<UDropdownMenu :items="dropdownItems" size="md">
|
||||
<UButton
|
||||
variant="subtle"
|
||||
color="primary"
|
||||
size="sm"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
:disabled="selectedItemsCount === 0"
|
||||
class="w-full sm:w-auto"
|
||||
>
|
||||
<span class="sm:hidden">Manage ({{ selectedItemsCount }})</span>
|
||||
<span class="hidden sm:inline">Manage Selected ({{ selectedItemsCount }})</span>
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Menu -->
|
||||
<UNavigationMenu :items="navigationMenuItems" orientation="vertical">
|
||||
<template v-for="item in allItemsWithSlots" :key="`slot-${item.id}`" #[item.slot!]>
|
||||
<div
|
||||
class="flex items-center gap-4 mb-2 min-w-0"
|
||||
@click="
|
||||
item.children && item.children.length > 0 ? toggleGroupExpansion(item.id) : undefined
|
||||
"
|
||||
>
|
||||
<UCheckbox
|
||||
:model-value="isItemSelected(item.id)"
|
||||
class="flex-shrink-0"
|
||||
@update:model-value="toggleItemSelection(item.id)"
|
||||
@click.stop
|
||||
/>
|
||||
<UIcon v-if="item.icon" :name="item.icon" class="h-5 w-5 flex-shrink-0" />
|
||||
<span class="truncate flex-1 min-w-0">{{ item.label }}</span>
|
||||
<UBadge
|
||||
v-if="item.badge"
|
||||
size="xs"
|
||||
:label="String(item.badge)"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
|
||||
<UIcon
|
||||
v-if="item.children?.length"
|
||||
name="i-lucide-chevron-down"
|
||||
:class="[
|
||||
'h-5 w-5 text-gray-400 transition-transform duration-200 flex-shrink-0',
|
||||
expandedGroups[item.id] ? 'rotate-180' : 'rotate-0',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UNavigationMenu>
|
||||
</div>
|
||||
</template>
|
||||
</UDrawer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
97
web/components/LayoutViews/Detail/DetailRightContent.vue
Normal file
97
web/components/LayoutViews/Detail/DetailRightContent.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { Item, TabItem } from './Detail.vue';
|
||||
|
||||
interface Props {
|
||||
selectedItem?: Item;
|
||||
tabs: TabItem[];
|
||||
selectedTab: string;
|
||||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
selectedItem: undefined,
|
||||
showHeader: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:selectedTab': [value: string];
|
||||
}>();
|
||||
|
||||
const tabItems = computed(() =>
|
||||
props.tabs.map((tab) => ({
|
||||
label: tab.label,
|
||||
key: tab.key,
|
||||
disabled: tab.disabled,
|
||||
}))
|
||||
);
|
||||
|
||||
const getCurrentTabComponent = () => {
|
||||
const tabIndex = parseInt(props.selectedTab);
|
||||
|
||||
// Validate that tabIndex is a valid number and within bounds
|
||||
if (isNaN(tabIndex) || tabIndex < 0 || tabIndex >= props.tabs.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return props.tabs[tabIndex]?.component || null;
|
||||
};
|
||||
|
||||
const getCurrentTabProps = () => {
|
||||
const tabIndex = parseInt(props.selectedTab);
|
||||
|
||||
// Validate that tabIndex is a valid number and within bounds
|
||||
if (isNaN(tabIndex) || tabIndex < 0 || tabIndex >= props.tabs.length) {
|
||||
return {
|
||||
item: props.selectedItem,
|
||||
};
|
||||
}
|
||||
|
||||
const currentTab = props.tabs[tabIndex];
|
||||
|
||||
return {
|
||||
item: props.selectedItem,
|
||||
...currentTab?.props,
|
||||
};
|
||||
};
|
||||
|
||||
const updateSelectedTab = (value: string | number) => {
|
||||
emit('update:selectedTab', String(value));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 min-w-0 px-4 lg:px-0">
|
||||
<div v-if="showHeader && selectedItem" class="mb-6">
|
||||
<slot name="header" :item="selectedItem" />
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto -mx-4 px-4">
|
||||
<UTabs
|
||||
:model-value="selectedTab"
|
||||
variant="link"
|
||||
:items="tabItems"
|
||||
class="w-full"
|
||||
:ui="{
|
||||
list: 'gap-3 sm:gap-6 md:gap-8 whitespace-nowrap text-sm sm:text-base',
|
||||
}"
|
||||
@update:model-value="updateSelectedTab"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 sm:mt-6">
|
||||
<component
|
||||
:is="getCurrentTabComponent()"
|
||||
v-if="getCurrentTabComponent() && selectedItem"
|
||||
v-bind="getCurrentTabProps()"
|
||||
/>
|
||||
<div v-else-if="!selectedItem">
|
||||
<slot name="empty">No item selected</slot>
|
||||
</div>
|
||||
<div v-else>
|
||||
<slot name="no-content">No content available</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
133
web/components/LayoutViews/Detail/DetailTest.ce.vue
Normal file
133
web/components/LayoutViews/Detail/DetailTest.ce.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import Console from '../../Docker/Console.vue';
|
||||
import Edit from '../../Docker/Edit.vue';
|
||||
import Logs from '../../Docker/Logs.vue';
|
||||
import Overview from '../../Docker/Overview.vue';
|
||||
import Preview from '../../Docker/Preview.vue';
|
||||
import Detail from './Detail.vue';
|
||||
|
||||
interface ContainerDetails {
|
||||
network: string;
|
||||
lanIpPort: string;
|
||||
containerIp: string;
|
||||
uptime: string;
|
||||
containerPort: string;
|
||||
creationDate: string;
|
||||
containerId: string;
|
||||
maintainer: string;
|
||||
}
|
||||
|
||||
const dockerContainers = [
|
||||
{
|
||||
id: 'immich',
|
||||
label: 'immich',
|
||||
icon: 'i-lucide-play-circle',
|
||||
slot: 'immich' as const,
|
||||
status: [
|
||||
{ label: 'Update available', dotColor: 'bg-orange-500' },
|
||||
{ label: 'Started', dotColor: 'bg-green-500' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'organizrv2',
|
||||
label: 'organizrv2',
|
||||
icon: 'i-lucide-layers',
|
||||
slot: 'organizrv2' as const,
|
||||
status: [{ label: 'Started', dotColor: 'bg-green-500' }],
|
||||
},
|
||||
{
|
||||
id: 'jellyfin',
|
||||
label: 'Jellyfin',
|
||||
icon: 'i-lucide-film',
|
||||
slot: 'jellyfin' as const,
|
||||
status: [{ label: 'Stopped', dotColor: 'bg-red-500' }],
|
||||
},
|
||||
{
|
||||
id: 'databases',
|
||||
label: 'Databases',
|
||||
icon: 'i-lucide-database',
|
||||
slot: 'databases' as const,
|
||||
isGroup: true,
|
||||
children: [
|
||||
{
|
||||
id: 'mongodb',
|
||||
label: 'MongoDB',
|
||||
icon: 'i-lucide-leafy-green',
|
||||
badge: 'DB',
|
||||
slot: 'mongodb' as const,
|
||||
status: [{ label: 'Started', dotColor: 'bg-green-500' }],
|
||||
},
|
||||
{
|
||||
id: 'postgres17',
|
||||
label: 'postgres17',
|
||||
icon: 'i-lucide-pyramid',
|
||||
badge: 'DB',
|
||||
slot: 'postgres17' as const,
|
||||
status: [
|
||||
{ label: 'Update available', dotColor: 'bg-orange-500' },
|
||||
{ label: 'Paused', dotColor: 'bg-blue-500' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'redis',
|
||||
label: 'Redis',
|
||||
icon: 'i-lucide-panda',
|
||||
badge: 'DB',
|
||||
slot: 'redis' as const,
|
||||
status: [{ label: 'Started', dotColor: 'bg-green-500' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const containerDetails: Record<string, ContainerDetails> = {
|
||||
immich: {
|
||||
network: 'Bridge',
|
||||
lanIpPort: '7878',
|
||||
containerIp: '172.17.0.4',
|
||||
uptime: '13 hours',
|
||||
containerPort: '9696:TCP',
|
||||
creationDate: '2 weeks ago',
|
||||
containerId: '472b4c2442b9',
|
||||
maintainer: 'ghcr.io/imagegenius/immich',
|
||||
},
|
||||
};
|
||||
|
||||
const getTabsWithProps = (containerId: string) => [
|
||||
{
|
||||
key: 'overview',
|
||||
label: 'Overview',
|
||||
component: Overview,
|
||||
props: { details: containerDetails[containerId] },
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
label: 'Logs',
|
||||
component: Logs,
|
||||
},
|
||||
{
|
||||
key: 'console',
|
||||
label: 'Console',
|
||||
component: Console,
|
||||
},
|
||||
{
|
||||
key: 'preview',
|
||||
label: 'Preview',
|
||||
component: Preview,
|
||||
props: { port: containerDetails[containerId]?.lanIpPort || '8080' },
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Edit',
|
||||
component: Edit,
|
||||
},
|
||||
];
|
||||
|
||||
const tabs = getTabsWithProps('immich');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<Detail :items="dockerContainers" :tabs="tabs" default-item-id="immich" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,6 +5,9 @@ import { DefaultApolloClient } from '@vue/apollo-composable';
|
||||
// Import Tailwind CSS for web components shadow DOM injection
|
||||
import tailwindStyles from '~/assets/main.css?inline';
|
||||
|
||||
// Import UI configurations from .nuxt generated files
|
||||
import * as uiConfig from '~/.nuxt/ui';
|
||||
|
||||
import en_US from '~/locales/en_US.json';
|
||||
import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
|
||||
import { globalPinia } from '~/store/globalPinia';
|
||||
@@ -23,7 +26,7 @@ export default function (Vue: App) {
|
||||
if (windowLocaleData) {
|
||||
try {
|
||||
parsedMessages = JSON.parse(decodeURIComponent(windowLocaleData));
|
||||
parsedLocale = Object.keys(parsedMessages)[0];
|
||||
parsedLocale = Object.keys(parsedMessages)[0] || '';
|
||||
nonDefaultLocale = parsedLocale !== defaultLocale;
|
||||
} catch (error) {
|
||||
console.error('[WebComponentPlugins] error parsing messages', error);
|
||||
@@ -50,6 +53,10 @@ export default function (Vue: App) {
|
||||
// Provide Apollo client for all web components
|
||||
Vue.provide(DefaultApolloClient, client);
|
||||
|
||||
// Provide UI config for components
|
||||
Vue.provide('ui.config', uiConfig);
|
||||
|
||||
|
||||
// Inject Tailwind CSS into the shadow DOM
|
||||
Vue.mixin({
|
||||
mounted() {
|
||||
|
||||
36
web/composables/defineShortcuts.ts
Normal file
36
web/composables/defineShortcuts.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
type ShortcutHandler = () => void;
|
||||
type Shortcuts = Record<string, ShortcutHandler>;
|
||||
|
||||
export function defineShortcuts(shortcuts: Shortcuts) {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Build key combination string
|
||||
const keys: string[] = [];
|
||||
if (event.metaKey || event.ctrlKey) keys.push('meta');
|
||||
if (event.altKey) keys.push('alt');
|
||||
if (event.shiftKey) keys.push('shift');
|
||||
|
||||
// Add the actual key
|
||||
const key = event.key.toLowerCase();
|
||||
if (key !== 'meta' && key !== 'control' && key !== 'alt' && key !== 'shift') {
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
const combination = keys.join('_');
|
||||
|
||||
// Check if we have a handler for this combination
|
||||
if (shortcuts[combination]) {
|
||||
event.preventDefault();
|
||||
shortcuts[combination]();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
}
|
||||
46
web/composables/useUIConfig.ts
Normal file
46
web/composables/useUIConfig.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { getCurrentInstance, inject } from 'vue';
|
||||
|
||||
export function useAppConfig() {
|
||||
// Only try inject if we're in a component context
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
if (instance) {
|
||||
// Try to get from injection first (only works in setup context)
|
||||
try {
|
||||
const injected = inject('appConfig', null);
|
||||
if (injected) {
|
||||
return injected;
|
||||
}
|
||||
} catch {
|
||||
// inject failed, continue with fallbacks
|
||||
}
|
||||
|
||||
// Fallback to global
|
||||
if (instance.appContext.config.globalProperties.$appConfig) {
|
||||
return instance.appContext.config.globalProperties.$appConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort - return from window
|
||||
if (typeof window !== 'undefined') {
|
||||
const globalWindow = window as typeof globalThis & { appConfig?: unknown };
|
||||
if (globalWindow.appConfig) {
|
||||
return globalWindow.appConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// Default config
|
||||
return {
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'blue',
|
||||
neutral: 'gray'
|
||||
}
|
||||
},
|
||||
toaster: {
|
||||
position: 'bottom-right' as const,
|
||||
expand: true,
|
||||
duration: 5000
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export default withNuxt(
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/no-undef-components': [
|
||||
/* 'vue/no-undef-components': [
|
||||
'error',
|
||||
{
|
||||
ignorePatterns: [
|
||||
@@ -46,7 +46,7 @@ export default withNuxt(
|
||||
'^ConnectSettingsCe$',
|
||||
],
|
||||
},
|
||||
],
|
||||
], */
|
||||
'eol-last': ['error', 'always'],
|
||||
|
||||
// TypeScript rules for unused variables and undefined variables
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ClientOnly, NuxtLink } from '#components';
|
||||
import { Badge, Toaster } from '@unraid/ui';
|
||||
|
||||
@@ -10,18 +9,71 @@ import DummyServerSwitcher from '~/components/DummyServerSwitcher.vue';
|
||||
import ModalsCe from '~/components/Modals.ce.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const openDropdown = ref<string | null>(null);
|
||||
|
||||
const toggleDropdown = (routePath: string) => {
|
||||
openDropdown.value = openDropdown.value === routePath ? null : routePath;
|
||||
};
|
||||
|
||||
const routes = computed(() => {
|
||||
return router
|
||||
const allRoutes = router
|
||||
.getRoutes()
|
||||
.filter((route) => !route.path.includes(':') && route.path !== '/404' && route.name)
|
||||
.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
// Group routes by parent path
|
||||
const grouped = new Map<string, any>();
|
||||
const topLevel: any[] = [];
|
||||
|
||||
allRoutes.forEach(route => {
|
||||
const pathParts = route.path.split('/').filter(Boolean);
|
||||
|
||||
if (pathParts.length === 1) {
|
||||
// Top level route
|
||||
const existing = grouped.get(pathParts[0]);
|
||||
if (existing) {
|
||||
// Already have a parent with this name, skip adding duplicate
|
||||
return;
|
||||
}
|
||||
topLevel.push({
|
||||
...route,
|
||||
children: []
|
||||
});
|
||||
grouped.set(pathParts[0], topLevel[topLevel.length - 1]);
|
||||
} else if (pathParts.length >= 2) {
|
||||
// Nested route - find or create parent
|
||||
const parentPath = pathParts[0];
|
||||
let parent = grouped.get(parentPath);
|
||||
|
||||
if (!parent) {
|
||||
// Create a virtual parent route
|
||||
parent = {
|
||||
path: `/${parentPath}`,
|
||||
name: parentPath,
|
||||
children: []
|
||||
};
|
||||
topLevel.push(parent);
|
||||
grouped.set(parentPath, parent);
|
||||
}
|
||||
|
||||
parent.children.push(route);
|
||||
}
|
||||
});
|
||||
|
||||
return topLevel;
|
||||
});
|
||||
|
||||
function formatRouteName(name: string | symbol | undefined) {
|
||||
function formatRouteName(name: string | symbol | undefined, path?: string) {
|
||||
if (!name) return 'Home';
|
||||
// Convert symbols to strings if needed
|
||||
const nameStr = typeof name === 'symbol' ? name.toString() : name;
|
||||
|
||||
// For nested routes, show the last part of the path
|
||||
if (path && path.includes('/') && path.split('/').filter(Boolean).length > 1) {
|
||||
const lastPart = path.split('/').pop();
|
||||
return lastPart ? lastPart.charAt(0).toUpperCase() + lastPart.slice(1).replace(/-/g, ' ') : nameStr;
|
||||
}
|
||||
|
||||
// Convert route names like "web-components" to "Web Components"
|
||||
return nameStr
|
||||
.replace(/-/g, ' ')
|
||||
@@ -38,7 +90,45 @@ function formatRouteName(name: string | symbol | undefined) {
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 p-3 md:p-4">
|
||||
<nav class="flex flex-wrap items-center gap-2">
|
||||
<template v-for="route in routes" :key="route.path">
|
||||
<NuxtLink :to="route.path">
|
||||
<!-- Routes with children get a dropdown -->
|
||||
<div v-if="route.children && route.children.length > 0" class="relative">
|
||||
<Badge
|
||||
:variant="router.currentRoute.value.path.startsWith(route.path) ? 'orange' : 'gray'"
|
||||
size="xs"
|
||||
class="cursor-pointer flex items-center gap-1"
|
||||
@click="toggleDropdown(route.path)"
|
||||
>
|
||||
{{ formatRouteName(route.name) }}
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform"
|
||||
:class="openDropdown === route.path && 'rotate-180'"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</Badge>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div
|
||||
v-if="openDropdown === route.path"
|
||||
class="absolute top-full mt-1 left-0 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg z-50 min-w-[150px]"
|
||||
>
|
||||
<NuxtLink
|
||||
v-for="child in route.children"
|
||||
:key="child.path"
|
||||
:to="child.path"
|
||||
class="block px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-200"
|
||||
@click="openDropdown = null"
|
||||
>
|
||||
{{ formatRouteName(child.name, child.path) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regular routes without children -->
|
||||
<NuxtLink v-else :to="route.path">
|
||||
<Badge
|
||||
:variant="router.currentRoute.value.path === route.path ? 'orange' : 'gray'"
|
||||
size="xs"
|
||||
|
||||
42
web/layouts/unraid-next.vue
Normal file
42
web/layouts/unraid-next.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import UApp from '@nuxt/ui/components/App.vue';
|
||||
import UPage from '@nuxt/ui/components/Page.vue';
|
||||
|
||||
import { defaultColors } from '~/themes/default';
|
||||
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
// Initialize theme on mount
|
||||
onMounted(() => {
|
||||
// Set a default theme similar to ColorSwitcherCe
|
||||
const whiteTheme = defaultColors.white;
|
||||
if (whiteTheme) {
|
||||
const defaultTheme = {
|
||||
banner: false,
|
||||
bannerGradient: false,
|
||||
descriptionShow: true,
|
||||
textColor: whiteTheme['--header-text-primary'],
|
||||
metaColor: whiteTheme['--header-text-secondary'],
|
||||
bgColor: whiteTheme['--header-background-color'],
|
||||
name: 'white',
|
||||
};
|
||||
themeStore.setTheme(defaultTheme);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<UPage>
|
||||
<slot />
|
||||
</UPage>
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Import theme styles */
|
||||
@import '~/assets/main.css';
|
||||
</style>
|
||||
@@ -149,13 +149,7 @@ export default defineNuxtConfig({
|
||||
|
||||
modules: ['@vueuse/nuxt', '@pinia/nuxt', '@nuxt/eslint', '@nuxt/ui'],
|
||||
|
||||
ui: {
|
||||
theme: {
|
||||
colors: ['primary'],
|
||||
},
|
||||
},
|
||||
|
||||
// Disable auto-imports
|
||||
// Disable auto-imports for manual control
|
||||
imports: {
|
||||
autoImport: false,
|
||||
},
|
||||
@@ -171,7 +165,16 @@ export default defineNuxtConfig({
|
||||
components: false,
|
||||
|
||||
vite: {
|
||||
plugins: getSharedPlugins(),
|
||||
plugins: [
|
||||
...getSharedPlugins(),
|
||||
// Add Nuxt UI vite plugin for Vue mode
|
||||
(async () => {
|
||||
const { default: NuxtUIVite } = await import('@nuxt/ui/vite');
|
||||
return NuxtUIVite({
|
||||
colorMode: true
|
||||
});
|
||||
})()
|
||||
],
|
||||
define: sharedDefine,
|
||||
build: {
|
||||
minify: 'terser',
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"@jsonforms/vue": "3.6.0",
|
||||
"@jsonforms/vue-vanilla": "3.6.0",
|
||||
"@jsonforms/vue-vuetify": "3.6.0",
|
||||
"@nuxt/ui": "3.3.2",
|
||||
"@nuxt/ui": "4.0.0-alpha.0",
|
||||
"@nuxtjs/color-mode": "3.5.2",
|
||||
"@pinia/nuxt": "0.11.2",
|
||||
"@unraid/shared-callbacks": "1.1.1",
|
||||
|
||||
176
web/pages/docker.vue
Normal file
176
web/pages/docker.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { definePageMeta } from '#imports';
|
||||
import UButton from '@nuxt/ui/components/Button.vue';
|
||||
import Console from '../components/Docker/Console.vue';
|
||||
import Edit from '../components/Docker/Edit.vue';
|
||||
import Logs from '../components/Docker/Logs.vue';
|
||||
import Overview from '../components/Docker/Overview.vue';
|
||||
import Preview from '../components/Docker/Preview.vue';
|
||||
import Card from '../components/LayoutViews/Card/Card.vue';
|
||||
import Detail from '../components/LayoutViews/Detail/Detail.vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'unraid-next',
|
||||
});
|
||||
|
||||
interface ContainerDetails {
|
||||
network: string;
|
||||
lanIpPort: string;
|
||||
containerIp: string;
|
||||
uptime: string;
|
||||
containerPort: string;
|
||||
creationDate: string;
|
||||
containerId: string;
|
||||
maintainer: string;
|
||||
}
|
||||
|
||||
const dockerContainers = [
|
||||
{
|
||||
id: 'immich',
|
||||
label: 'immich',
|
||||
icon: 'i-lucide-play-circle',
|
||||
slot: 'immich' as const,
|
||||
status: [
|
||||
{ label: 'Update available', dotColor: 'bg-orange-500' },
|
||||
{ label: 'Started', dotColor: 'bg-green-500' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'organizrv2',
|
||||
label: 'organizrv2',
|
||||
icon: 'i-lucide-layers',
|
||||
slot: 'organizrv2' as const,
|
||||
status: [{ label: 'Started', dotColor: 'bg-green-500' }],
|
||||
},
|
||||
{
|
||||
id: 'jellyfin',
|
||||
label: 'Jellyfin',
|
||||
icon: 'i-lucide-film',
|
||||
slot: 'jellyfin' as const,
|
||||
status: [{ label: 'Stopped', dotColor: 'bg-red-500' }],
|
||||
},
|
||||
{
|
||||
id: 'databases',
|
||||
label: 'Databases',
|
||||
icon: 'i-lucide-database',
|
||||
slot: 'databases' as const,
|
||||
isGroup: true,
|
||||
children: [
|
||||
{
|
||||
id: 'mongodb',
|
||||
label: 'MongoDB',
|
||||
icon: 'i-lucide-leafy-green',
|
||||
badge: 'DB',
|
||||
slot: 'mongodb' as const,
|
||||
status: [{ label: 'Started', dotColor: 'bg-green-500' }],
|
||||
},
|
||||
{
|
||||
id: 'postgres17',
|
||||
label: 'postgres17',
|
||||
icon: 'i-lucide-pyramid',
|
||||
badge: 'DB',
|
||||
slot: 'postgres17' as const,
|
||||
status: [
|
||||
{ label: 'Update available', dotColor: 'bg-orange-500' },
|
||||
{ label: 'Paused', dotColor: 'bg-blue-500' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'redis',
|
||||
label: 'Redis',
|
||||
icon: 'i-lucide-panda',
|
||||
badge: 'DB',
|
||||
slot: 'redis' as const,
|
||||
status: [{ label: 'Started', dotColor: 'bg-green-500' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const containerDetails: Record<string, ContainerDetails> = {
|
||||
immich: {
|
||||
network: 'Bridge',
|
||||
lanIpPort: '7878',
|
||||
containerIp: '172.17.0.4',
|
||||
uptime: '13 hours',
|
||||
containerPort: '9696:TCP',
|
||||
creationDate: '2 weeks ago',
|
||||
containerId: '472b4c2442b9',
|
||||
maintainer: 'ghcr.io/imagegenius/immich',
|
||||
},
|
||||
};
|
||||
|
||||
const getTabsWithProps = (containerId: string) => [
|
||||
{
|
||||
key: 'overview',
|
||||
label: 'Overview',
|
||||
component: Overview,
|
||||
props: { details: containerDetails[containerId] },
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
label: 'Logs',
|
||||
component: Logs,
|
||||
},
|
||||
{
|
||||
key: 'console',
|
||||
label: 'Console',
|
||||
component: Console,
|
||||
},
|
||||
{
|
||||
key: 'preview',
|
||||
label: 'Preview',
|
||||
component: Preview,
|
||||
props: { port: containerDetails[containerId]?.lanIpPort || '8080' },
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Edit',
|
||||
component: Edit,
|
||||
},
|
||||
];
|
||||
|
||||
const tabs = getTabsWithProps('immich');
|
||||
|
||||
// View mode toggle
|
||||
const viewMode = ref<'detail' | 'card'>('detail');
|
||||
|
||||
const toggleView = () => {
|
||||
viewMode.value = viewMode.value === 'detail' ? 'card' : 'detail';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- View Toggle Header -->
|
||||
<div class="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Docker</h1>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">View:</span>
|
||||
<UButton
|
||||
:icon="viewMode === 'detail' ? 'i-lucide-list' : 'i-lucide-grid-3x3'"
|
||||
:color="viewMode === 'detail' ? 'primary' : 'neutral'"
|
||||
:variant="viewMode === 'detail' ? 'solid' : 'outline'"
|
||||
size="sm"
|
||||
@click="toggleView"
|
||||
>
|
||||
{{ viewMode === 'detail' ? 'Detail' : 'Card' }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View Content -->
|
||||
<div class="flex-1 min-h-0">
|
||||
<Detail
|
||||
v-if="viewMode === 'detail'"
|
||||
:items="dockerContainers"
|
||||
:tabs="tabs"
|
||||
default-item-id="immich"
|
||||
/>
|
||||
<Card v-else :items="dockerContainers" navigation-label="Docker Overview" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
36
web/pages/docker/card.vue
Normal file
36
web/pages/docker/card.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { definePageMeta } from '#imports';
|
||||
import Card from '../../components/LayoutViews/Card/Card.vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'unraid-next',
|
||||
});
|
||||
|
||||
const dockerContainers = [
|
||||
{
|
||||
id: 'immich',
|
||||
label: 'immich',
|
||||
icon: 'i-lucide-play-circle',
|
||||
slot: 'immich' as const,
|
||||
status: [
|
||||
{ label: 'Update available', dotColor: 'bg-orange-500' },
|
||||
{ label: 'Started', dotColor: 'bg-green-500' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'organizrv2',
|
||||
label: 'organizrv2',
|
||||
icon: 'i-lucide-layers',
|
||||
slot: 'organizrv2' as const,
|
||||
status: [{ label: 'Started', dotColor: 'bg-green-500' }],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Test Card Component</h1>
|
||||
<Card :items="dockerContainers" navigation-label="Docker Overview" />
|
||||
</div>
|
||||
</template>
|
||||
16
web/pages/docker/console.vue
Normal file
16
web/pages/docker/console.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { definePageMeta } from '#imports';
|
||||
import Console from '../../components/Docker/Console.vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'unraid-next',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Console Component Test</h1>
|
||||
<Console />
|
||||
</div>
|
||||
</template>
|
||||
82
web/pages/docker/detail.vue
Normal file
82
web/pages/docker/detail.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { definePageMeta } from '#imports';
|
||||
import Detail from '../../components/LayoutViews/Detail/Detail.vue';
|
||||
|
||||
import Console from '../../components/Docker/Console.vue';
|
||||
import Edit from '../../components/Docker/Edit.vue';
|
||||
import Logs from '../../components/Docker/Logs.vue';
|
||||
import Overview from '../../components/Docker/Overview.vue';
|
||||
import Preview from '../../components/Docker/Preview.vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'unraid-next',
|
||||
});
|
||||
|
||||
const dockerContainers = [
|
||||
{
|
||||
id: 'immich',
|
||||
label: 'immich',
|
||||
icon: 'i-lucide-play-circle',
|
||||
slot: 'immich' as const,
|
||||
status: [
|
||||
{ label: 'Update available', dotColor: 'bg-orange-500' },
|
||||
{ label: 'Started', dotColor: 'bg-green-500' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const containerDetails = {
|
||||
immich: {
|
||||
network: 'Bridge',
|
||||
lanIpPort: '7878',
|
||||
containerIp: '172.17.0.4',
|
||||
uptime: '13 hours',
|
||||
containerPort: '9696:TCP',
|
||||
creationDate: '2 weeks ago',
|
||||
containerId: '472b4c2442b9',
|
||||
maintainer: 'ghcr.io/imagegenius/immich',
|
||||
},
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: 'overview',
|
||||
label: 'Overview',
|
||||
component: Overview,
|
||||
props: { details: containerDetails['immich'] },
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
label: 'Logs',
|
||||
component: Logs,
|
||||
},
|
||||
{
|
||||
key: 'console',
|
||||
label: 'Console',
|
||||
component: Console,
|
||||
},
|
||||
{
|
||||
key: 'preview',
|
||||
label: 'Preview',
|
||||
component: Preview,
|
||||
props: { port: containerDetails['immich']?.lanIpPort || '8080' },
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Edit',
|
||||
component: Edit,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Test Detail Component</h1>
|
||||
<Detail
|
||||
:items="dockerContainers"
|
||||
:tabs="tabs"
|
||||
default-item-id="immich"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
16
web/pages/docker/edit.vue
Normal file
16
web/pages/docker/edit.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { definePageMeta } from '#imports';
|
||||
import Edit from '../../components/Docker/Edit.vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'unraid-next',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Test Edit Component</h1>
|
||||
<Edit />
|
||||
</div>
|
||||
</template>
|
||||
16
web/pages/docker/logs.vue
Normal file
16
web/pages/docker/logs.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { definePageMeta } from '#imports';
|
||||
import Logs from '../../components/Docker/Logs.vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'unraid-next',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Test Logs Component</h1>
|
||||
<Logs />
|
||||
</div>
|
||||
</template>
|
||||
27
web/pages/docker/overview.vue
Normal file
27
web/pages/docker/overview.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { definePageMeta } from '#imports';
|
||||
import Overview from '../../components/Docker/Overview.vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'unraid-next',
|
||||
});
|
||||
|
||||
const testDetails = {
|
||||
network: 'Bridge',
|
||||
lanIpPort: '7878',
|
||||
containerIp: '172.17.0.4',
|
||||
uptime: '13 hours',
|
||||
containerPort: '9696:TCP',
|
||||
creationDate: '2 weeks ago',
|
||||
containerId: '472b4c2442b9',
|
||||
maintainer: 'ghcr.io/imagegenius/immich',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Test Overview Component</h1>
|
||||
<Overview :details="testDetails" />
|
||||
</div>
|
||||
</template>
|
||||
16
web/pages/docker/preview.vue
Normal file
16
web/pages/docker/preview.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { definePageMeta } from '#imports';
|
||||
import Preview from '../../components/Docker/Preview.vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'unraid-next',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Test Preview Component</h1>
|
||||
<Preview port="8080" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,7 +4,8 @@ import { storeToRefs } from 'pinia';
|
||||
|
||||
import { ExclamationTriangleIcon } from '@heroicons/vue/24/solid';
|
||||
import { BrandButton, Toaster } from '@unraid/ui';
|
||||
import { UButton } from '#components';
|
||||
import { useHead } from '#imports';
|
||||
import UButton from '@nuxt/ui/components/Button.vue';
|
||||
import { useDummyServerStore } from '~/_data/serverState';
|
||||
import AES from 'crypto-js/aes';
|
||||
|
||||
|
||||
@@ -1,21 +1,79 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useDummyServerStore } from '~/_data/serverState';
|
||||
|
||||
import { Toaster } from '@unraid/ui';
|
||||
import { useDummyServerStore } from '~/_data/serverState';
|
||||
|
||||
import BrandLogo from '~/components/Brand/Logo.vue';
|
||||
import HeaderOsVersionCe from '~/components/HeaderOsVersion.ce.vue';
|
||||
import ConnectSettingsCe from '~/components/ConnectSettings/ConnectSettings.ce.vue';
|
||||
import HeaderOsVersionCe from '~/components/HeaderOsVersion.ce.vue';
|
||||
|
||||
const serverStore = useDummyServerStore();
|
||||
const { serverState } = storeToRefs(serverStore);
|
||||
|
||||
// Define window type extension
|
||||
declare global {
|
||||
interface Window {
|
||||
__unraidUiComponentsRegistered?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Log immediately when component is created
|
||||
console.log('[WebComponents Test] Component setup executing');
|
||||
|
||||
// Check if components are loaded after mount
|
||||
onMounted(async () => {
|
||||
console.log('[WebComponents Test] onMounted hook called');
|
||||
|
||||
// Check if components have already been registered
|
||||
if (window.__unraidUiComponentsRegistered) {
|
||||
console.log('[WebComponents Test] Components already registered');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// For development, we need to manually import and run the setup
|
||||
if (import.meta.dev) {
|
||||
console.log('[WebComponents Test] Loading web components in dev mode...');
|
||||
|
||||
// Dynamically import the entry file and execute it
|
||||
await import('#customElementsEntries/unraid-components.client.js');
|
||||
|
||||
console.log('[WebComponents Test] Web components imported successfully');
|
||||
window.__unraidUiComponentsRegistered = true;
|
||||
} else {
|
||||
// In production, create script tag for built bundle
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = '/_nuxt/unraid-components.client.js';
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error('Failed to load production script'));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
window.__unraidUiComponentsRegistered = true;
|
||||
}
|
||||
|
||||
// Give components time to register
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Check if components are registered
|
||||
console.log('[WebComponents Test] Checking for registered components...');
|
||||
console.log('unraid-auth defined?', customElements.get('unraid-auth') !== undefined);
|
||||
console.log('unraid-user-profile defined?', customElements.get('unraid-user-profile') !== undefined);
|
||||
console.log('unraid-detail-test defined?', customElements.get('unraid-detail-test') !== undefined);
|
||||
} catch (error) {
|
||||
console.error('[WebComponents Test] Failed to load web components:', error);
|
||||
window.__unraidUiComponentsRegistered = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<client-only>
|
||||
<div
|
||||
class="flex flex-col gap-6 p-6 mx-auto text-black bg-white dark:text-white dark:bg-black"
|
||||
>
|
||||
<div class="flex flex-col gap-6 p-6 mx-auto text-black bg-white dark:text-white dark:bg-black">
|
||||
<h2 class="text-xl font-semibold font-mono">Web Components</h2>
|
||||
<h3 class="text-lg font-semibold font-mono">UserProfileCe</h3>
|
||||
<header class="bg-header-background-color py-4 flex flex-row justify-between items-center">
|
||||
@@ -61,6 +119,8 @@ const { serverState } = storeToRefs(serverStore);
|
||||
<hr class="border-muted" >
|
||||
<h3 class="text-lg font-semibold font-mono">ApiKeyManagerCe</h3>
|
||||
<unraid-api-key-manager />
|
||||
|
||||
<unraid-detail-test />
|
||||
</div>
|
||||
<Toaster rich-colors close-button />
|
||||
</client-only>
|
||||
|
||||
29
web/plugins/nuxt-ui.client.ts
Normal file
29
web/plugins/nuxt-ui.client.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineNuxtPlugin, useAppConfig } from '#app';
|
||||
import ui from '@nuxt/ui/vue-plugin';
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'nuxt-ui-vue-mode',
|
||||
enforce: 'pre', // Run before other plugins
|
||||
setup(nuxtApp) {
|
||||
// Get the app config
|
||||
const appConfig = useAppConfig();
|
||||
|
||||
// Provide the app config globally BEFORE initializing the plugin
|
||||
nuxtApp.vueApp.config.globalProperties.$appConfig = appConfig;
|
||||
|
||||
// Also make it available on window for the composable fallback
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as typeof globalThis & { appConfig?: typeof appConfig }).appConfig = appConfig;
|
||||
}
|
||||
|
||||
// Use Nuxt UI in Vue mode - the vue-plugin provides stubs for Nuxt-specific imports
|
||||
nuxtApp.vueApp.use(ui, {
|
||||
prefix: 'U',
|
||||
// Pass the config directly to the plugin
|
||||
appConfig
|
||||
});
|
||||
|
||||
// Provide after plugin initialization for components
|
||||
nuxtApp.vueApp.provide('appConfig', appConfig);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user