Bläddra i källkod

style: accessibility improvements (aria-labels, form labels, contrast)

unknown 2 dagar sedan
förälder
incheckning
e3fc3f40cd

+ 1 - 1
src/components/Header.vue

@@ -70,7 +70,7 @@
           <LanguageSwitcher />
 
           <!-- Hamburger -->
-          <Button variant="ghost" size="icon" class="lg:hidden" @click="mobileOpen = !mobileOpen">
+          <Button variant="ghost" size="icon" class="lg:hidden" @click="mobileOpen = !mobileOpen" :aria-label="t('nav.toggleMenu')">
             <X v-if="mobileOpen" class="w-5 h-5" />
             <Menu v-else class="w-5 h-5" />
           </Button>

+ 5 - 3
src/components/LanguageSwitcher.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="relative" ref="containerRef">
-    <Button variant="ghost" size="icon" class="gap-1.5 w-auto px-2" @click="isOpen = !isOpen">
+    <Button variant="ghost" size="icon" class="gap-1.5 w-auto px-2" @click="isOpen = !isOpen" :aria-label="t('nav.changeLanguage')">
       <Globe class="w-4 h-4" />
-      <span class="text-sm">{{ currentLang.flag }}</span>
-      <ChevronDown class="w-3 h-3" />
+      <span class="text-sm font-medium">{{ currentLang.flag }}</span>
+      <ChevronDown class="w-3 h-3 text-muted-foreground/60" />
     </Button>
 
     <Transition
@@ -39,7 +39,9 @@ import { onClickOutside } from "@vueuse/core";
 import { Globe, ChevronDown } from "lucide-vue-next";
 import Button from "./ui/button.vue";
 import { currentLanguage } from "@/i18n";
+import { useI18n } from "vue-i18n";
 
+const { t } = useI18n();
 const languages = [
   { code: "en", label: "English", flag: "🇬🇧" },
   { code: "ru", label: "Русский", flag: "🇷🇺" },

+ 10 - 10
src/components/ModelUploadSection.vue

@@ -15,43 +15,43 @@
         <!-- Contact -->
         <div class="grid sm:grid-cols-2 gap-6">
           <div class="space-y-1.5">
-            <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
+            <label for="upload-first-name" class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
               {{ t("upload.firstName") }} *
             </label>
-            <input v-model="firstName" type="text" required :placeholder="t('upload.firstName')"
+            <input id="upload-first-name" v-model="firstName" type="text" required :placeholder="t('upload.firstName')"
               class="w-full bg-secondary/50 border border-black/[0.03] rounded-2xl px-4 py-2.5 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
           </div>
           <div class="space-y-1.5">
-            <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
+            <label for="upload-last-name" class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
               {{ t("upload.lastName") }} *
             </label>
-            <input v-model="lastName" type="text" required :placeholder="t('upload.lastName')"
+            <input id="upload-last-name" v-model="lastName" type="text" required :placeholder="t('upload.lastName')"
               class="w-full bg-secondary/50 border border-black/[0.03] rounded-2xl px-4 py-2.5 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
           </div>
         </div>
 
         <div class="grid sm:grid-cols-2 gap-6">
           <div class="space-y-1.5">
-            <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
+            <label for="upload-email" class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
               {{ t("upload.email") }} *
             </label>
-            <input v-model="email" type="email" required :placeholder="t('upload.email')"
+            <input id="upload-email" v-model="email" type="email" required :placeholder="t('upload.email')"
               class="w-full bg-secondary/50 border border-black/[0.03] rounded-2xl px-4 py-2.5 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
           </div>
           <div class="space-y-1.5">
-            <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
+            <label for="upload-phone" class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
               {{ t("upload.phone") }} *
             </label>
-            <input v-model="phone" type="tel" required :placeholder="t('upload.phone')"
+            <input id="upload-phone" v-model="phone" type="tel" required :placeholder="t('upload.phone')"
               class="w-full bg-secondary/50 border border-black/[0.03] rounded-2xl px-4 py-2.5 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
           </div>
         </div>
 
         <div class="space-y-1.5">
-          <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
+          <label for="upload-address" class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
             {{ t("upload.shippingAddress") }} *
           </label>
-          <textarea v-model="address" required :placeholder="t('upload.addressPlaceholder')" rows="2"
+          <textarea id="upload-address" v-model="address" required :placeholder="t('upload.addressPlaceholder')" rows="2"
             class="w-full bg-secondary/50 border border-black/[0.03] rounded-2xl px-4 py-3 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium resize-none" />
         </div>
 

+ 1 - 1
src/index.css

@@ -23,7 +23,7 @@
     --secondary-foreground: 240 5.9% 10%;
 
     --muted: 240 4.8% 95.9%;
-    --muted-foreground: 240 3.8% 46.1%;
+    --muted-foreground: 240 3.8% 40%;
 
     --accent: 240 4.8% 95.9%;
     --accent-foreground: 240 5.9% 10%;

+ 12 - 0
src/locales/translations.user.json

@@ -2018,6 +2018,18 @@
       "ru": "Регистрация",
       "ua": "Реєстрація"
     },
+    "toggleMenu": {
+      "en": "Toggle Menu",
+      "me": "Otvori/Zatvori meni",
+      "ru": "Открыть/Закрыть меню",
+      "ua": "Відкрити/Закрити меню"
+    },
+    "changeLanguage": {
+      "en": "Change Language",
+      "me": "Promijeni jezik",
+      "ru": "Сменить язык",
+      "ua": "Змінити мову"
+    },
     "services": {
       "en": "Services",
       "me": "Usluge",

+ 16 - 17
src/pages/Auth.vue

@@ -40,12 +40,12 @@
         <form @submit.prevent="handleSubmit" class="space-y-5">
           <!-- Email -->
           <div v-if="mode !== 'reset'" class="space-y-2">
-            <label class="text-xs font-semibold uppercase tracking-wider text-muted-foreground ml-1">{{ t("auth.fields.email") }}</label>
+            <label for="email" class="text-xs font-semibold uppercase tracking-wider text-muted-foreground ml-1">{{ t("auth.fields.email") }}</label>
             <div class="relative group">
               <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors">
                 <Mail class="w-4 h-4" />
               </div>
-              <input v-model="formData.email" type="email" required placeholder="john@example.com"
+              <input id="email" v-model="formData.email" type="email" required placeholder="john@example.com"
                 class="w-full bg-background/50 border border-border/50 rounded-xl pl-11 pr-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all text-sm" />
             </div>
           </div>
@@ -109,8 +109,8 @@
                 class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-2.5 focus:outline-none focus:ring-1 focus:ring-primary/30 text-sm" />
             </div>
             <div class="space-y-1.5">
-              <label class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("upload.shippingAddress") }}</label>
-              <textarea v-model="formData.address" rows="2"
+              <label for="shipping-address" class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("upload.shippingAddress") }}</label>
+              <textarea id="shipping-address" v-model="formData.address" rows="2"
                 class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-2.5 focus:outline-none focus:ring-1 focus:ring-primary/30 text-sm resize-none" />
             </div>
 
@@ -118,18 +118,18 @@
             <Transition enter-active-class="transition duration-200" enter-from-class="opacity-0 -translate-y-2" enter-to-class="opacity-100 translate-y-0">
                <div v-if="formData.is_company" class="space-y-4 pt-4 border-t border-border/50">
                   <div class="space-y-1.5">
-                    <label class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("auth.fields.companyName") }}</label>
-                    <input v-model="formData.company_name" type="text" :required="formData.is_company"
+                    <label for="company-name" class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("auth.fields.companyName") }}</label>
+                    <input id="company-name" v-model="formData.company_name" type="text" :required="formData.is_company"
                       class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-2.5 focus:outline-none focus:ring-1 focus:ring-primary/30 text-sm" />
                   </div>
                   <div class="space-y-1.5">
-                    <label class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("auth.fields.companyPIB") }}</label>
-                    <input v-model="formData.company_pib" type="text" :required="formData.is_company"
+                    <label for="company-pib" class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("auth.fields.companyPIB") }}</label>
+                    <input id="company-pib" v-model="formData.company_pib" type="text" :required="formData.is_company"
                       class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-2.5 focus:outline-none focus:ring-1 focus:ring-primary/30 text-sm" />
                   </div>
                   <div class="space-y-1.5">
-                    <label class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("auth.fields.companyAddress") }}</label>
-                    <textarea v-model="formData.company_address" rows="2"
+                    <label for="company-address" class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("auth.fields.companyAddress") }}</label>
+                    <textarea id="company-address" v-model="formData.company_address" rows="2"
                       class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-2.5 focus:outline-none focus:ring-1 focus:ring-primary/30 text-sm resize-none" />
                   </div>
                </div>
@@ -140,9 +140,7 @@
           <div v-if="mode !== 'forgot'" class="space-y-5">
             <div class="space-y-2 pt-2">
               <div class="flex justify-between items-center px-1">
-                <label class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
-                  {{ mode === "reset" ? t("auth.fields.newPassword") : t("auth.fields.password") }}
-                </label>
+                <label for="password" class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">{{ mode === 'reset' ? t("auth.fields.newPassword") : t("auth.fields.password") }}</label>
                 <button v-if="mode === 'login'" type="button" @click="mode = 'forgot'"
                   class="text-[11px] text-primary hover:underline font-medium">{{ t("auth.forgot.link") }}</button>
               </div>
@@ -150,18 +148,19 @@
                 <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors">
                   <Lock class="w-4 h-4" />
                 </div>
-                <input v-model="formData.password" type="password" required placeholder="••••••••"
+                <input id="password" v-model="formData.password" type="password" required placeholder="••••••••"
                   class="w-full bg-background/50 border border-border/50 rounded-xl pl-11 pr-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all text-sm" />
               </div>
             </div>
 
-            <div v-if="mode === 'register' || mode === 'reset'" class="space-y-2">
-              <label class="text-xs font-semibold uppercase tracking-wider text-muted-foreground ml-1">{{ t("auth.fields.confirmPassword") }}</label>
+            <!-- Confirm Password (Register/Reset only) -->
+            <div v-if="mode === 'register' || mode === 'reset'" class="space-y-2 animate-in fade-in slide-in-from-top-2 duration-300">
+              <label for="confirm-password" class="text-xs font-semibold uppercase tracking-wider text-muted-foreground ml-1">{{ t("auth.fields.confirmPassword") }}</label>
               <div class="relative group">
                 <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors">
                   <ShieldCheck class="w-4 h-4" />
                 </div>
-                <input v-model="formData.confirmPassword" type="password" required placeholder="••••••••"
+                <input id="confirm-password" v-model="formData.confirmPassword" type="password" required placeholder="••••••••"
                   class="w-full bg-background/50 border border-border/50 rounded-xl pl-11 pr-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all text-sm" />
               </div>
             </div>