From 7af148058e8042564f9662fdae971bfad96d401c Mon Sep 17 00:00:00 2001
From: webfussel <fiona@webfussel.de>
Date: Sat, 10 May 2025 11:07:06 +0200
Subject: [PATCH] add: new design (wip)

New design as WIP, swipe controls
---
 app/assets/styles/button.css      |  25 ++++--
 app/assets/styles/buttonGroup.css |  12 ++-
 app/assets/styles/footer.css      |   8 +-
 app/assets/styles/formInput.css   |  12 +--
 app/assets/styles/general.css     |  48 +++++++---
 app/assets/styles/header.css      |   8 +-
 app/assets/styles/priceCard.css   | 144 +++++++++++++++++++-----------
 app/assets/styles/toolbar.css     |   2 +-
 app/components/Pp/ButtonGroup.vue |   2 +-
 app/components/Pp/PriceCard.vue   | 111 +++++++++++++++--------
 app/pages/index.vue               |   4 +-
 app/utils/number.ts               |   4 +-
 nuxt.config.ts                    |   6 +-
 package-lock.json                 | 108 ++++++++++++++++++++++
 package.json                      |   2 +
 15 files changed, 354 insertions(+), 142 deletions(-)

diff --git a/app/assets/styles/button.css b/app/assets/styles/button.css
index 46e25a1..3a78022 100755
--- a/app/assets/styles/button.css
+++ b/app/assets/styles/button.css
@@ -1,7 +1,7 @@
 .Button {
     --padding: .2rem;
-    --background: var(--color-main);
-    --color: var(--color-white);
+    --background: var(--color-gradient-main-dark);
+    --color: var(--color-lightest);
     --background-hover: var(--color-main-dark);
 
     position: relative;
@@ -16,20 +16,27 @@
     background: var(--background);
     color: var(--color);
 
+    &.transparent {
+        --background: transparent;
+        box-shadow: none;
+        padding: .5em 1.5em;
+        border-radius: var(--radius-default);
+    }
+
     &.raised {
         box-shadow: var(--box-shadow-z2);
         padding: .5em 1.5em;
         border-radius: var(--radius-default);
 
         &.danger {
-            --background: var(--color-red);
-            --color: var(--color-white);
+            --background: var(--color-gradient-error);
+            --color: var(--color-lightest);
         }
     }
 
     &.text {
         --background: transparent;
-        --color: var(--color-black);
+        --color: var(--color-darkest);
         padding: .5em 1.5em;
         border-radius: var(--radius-default);
 
@@ -38,11 +45,11 @@
         }
 
         &.white {
-            --color: var(--color-white);
+            --color: var(--color-lightest);
         }
 
         &.danger {
-            --color: var(--color-red);
+            --color: var(--color-error);
 
             &:hover {
                 --background: rgba(255, 0, 0, 0.1);
@@ -71,9 +78,9 @@
     }
 
     &.cta.white {
-        --background: var(--color-white);
+        --background: var(--color-lightest);
         --color: var(--color-main);
-        --background-hover: var(--color-grey);
+        --background-hover: var(--color-light);
     }
 
     &.icon-button {
diff --git a/app/assets/styles/buttonGroup.css b/app/assets/styles/buttonGroup.css
index 9c47667..9901a40 100644
--- a/app/assets/styles/buttonGroup.css
+++ b/app/assets/styles/buttonGroup.css
@@ -1,9 +1,13 @@
 .ButtonGroup {
     display: flex;
+    background: var(--color-main);
+    border-radius: var(--radius-default);
+    overflow: hidden;
+    box-shadow: var(--box-shadow-z2);
 
     & button {
-        --color: var(--color-white-transparent);
-        --background: var(--color-main);
+        --color: var(--color-light);
+        --background: var(--color-main-darkest);
         all: unset;
         display: flex;
         align-items: center;
@@ -17,8 +21,8 @@
         transition: var(--transition-default);
 
         &.active {
-            --color: var(--color-white);
-            --background: var(--color-main-dark);
+            --color: var(--color-darkest);
+            --background: var(--color-gradient-main);
         }
 
         &:first-child {
diff --git a/app/assets/styles/footer.css b/app/assets/styles/footer.css
index 23c47e5..ffc36d1 100644
--- a/app/assets/styles/footer.css
+++ b/app/assets/styles/footer.css
@@ -1,9 +1,9 @@
 .Footer {
-    background: var(--color-main-dark);
+    background: var(--color-main-darkest);
     padding: 1rem;
 
     & h4 {
-        color: var(--color-white);
+        color: var(--color-lightest);
         text-align: center;
         margin-bottom: 1rem;
     }
@@ -12,7 +12,7 @@
         display: flex;
         gap: 1rem;
         justify-content: space-between;
-        color: var(--color-white-transparent);
+        color: var(--color-light);
     }
 
     & .socials {
@@ -33,7 +33,7 @@
         gap: 1rem;
 
         & a {
-            color: var(--color-white);
+            color: var(--color-lightest);
             text-decoration: none;
         }
     }
diff --git a/app/assets/styles/formInput.css b/app/assets/styles/formInput.css
index c207605..3d9c8a0 100755
--- a/app/assets/styles/formInput.css
+++ b/app/assets/styles/formInput.css
@@ -1,12 +1,12 @@
 .Input {
     &.error {
         & .input-wrapper {
-            border-color: var(--color-red);
+            border-color: var(--color-error);
             outline-width: 2px;
         }
 
         & span {
-            color: var(--color-red);
+            color: var(--color-error);
         }
     }
 
@@ -17,11 +17,11 @@
     & .input-wrapper {
         position: relative;
         flex: 25% 1 0;
-        border: 2px solid var(--color-blue);
+        border: 2px solid var(--color-main-dark);
         border-radius: var(--radius-default);
         overflow: hidden;
         transition: var(--transition-default);
-        outline: 0 solid var(--color-white);
+        outline: 0 solid var(--color-lightest);
 
         & label {
             position: absolute;
@@ -35,7 +35,7 @@
             all: unset;
             width: calc(100% - 1rem);
             padding: 1.3rem .5rem .5rem .5rem;
-            background: var(--color-white);
+            background: var(--color-lightest);
 
             &[type="number"] {
                 text-align: right;
@@ -45,7 +45,7 @@
         & input:focus,
         & input:not(:placeholder-shown) {
             & + label {
-                color: var(--color-main);
+                color: var(--color-main-dark);
             }
         }
     }
diff --git a/app/assets/styles/general.css b/app/assets/styles/general.css
index 670efcf..50e8dc6 100755
--- a/app/assets/styles/general.css
+++ b/app/assets/styles/general.css
@@ -1,29 +1,50 @@
 :root {
     --padding-default: 1rem;
     --padding-small: .5rem;
-    --radius-default: 5px;
+    --radius-default: 3px;
     --transition-default: 150ms;
 
-    --color-white: white;
-    --color-white-transparent: rgba(255, 255, 255, 0.67);
-    --color-red: #cc0001;
-    --color-blue-light: #61a7fd;
-    --color-blue: #2e86de;
-    --color-blue-dark: #1b4b7f;
-    --color-grey: #c7c7c7;
-    --color-black: #333;
+    --color-success: #328104;
+    --color-error: #A20606;
+    --color-blue-light: #0DDCE7;
+    --color-blue: #05B0FF;
+    --color-blue-dark: #0266F2;
+    --color-blue-darkest: #013174;
+
+    --color-darkest: #292929;
+    --color-dark: #404040;
+    --color-middle: #707070;
+    --color-light: #E0E0E6;
+    --color-lightest: #FAFAFF;
+
+    --color-green-light: #05FFC5;
+    --color-green: #02F276;
+    --color-green-dark: #09DC33;
+    --color-green-darkest: #07B029;
+    --color-green-darkest-most: #157C2A;
 
-    --color-orange: #DE9C2F;
 
     --color-main: var(--color-blue);
     --color-main-light: var(--color-blue-light);
     --color-main-dark: var(--color-blue-dark);
+    --color-main-darkest: var(--color-blue-darkest);
 
-    --color-accent: var(--color-orange);
+    --color-accent: var(--color-green);
+    --color-accent-light: var(--color-green-light);
+    --color-accent-dark: var(--color-green-dark);
+    --color-accent-darkest: var(--color-green-darkest);
+
+    --color-gradient-main: linear-gradient(to bottom right, var(--color-main), var(--color-main-light));
+    --color-gradient-main-dark: linear-gradient(to bottom right, var(--color-main-darkest), var(--color-main-dark));
+    --color-gradient-accent: linear-gradient(to bottom right, var(--color-accent), var(--color-accent-light));
+    --color-gradient-accent-dark: linear-gradient(to bottom right, var(--color-accent-darkest), var(--color-accent-dark));
+    --color-gradient-error: linear-gradient(to bottom right, #B00707, #DC0909);
+    --color-gradient-error-reverse: linear-gradient(to top left, #B00707, #DC0909);
 
     --box-shadow-upper: 0 -3px 6px rgba(0,0,0,0.16), 0 -3px 6px rgba(0,0,0,0.23);
     --box-shadow-z2: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
     --box-shadow-inset: inset 0 3px 6px rgba(0,0,0,0.16), inset 0 3px 6px rgba(0,0,0,0.23);
+
 }
 
 * {
@@ -79,7 +100,6 @@ body {
 .card {
     overflow: hidden;
     border-radius: var(--radius-default);
-    border: 1px solid var(--color-main);
     box-shadow: var(--box-shadow-z2);
 }
 
@@ -108,7 +128,7 @@ body {
 }
 
 .text-white {
-    color: var(--color-white);
+    color: var(--color-lightest);
 }
 
 .bg-main {
@@ -136,7 +156,7 @@ body {
 }
 
 .bg-white {
-    background: var(--color-white);
+    background: var(--color-lightest);
 }
 
 .padding {
diff --git a/app/assets/styles/header.css b/app/assets/styles/header.css
index f89db28..4a13397 100755
--- a/app/assets/styles/header.css
+++ b/app/assets/styles/header.css
@@ -3,16 +3,18 @@
     align-items: center;
     justify-content: space-between;
     padding: var(--padding-default);
-    background-color: var(--color-white);
+    background-color: var(--color-lightest);
     box-shadow: var(--box-shadow-z2);
+    color: var(--color-darkest);
     position: sticky;
     top: 0;
     z-index: 1;
 
     & strong {
         font-size: 2em;
+
         & span {
-            color: var(--color-main);
+            color: var(--color-main-dark);
         }
     }
 
@@ -38,7 +40,7 @@
         top: 0;
         height: 100dvh;
         transition: 150ms ease-in-out;
-        background: var(--color-white);
+        background: var(--color-lightest);
         font-size: 2em;
         align-items: end;
         z-index: 100;
diff --git a/app/assets/styles/priceCard.css b/app/assets/styles/priceCard.css
index 119a4c0..1d51c4e 100755
--- a/app/assets/styles/priceCard.css
+++ b/app/assets/styles/priceCard.css
@@ -1,72 +1,108 @@
 .PriceCard {
+    position: relative;
+    overflow: hidden;
     width: 100%;
     transition: 150ms;
     opacity: 1;
-    color: var(--color-white);
-    gap: 1rem;
-    padding: 1rem;
+    color: var(--color-main-dark);
+    background: black;
 
-    &.deleting {
-        max-height: 0;
-        opacity: 0;
-    }
-
-    & > header {
+    .bottom {
+        position: absolute;
+        z-index: 1;
         display: flex;
-        justify-content: space-between;
-        align-items: center;
-
-        & .icon {
-            font-size: 1rem;
-            cursor: pointer;
-        }
-    }
-
-    & .name-price {
-        display: flex;
-        gap: .5rem;
-
-        & > span:nth-child(1) {
-            font-weight: bold;
-            max-width: 150px;
-            white-space: nowrap;
-            overflow: hidden;
-            text-overflow: ellipsis;
-        }
-
-        & > span:nth-child(2)::before {
-            content: '•';
-            margin-right: .5rem;
-            opacity: .5;
-        }
-    }
-
-    & .wrapper {
-        display: flex;
-        flex-direction: row;
+        align-items: stretch;
         width: 100%;
-        gap: 1rem;
-        justify-content: space-between;
+        height: 100%;
 
-        & > .info {
-            flex-grow: 0;
+        & > * {
+            flex-grow: 1;
+            color: var(--color-lightest);
+            font-size: 2rem;
+            display: flex;
             align-items: center;
-            gap: .25rem;
 
-            & > .icon {
-                font-size: 2rem;
-                padding: .2rem;
+            & .icon {
+                filter: drop-shadow(1px 3px 1px rgb(0 0 0 / 0.4));
             }
+        }
 
-            & > .price {
-                display: flex;
-                align-items: center;
-                gap: .5rem;
+        & .bg-edit {
+            background: var(--color-gradient-main-dark);
+            padding: 2rem;
+            text-align: left;
+        }
+
+        & .bg-delete {
+            background: var(--color-gradient-error-reverse);
+            padding: 2rem;
+            text-align: right;
+            justify-content: flex-end;
+        }
+    }
+
+    .top {
+        position: relative;
+        background: var(--color-lightest);
+        z-index: 2;
+        gap: 1rem;
+        padding: 1rem;
+        border-radius: var(--radius-default);
+
+        & > header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            color: var(--color-darkest);
+
+            & .icon {
+                font-size: 1rem;
+                cursor: pointer;
             }
+        }
 
-            & > .pro {
-                font-size: .6rem;
+        & .name-price {
+            display: flex;
+            gap: .5rem;
+
+            & > span:nth-child(1) {
                 font-weight: bold;
+                max-width: 150px;
+                white-space: nowrap;
+                overflow: hidden;
+                text-overflow: ellipsis;
+            }
+
+            & > span:nth-child(2)::before {
+                content: '•';
+                margin-right: .5rem;
+                opacity: .5;
+            }
+        }
+
+        & .wrapper {
+            display: flex;
+            flex-direction: row;
+            width: 100%;
+            gap: 1rem;
+            justify-content: space-between;
+
+            & > .info {
+                flex-grow: 0;
+                align-items: center;
+                gap: .25rem;
+
+                & > .price {
+                    display: flex;
+                    align-items: center;
+                    gap: .5rem;
+                }
+
+                & > .pro {
+                    font-size: .6rem;
+                    font-weight: bold;
+                    opacity: .7;
+                }
             }
         }
     }
diff --git a/app/assets/styles/toolbar.css b/app/assets/styles/toolbar.css
index 44f305f..b146001 100755
--- a/app/assets/styles/toolbar.css
+++ b/app/assets/styles/toolbar.css
@@ -1,7 +1,7 @@
 .Toolbar {
     display: flex;
     justify-content: space-evenly;
-    background: var(--color-main);
+    background: var(--color-gradient-main-dark);
     position: sticky;
     bottom: 0;
     box-shadow: var(--box-shadow-upper);
diff --git a/app/components/Pp/ButtonGroup.vue b/app/components/Pp/ButtonGroup.vue
index 322b6f7..71b90d4 100644
--- a/app/components/Pp/ButtonGroup.vue
+++ b/app/components/Pp/ButtonGroup.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="ButtonGroup">
+  <div class="ButtonGroup z-2">
     <button
       v-for="(button, index) in buttons"
       @click="click(index)"
diff --git a/app/components/Pp/PriceCard.vue b/app/components/Pp/PriceCard.vue
index 99886de..884b221 100755
--- a/app/components/Pp/PriceCard.vue
+++ b/app/components/Pp/PriceCard.vue
@@ -1,47 +1,57 @@
 <template>
-  <article
-    ref="root"
-    class="PriceCard card bg-blue flex-col"
-    :class="{ deleting }"
-  >
-    <header>
-      <div class="name-price">
-        <span>{{ card.name || 'Kein Name' }}</span>
-        <span>{{ intl.format(+replaceComma(card.price))}}</span>
+  <article class="PriceCard card">
+    <div class="bottom">
+      <div class="bg-edit">
+        <Icon class="icon" name="uil:pen" mode="svg" />
       </div>
-      <div class="flex-row gap-default">
-        <PpButton class="icon-button" @click="update()">
-          <Icon class="icon" name="uil:pen" mode="svg" />
-        </PpButton>
-        <PpButton class="icon-button" @click="deleteCard()">
-          <Icon class="icon" name="uil:times" mode="svg" />
-        </PpButton>
+      <div class="bg-delete">
+        <Icon class="icon" name="uil:trash-alt" mode="svg" />
       </div>
-    </header>
+    </div>
+    <div
+      ref="top"
+      class="top flex-col"
+      :style="{ left, transition }"
+    >
+      <header>
+        <div class="name-price">
+          <span>{{ card.name || 'Kein Name' }}</span>
+          <span>{{ intl.format(+replaceComma(card.price))}}</span>
+        </div>
+        <div v-if="$device.isDesktop" class="flex-row gap-default">
+          <PpButton class="icon-button" @click="update()">
+            <Icon class="icon" name="uil:pen" mode="svg" />
+          </PpButton>
+          <PpButton class="icon-button" @click="deleteCard()">
+            <Icon class="icon" name="uil:trash-alt" mode="svg" />
+          </PpButton>
+        </div>
+      </header>
 
-    <main class="wrapper">
-      <div class="info flex-col">
-        <div class="price">
-          <Icon class="icon" name="uil:toilet-paper" mode="svg" />
-          <span class="value">{{ intl.format(card.ppr) }}</span>
+      <main class="wrapper">
+        <div class="info flex-col">
+          <div class="price">
+            <Icon class="icon" name="uil:toilet-paper" mode="svg" />
+            <span class="value">{{ intl.format(card.ppr) }}</span>
+          </div>
+          <span class="pro">Pro 1 {{ card.roles ? `(${card.roles})` : '' }}</span>
         </div>
-        <span class="pro">Pro 1 {{ card.roles ? `(${card.roles})` : '' }}</span>
-      </div>
-      <div class="info flex-col">
-        <div class="price">
-          <Icon class="icon" name="uil:file-landscape" mode="svg" />
-          <span class="value">{{ intl.format(card.pps) }}</span>
+        <div class="info flex-col">
+          <div class="price">
+            <Icon class="icon" name="uil:file-landscape" mode="svg" />
+            <span class="value">{{ intl.format(card.pps) }}</span>
+          </div>
+          <span class="pro">Pro 100 {{ card.sheets ? `(${card.sheets})` : '' }}</span>
         </div>
-        <span class="pro">Pro 100 {{ card.sheets ? `(${card.sheets})` : '' }}</span>
-      </div>
-      <div class="info flex-col">
-        <div class="price">
-          <Icon class="icon" name="uil:layer-group" mode="svg" />
-          <span class="value">{{ intl.format(card.ppl) }}</span>
+        <div class="info flex-col">
+          <div class="price">
+            <Icon class="icon" name="uil:layer-group" mode="svg" />
+            <span class="value">{{ intl.format(card.ppl) }}</span>
+          </div>
+          <span class="pro">Pro 100 {{ card.layers ? `(${card.layers})` : '' }}</span>
         </div>
-        <span class="pro">Pro 100 {{ card.layers ? `(${card.layers})` : '' }}</span>
-      </div>
-    </main>
+      </main>
+    </div>
   </article>
 </template>
 
@@ -56,14 +66,37 @@ type Props = {
 const { card } = defineProps<Props>()
 const emit = defineEmits(['remove', 'update'])
 
-const root = ref<HTMLElement>()
-const deleting = ref<boolean>(false)
+const top = useTemplateRef('top')
+const left = shallowRef<string>('0')
+const transition = shallowRef<string>('none')
 
 const intl = Intl.NumberFormat('de-DE', {
   style: 'currency',
   currency: 'EUR',
 })
 
+const { lengthX } = useSwipe(top, {
+  passive: false,
+  threshold: 5,
+  onSwipe() {
+    if (lengthX.value != 0) {
+      left.value = `${-clamp(lengthX.value, -100, 100)}px`
+    }
+  },
+  onSwipeEnd() {
+    if (lengthX.value < -50) update()
+    if (lengthX.value > 50) deleteCard()
+    transition.value = '150ms'
+
+    setTimeout(() => {
+      left.value = '0'
+      setTimeout(() => {
+        transition.value = 'none'
+      }, 50)
+    }, 100)
+  },
+})
+
 const update = () => emit('update')
 
 const deleteCard = () => emit('remove')
diff --git a/app/pages/index.vue b/app/pages/index.vue
index e6e1aca..5e39a87 100755
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -29,7 +29,7 @@
     </div>
   </section>
   <PpToolbar>
-    <PpButton class="mini-button text-white" @click="sort(currentSort)">
+    <PpButton class="mini-button text-white transparent" @click="sort(currentSort)">
       <Icon class="icon" name="uil:refresh" mode="svg" />
       <span>Neu sortieren</span>
       <span
@@ -38,7 +38,7 @@
         aria-hidden="true"
       />
     </PpButton>
-    <PpButton class="mini-button text-white" @click="openModal(true, -1)">
+    <PpButton class="mini-button text-white transparent" @click="openModal(true, -1)">
       <Icon class="icon" name="uil:plus" mode="svg" />
       <span>Hinzufügen</span>
     </PpButton>
diff --git a/app/utils/number.ts b/app/utils/number.ts
index 388e4cb..fb31fe2 100644
--- a/app/utils/number.ts
+++ b/app/utils/number.ts
@@ -1 +1,3 @@
-export const replaceComma = (value: string | number) => `${value}`.replace(',', '.');
\ No newline at end of file
+export const replaceComma = (value: string | number) => `${value}`.replace(',', '.')
+
+export const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)
\ No newline at end of file
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 4883d99..68c0d99 100755
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -22,9 +22,7 @@ export default defineNuxtConfig({
     '/privacy': { prerender: true },
   },
 
-  modules: [
-    '@nuxt/icon',
-  ],
+  modules: ['@nuxt/icon', '@vueuse/nuxt', '@nuxtjs/device'],
 
   css : [
     './app/assets/styles/general.css',
@@ -36,4 +34,4 @@ export default defineNuxtConfig({
     './app/assets/styles/formInput.css',
     './app/assets/styles/toolbar.css',
   ]
-})
+})
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index f8c69af..0ae7a87 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,8 @@
         "@iconify-json/simple-icons": "^1.2.32",
         "@iconify-json/uil": "^1.2.3",
         "@nuxt/icon": "^1.10.3",
+        "@nuxtjs/device": "^3.2.4",
+        "@vueuse/nuxt": "^13.1.0",
         "nuxt": "^3.16.2",
         "vue": "latest",
         "vue-router": "latest"
@@ -2664,6 +2666,15 @@
         "pathe": "^2.0.3"
       }
     },
+    "node_modules/@nuxtjs/device": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@nuxtjs/device/-/device-3.2.4.tgz",
+      "integrity": "sha512-jIvN6QeodBNrUrL/1FCHk4bebsiLsGHlJd8c/m2ksLrGY4IZ0npA8IYhDTdYV92epGxoe8+3iZOzCjav+6TshQ==",
+      "license": "MIT",
+      "dependencies": {
+        "defu": "^6.1.4"
+      }
+    },
     "node_modules/@oxc-parser/binding-darwin-arm64": {
       "version": "0.56.5",
       "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.56.5.tgz",
@@ -3793,6 +3804,12 @@
       "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
       "license": "MIT"
     },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.21",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+      "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+      "license": "MIT"
+    },
     "node_modules/@types/yauzl": {
       "version": "2.10.3",
       "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -4265,6 +4282,97 @@
       "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
       "license": "MIT"
     },
+    "node_modules/@vueuse/core": {
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.1.0.tgz",
+      "integrity": "sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.21",
+        "@vueuse/metadata": "13.1.0",
+        "@vueuse/shared": "13.1.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.1.0.tgz",
+      "integrity": "sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/nuxt": {
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/nuxt/-/nuxt-13.1.0.tgz",
+      "integrity": "sha512-4xdxwKanLY4+z+/ZgSZcJvwuHlgZMU3km7z4lhlbLl6WZTKS3BiztnRzcrdt4zjU512oTlH5nsPNhUhV0KXiOA==",
+      "license": "MIT",
+      "dependencies": {
+        "@nuxt/kit": "^3.16.2",
+        "@vueuse/core": "13.1.0",
+        "@vueuse/metadata": "13.1.0",
+        "local-pkg": "^1.1.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "nuxt": "^3.0.0 || ^4.0.0-0",
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/@vueuse/nuxt/node_modules/confbox": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
+      "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
+      "license": "MIT"
+    },
+    "node_modules/@vueuse/nuxt/node_modules/local-pkg": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
+      "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
+      "license": "MIT",
+      "dependencies": {
+        "mlly": "^1.7.4",
+        "pkg-types": "^2.0.1",
+        "quansync": "^0.2.8"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/nuxt/node_modules/pkg-types": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
+      "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==",
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.1",
+        "exsolve": "^1.0.1",
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.1.0.tgz",
+      "integrity": "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
     "node_modules/@whatwg-node/disposablestack": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",
diff --git a/package.json b/package.json
index 16aac4b..c56f122 100755
--- a/package.json
+++ b/package.json
@@ -15,6 +15,8 @@
     "@iconify-json/simple-icons": "^1.2.32",
     "@iconify-json/uil": "^1.2.3",
     "@nuxt/icon": "^1.10.3",
+    "@nuxtjs/device": "^3.2.4",
+    "@vueuse/nuxt": "^13.1.0",
     "nuxt": "^3.16.2",
     "vue": "latest",
     "vue-router": "latest"