Repository: js-tasks-ru/vue-20250304_ktrnsajbel
Pull request #7: solving tasks from 4 and 5 modules
Author: login: KatyaFront
Created: 2025-04-03T10:57:59Z | Merged: 2025-04-16T16:56:00Z



Diff:
diff --git a/04-sfc/10-MeetupView-script-setup/MeetupAgenda.vue b/04-sfc/10-MeetupView-script-setup/MeetupAgenda.vue
index d5dee66..a549a9f 100644
--- a/04-sfc/10-MeetupView-script-setup/MeetupAgenda.vue
+++ b/04-sfc/10-MeetupView-script-setup/MeetupAgenda.vue
@@ -1,19 +1,10 @@
-<script>
-import { defineComponent } from 'vue'
+<script setup>
 import MeetupAgendaItem from './MeetupAgendaItem.vue'
 
-export default defineComponent({
-  name: 'MeetupAgenda',
-
-  components: {
-    MeetupAgendaItem,
-  },
-
-  props: {
-    agenda: {
-      type: Array,
-      required: true,
-    },
+defineProps({
+  agenda: {
+    type: Array,
+    required: true,
   },
 })
 </script>
diff --git a/04-sfc/10-MeetupView-script-setup/MeetupAgendaItem.vue b/04-sfc/10-MeetupView-script-setup/MeetupAgendaItem.vue
index 5674dd3..584a8e6 100644
--- a/04-sfc/10-MeetupView-script-setup/MeetupAgendaItem.vue
+++ b/04-sfc/10-MeetupView-script-setup/MeetupAgendaItem.vue
@@ -1,5 +1,5 @@
-<script>
-import { computed, defineComponent } from 'vue'
+<script setup>
+import { computed } from 'vue'
 import { UiIcon } from '@shgk/vue-course-ui'
 
 const agendaItemDefaultTitles = {
@@ -24,29 +24,15 @@ const agendaItemIcons = {
   other: 'cal-sm',
 }
 
-export default defineComponent({
-  name: 'MeetupAgendaItem',
-
-  components: {
-    UiIcon,
-  },
-
-  props: {
-    agendaItem: {
-      type: Object,
-      required: true,
-    },
-  },
-
-  setup(props) {
-    const icon = computed(() => agendaItemIcons[props.agendaItem.type])
-    const title = computed(() => agendaItemDefaultTitles[props.agendaItem.type])
-    return {
-      icon,
-      title,
-    }
+const props = defineProps({
+  agendaItem: {
+    type: Object,
+    required: true,
   },
 })
+
+const icon = computed(() => agendaItemIcons[props.agendaItem.type])
+const title = computed(() => agendaItemDefaultTitles[props.agendaItem.type])
 </script>
 
 <template>
diff --git a/04-sfc/10-MeetupView-script-setup/MeetupCover.vue b/04-sfc/10-MeetupView-script-setup/MeetupCover.vue
index b53fc38..f324363 100644
--- a/04-sfc/10-MeetupView-script-setup/MeetupCover.vue
+++ b/04-sfc/10-MeetupView-script-setup/MeetupCover.vue
@@ -1,26 +1,15 @@
-<script>
-import { computed, defineComponent } from 'vue'
-
-export default defineComponent({
-  name: 'MeetupCover',
-
-  props: {
-    title: {
-      type: String,
-    },
-
-    image: {
-      type: String,
-    },
+<script setup>
+import { computed } from 'vue'
+const props = defineProps({
+  title: {
+    type: String,
   },
-
-  setup(props) {
-    const bgStyle = computed(() => (props.image ? { '--bg-url': `url('${props.image}')` } : undefined))
-    return {
-      bgStyle,
-    }
+  image: {
+    type: String,
   },
 })
+
+const bgStyle = computed(() => (props.image ? { '--bg-url': `url('${props.image}')` } : undefined))
 </script>
 
 <template>
diff --git a/04-sfc/10-MeetupView-script-setup/MeetupDescription.vue b/04-sfc/10-MeetupView-script-setup/MeetupDescription.vue
index c8f4185..79485f9 100644
--- a/04-sfc/10-MeetupView-script-setup/MeetupDescription.vue
+++ b/04-sfc/10-MeetupView-script-setup/MeetupDescription.vue
@@ -1,13 +1,7 @@
-<script>
-import { defineComponent } from 'vue'
-
-export default defineComponent({
-  name: 'MeetupDescription',
-
-  props: {
-    description: {
-      type: String,
-    },
+<script setup>
+defineProps({
+  description: {
+    type: String,
   },
 })
 </script>
diff --git a/04-sfc/10-MeetupView-script-setup/MeetupInfo.vue b/04-sfc/10-MeetupView-script-setup/MeetupInfo.vue
index 1daf6c5..292cac7 100644
--- a/04-sfc/10-MeetupView-script-setup/MeetupInfo.vue
+++ b/04-sfc/10-MeetupView-script-setup/MeetupInfo.vue
@@ -1,43 +1,27 @@
-<script>
-import { computed, defineComponent } from 'vue'
+<script setup>
+import { computed } from 'vue'
 import { UiIcon } from '@shgk/vue-course-ui'
 
-export default defineComponent({
-  name: 'MeetupInfo',
-
-  components: {
-    UiIcon,
+const props = defineProps({
+  organizer: {
+    type: String,
   },
-
-  props: {
-    organizer: {
-      type: String,
-    },
-
-    place: {
-      type: String,
-    },
-
-    date: {
-      type: Number,
-    },
+  place: {
+    type: String,
   },
-
-  setup(props) {
-    const isoDate = computed(() => new Date(props.date).toISOString().slice(0, 10))
-    const localDate = computed(() =>
-      new Date(props.date).toLocaleString(navigator.language, {
-        year: 'numeric',
-        month: 'long',
-        day: 'numeric',
-      }),
-    )
-    return {
-      isoDate,
-      localDate,
-    }
+  date: {
+    type: Number,
   },
 })
+
+const isoDate = computed(() => new Date(props.date).toISOString().slice(0, 10))
+const localDate = computed(() =>
+  new Date(props.date).toLocaleString(navigator.language, {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+  }),
+)
 </script>
 
 <template>
diff --git a/04-sfc/10-MeetupView-script-setup/MeetupView.vue b/04-sfc/10-MeetupView-script-setup/MeetupView.vue
index 1250e12..11c1af7 100644
--- a/04-sfc/10-MeetupView-script-setup/MeetupView.vue
+++ b/04-sfc/10-MeetupView-script-setup/MeetupView.vue
@@ -1,17 +1,14 @@
-<script>
-import { defineComponent } from 'vue'
+<script setup>
 import { UiAlert, UiContainer } from '@shgk/vue-course-ui'
 import MeetupAgenda from './MeetupAgenda.vue'
 import MeetupDescription from './MeetupDescription.vue'
 import MeetupCover from './MeetupCover.vue'
 import MeetupInfo from './MeetupInfo.vue'
 
-export default defineComponent({
-  name: 'MeetupView',
-
-  components: {
-    UiAlert,
-    UiContainer,
+defineProps({
+  meetup: {
+    type: Object,
+    required: true,
   },
 })
 </script>
@@ -19,6 +16,7 @@ export default defineComponent({
 <template>
   <div>
     <!-- Обложка митапа -->
+    <MeetupCover :title="meetup.title" :image="meetup.image" />
 
     <UiContainer>
       <div class="meetup">
@@ -26,15 +24,18 @@ export default defineComponent({
           <h2>Описание</h2>
 
           <!-- Описание митапа -->
+          <MeetupDescription :description="meetup.description" />
 
           <h2>Программа</h2>
 
           <!-- Программа митапа -->
+          <MeetupAgenda v-if="meetup.agenda?.length" :agenda="meetup.agenda" />
           <!-- Или при пустой программе - сообщение "Программа пока пуста..." в UiAlert -->
-          <UiAlert></UiAlert>
+          <UiAlert v-else>Программа пока пуста...</UiAlert>
         </div>
         <div class="meetup__aside">
           <!-- Краткая информация о митапе -->
+          <MeetupInfo :organizer="meetup.organizer" :place="meetup.place" :date="meetup.date" />
 
           <div class="meetup__aside-buttons"></div>
         </div>
diff --git a/04-sfc/20-MeetupCover-style-v-bind/MeetupCover.vue b/04-sfc/20-MeetupCover-style-v-bind/MeetupCover.vue
index fbbc245..712c95d 100644
--- a/04-sfc/20-MeetupCover-style-v-bind/MeetupCover.vue
+++ b/04-sfc/20-MeetupCover-style-v-bind/MeetupCover.vue
@@ -11,11 +11,11 @@ const props = defineProps({
   },
 })
 
-const bgStyle = computed(() => (props.image ? { '--bg-url': `url('${props.image}')` } : undefined))
+const bgStyle = computed(() => (props.image ? `url('${props.image}')` : 'var(--default-cover)'))
 </script>
 
 <template>
-  <div class="meetup-cover" :style="bgStyle">
+  <div class="meetup-cover">
     <h1 class="meetup-cover__title">{{ title }}</h1>
   </div>
 </template>
@@ -26,7 +26,7 @@ const bgStyle = computed(() => (props.image ? { '--bg-url': `url('${props.image}
   background-position: center;
   /* Если изображение присутствует - берём его из CSS переменной, установленной на элемент в шаблоне */
   /* Иначе выводим изображение по умолчанию - var(--default-cover) */
-  background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), var(--bg-url, var(--default-cover));
+  background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), v-bind(bgStyle);
   display: flex;
   flex-direction: column;
   align-items: center;
diff --git a/04-sfc/30-UiStretch/UiStretch.vue b/04-sfc/30-UiStretch/UiStretch.vue
index 237c492..065f6dd 100644
--- a/04-sfc/30-UiStretch/UiStretch.vue
+++ b/04-sfc/30-UiStretch/UiStretch.vue
@@ -12,9 +12,9 @@
   height: 100%;
 }
 
-.stretch-container > img,
-.stretch-container > video,
-.stretch-container > picture {
+:deep(.stretch-container > img),
+:deep(.stretch-container > video),
+:deep(.stretch-container > picture) {
   object-fit: scale-down;
 }
 </style>
diff --git a/05-ts/10-MeetupView-TS/MeetupAgenda.vue b/05-ts/10-MeetupView-TS/MeetupAgenda.vue
index f3e1ce3..51c6835 100644
--- a/05-ts/10-MeetupView-TS/MeetupAgenda.vue
+++ b/05-ts/10-MeetupView-TS/MeetupAgenda.vue
@@ -1,14 +1,10 @@
-<script setup>
-// import type { MeetupAgendaItemDTO } from '@shgk/vue-course-ui'
+<script setup lang="ts">
+import type { MeetupAgendaItemDTO } from '@shgk/vue-course-ui'
 import MeetupAgendaItem from './MeetupAgendaItem.vue'
 
-defineProps({
-  agenda: {
-    // Настоящий тип - массив объектов типа MeetupAgendaItemDTO
-    type: Array,
-    required: true,
-  },
-})
+defineProps<{
+  agenda: MeetupAgendaItemDTO[],
+}>();
 </script>
 
 <template>
diff --git a/05-ts/10-MeetupView-TS/MeetupAgendaItem.vue b/05-ts/10-MeetupView-TS/MeetupAgendaItem.vue
index 1496cd8..995d7e8 100644
--- a/05-ts/10-MeetupView-TS/MeetupAgendaItem.vue
+++ b/05-ts/10-MeetupView-TS/MeetupAgendaItem.vue
@@ -1,15 +1,11 @@
-<script setup>
-// import type { MeetupAgendaItemDTO } from '@shgk/vue-course-ui'
+<script setup lang="ts">
+import type { MeetupAgendaItemDTO } from '@shgk/vue-course-ui'
 import { computed } from 'vue'
 import { UiIcon } from '@shgk/vue-course-ui'
 
-const props = defineProps({
-  agendaItem: {
-    // Настоящий тип - MeetupAgendaItemDTO
-    type: Object,
-    required: true,
-  },
-})
+const props = defineProps<{
+  agendaItem: MeetupAgendaItemDTO,
+}>();
 
 const agendaItemDefaultTitles = {
   registration: 'Регистрация',
@@ -34,7 +30,7 @@ const agendaItemIcons = {
   closing: 'key',
   afterparty: 'cal-sm',
   other: 'cal-sm',
-} // as const
+} as const
 
 const icon = computed(() => agendaItemIcons[props.agendaItem.type])
 const title = computed(() => agendaItemDefaultTitles[props.agendaItem.type])
diff --git a/05-ts/10-MeetupView-TS/MeetupCover.vue b/05-ts/10-MeetupView-TS/MeetupCover.vue
index 069b144..ee5bea9 100644
--- a/05-ts/10-MeetupView-TS/MeetupCover.vue
+++ b/05-ts/10-MeetupView-TS/MeetupCover.vue
@@ -1,16 +1,10 @@
-<script setup>
+<script setup lang="ts">
 import { computed } from 'vue'
 
-const props = defineProps({
-  title: {
-    type: String,
-    required: true,
-  },
-
-  image: {
-    type: String,
-  },
-})
+const props = defineProps<{
+  title: string
+  image?: string
+}>();
 
 const bgStyle = computed(() => (props.image ? { '--bg-url': `url('${props.image}')` } : undefined))
 </script>
diff --git a/05-ts/10-MeetupView-TS/MeetupDescription.vue b/05-ts/10-MeetupView-TS/MeetupDescription.vue
index 79485f9..d26c8b7 100644
--- a/05-ts/10-MeetupView-TS/MeetupDescription.vue
+++ b/05-ts/10-MeetupView-TS/MeetupDescription.vue
@@ -1,9 +1,7 @@
-<script setup>
-defineProps({
-  description: {
-    type: String,
-  },
-})
+<script setup lang="ts">
+defineProps<{
+  description?: string,
+}>();
 </script>
 
 <template>
diff --git a/05-ts/10-MeetupView-TS/MeetupInfo.vue b/05-ts/10-MeetupView-TS/MeetupInfo.vue
index 3c331d4..3c077b9 100644
--- a/05-ts/10-MeetupView-TS/MeetupInfo.vue
+++ b/05-ts/10-MeetupView-TS/MeetupInfo.vue
@@ -1,23 +1,12 @@
-<script setup>
+<script setup lang="ts">
 import { computed } from 'vue'
 import { UiIcon } from '@shgk/vue-course-ui'
 
-const props = defineProps({
-  organizer: {
-    type: String,
-    required: true,
-  },
-
-  place: {
-    type: String,
-    required: true,
-  },
-
-  date: {
-    type: Number,
-    required: true,
-  },
-})
+const props = defineProps<{
+  organizer: string,
+  place: string,
+  date: number,
+}>();
 
 const isoDate = computed(() => new Date(props.date).toISOString().slice(0, 10))
 const localDate = computed(() =>
diff --git a/05-ts/10-MeetupView-TS/MeetupView.vue b/05-ts/10-MeetupView-TS/MeetupView.vue
index a4c5f6f..63b4644 100644
--- a/05-ts/10-MeetupView-TS/MeetupView.vue
+++ b/05-ts/10-MeetupView-TS/MeetupView.vue
@@ -1,34 +1,40 @@
-<script setup>
-// import type { MeetupDTO } from '@shgk/vue-course-ui'
+<script setup lang="ts">
+import type { MeetupDTO } from '@shgk/vue-course-ui'
 import { UiAlert, UiContainer } from '@shgk/vue-course-ui'
 import MeetupAgenda from './MeetupAgenda.vue'
 import MeetupDescription from './MeetupDescription.vue'
 import MeetupCover from './MeetupCover.vue'
 import MeetupInfo from './MeetupInfo.vue'
 
-defineProps({
-  meetup: {
-    // Настоящий тип - MeetupDTO
-    type: Object,
-    required: true,
-  },
-})
+defineProps<{
+  meetup: MeetupDTO,
+}>();
 </script>
 
 <template>
   <div>
+    <!-- Обложка митапа -->
     <MeetupCover :title="meetup.title" :image="meetup.image" />
+
     <UiContainer>
       <div class="meetup">
         <div class="meetup__content">
           <h2>Описание</h2>
+
+          <!-- Описание митапа -->
           <MeetupDescription :description="meetup.description" />
+
           <h2>Программа</h2>
-          <MeetupAgenda v-if="meetup.agenda.length" :agenda="meetup.agenda" />
+
+          <!-- Программа митапа -->
+          <MeetupAgenda v-if="meetup.agenda?.length" :agenda="meetup.agenda" />
+          <!-- Или при пустой программе - сообщение "Программа пока пуста..." в UiAlert -->
           <UiAlert v-else>Программа пока пуста...</UiAlert>
         </div>
         <div class="meetup__aside">
+          <!-- Краткая информация о митапе -->
           <MeetupInfo :organizer="meetup.organizer" :place="meetup.place" :date="meetup.date" />
+
           <div class="meetup__aside-buttons"></div>
         </div>
       </div>
diff --git a/05-ts/20-UiFormGroup/UiFormGroup.vue b/05-ts/20-UiFormGroup/UiFormGroup.vue
index 4784b42..c9bb74d 100644
--- a/05-ts/20-UiFormGroup/UiFormGroup.vue
+++ b/05-ts/20-UiFormGroup/UiFormGroup.vue
@@ -1,13 +1,42 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import type { Slot } from 'vue'
+
+defineProps<{
+  for?: string,
+  label?: string,
+  description?: string,
+  hint?: string,
+  showHint?: boolean,
+  invalid?: boolean,
+}>()
+
+defineSlots<{
+  default?: Slot,
+  label?: Slot,
+  description?: Slot,
+}>();
+</script>
 
 <template>
   <div class="form-group">
     <div class="form-group__label-wrapper">
-      <label for="FOR" class="form-group__label">LABEL</label>
-      <div class="form-group__description">DESCRIPTION</div>
+      <label :for="$props.for" class="form-group__label">
+        <slot name="label">
+          {{ label }}
+        </slot>
+      </label>
+      <div class="form-group__description">
+        <slot name="description">
+          {{ description }}
+        </slot>
+      </div>   
+    </div>
+    <div class="form-group__control">
+      <slot />
+    </div>
+    <div v-if="hint !== undefined" class="form-group__hint" :class="{'form-group__hint--invalid': invalid}">
+      <span v-if="showHint || invalid">{{ hint }}</span>
     </div>
-    <div class="form-group__control">CONTENT</div>
-    <div class="form-group__hint form-group__hint--invalid">HINT | ERROR</div>
   </div>
 </template>
 
diff --git a/05-ts/30-UiSelect/App.vue b/05-ts/30-UiSelect/App.vue
index ed0728c..ef3dd59 100644
--- a/05-ts/30-UiSelect/App.vue
+++ b/05-ts/30-UiSelect/App.vue
@@ -22,6 +22,6 @@ function handleUpdate(newValue: SelectValue) {}
   <UiContainer>
     <p>Выбрано: {{ selected }}</p>
     <!-- Если тип компонента будет некорректным, использование handleUpdate приведёт здесь к ошибке TS -->
-    <UiSelect v-model="selected" :options @update:model-value="handleUpdate" />
+    <UiSelect v-model="selected" :options @update:model-value="handleUpdate"/>
   </UiContainer>
 </template>
diff --git a/05-ts/30-UiSelect/UiSelect.vue b/05-ts/30-UiSelect/UiSelect.vue
index 9ab5eab..c8b10b9 100644
--- a/05-ts/30-UiSelect/UiSelect.vue
+++ b/05-ts/30-UiSelect/UiSelect.vue
@@ -1,9 +1,21 @@
-<script setup lang="ts"></script>
+<script setup lang="ts" generic="T">
+type Option<T> = {
+  value: T;
+  text: string;
+};
+
+const modelValue = defineModel<T>({ required: true });
+
+defineProps<{
+  options: Option<T>[],
+}>();
+</script>
 
 <template>
-  <select class="select">
-    <option value="value-1">One</option>
-    <option value="value-2">Two</option>
+  <select v-model="modelValue" class="select">
+    <option v-for="(option, index) in options" :key="index" :value="option.value">
+      {{ option.text}}
+    </option>
   </select>
 </template>
 


Reviews:
user: login: ShGKme
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T18:48:14Z
user: login: ShGKme
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T20:01:04Z
user: login: ShGKme
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:54:40Z

Review comments:
diff_hunk: @@ -1,19 +1,11 @@
-<script>
-import { defineComponent } from 'vue'
+<script setup>
+import { defineProps } from 'vue'
path: 04-sfc/10-MeetupView-script-setup/MeetupAgenda.vue
user: login: ShGKme
body: `defineProps` - это не настоящая функция, она не существует, как функция, и не выполняется во время работы приложения. Это макрос компиляции - просто указание компилятору на часть кода, которую компилятор будет использовать для генерации кода компонента. Поэтому их не нужно импортировать (в терминале сборки должно быть предупреждение).
created_at: 2025-04-03T18:22:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,19 +1,11 @@
-<script>
-import { defineComponent } from 'vue'
+<script setup>
+import { defineProps } from 'vue'
path: 04-sfc/10-MeetupView-script-setup/MeetupAgenda.vue
user: login: ShGKme
body: Тоже в других файлах
created_at: 2025-04-03T18:23:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,9 +12,9 @@
   height: 100%;
 }
 
-.stretch-container > img,
+:deep(.stretch-container > img,
 .stretch-container > video,
-.stretch-container > picture {
+.stretch-container > picture) {
path: 04-sfc/30-UiStretch/UiStretch.vue
user: login: ShGKme
body: `:deep()` / `:slotted()` работают как и другие CSS псевдо-селекторы (за исключением `:is()`). В них можно передать только другой селектор, но нельзя передать перечисление селекторов. Они должны быть применены к каждому селектору в перечислении по отдельности.
created_at: 2025-04-03T18:40:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,10 +30,10 @@ const agendaItemIcons = {
   closing: 'key',
   afterparty: 'cal-sm',
   other: 'cal-sm',
-} // as const
+} as const
 
-const icon = computed(() => agendaItemIcons[props.agendaItem.type])
-const title = computed(() => agendaItemDefaultTitles[props.agendaItem.type])
+const icon = computed(() => agendaItemIcons[props.agendaItem.type as keyof typeof agendaItemIcons])
+const title = computed(() => agendaItemDefaultTitles[props.agendaItem.type as keyof typeof agendaItemIcons])
path: 05-ts/10-MeetupView-TS/MeetupAgendaItem.vue
user: login: ShGKme
body: После добавления `as const` к константам выше, здесь уже не должно требоваться добавлять `as` - TS должен сам понимать, что значения этого объекта подходят вод этот тип.
created_at: 2025-04-03T18:41:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,10 +30,10 @@ const agendaItemIcons = {
   closing: 'key',
   afterparty: 'cal-sm',
   other: 'cal-sm',
-} // as const
+} as const
 
-const icon = computed(() => agendaItemIcons[props.agendaItem.type])
-const title = computed(() => agendaItemDefaultTitles[props.agendaItem.type])
+const icon = computed(() => agendaItemIcons[props.agendaItem.type as keyof typeof agendaItemIcons])
+const title = computed(() => agendaItemDefaultTitles[props.agendaItem.type as keyof typeof agendaItemIcons])
path: 05-ts/10-MeetupView-TS/MeetupAgendaItem.vue
user: login: ShGKme
body: Для `title` этот тип некорректный. Заголовок - не иконка, а просто строка.
created_at: 2025-04-03T18:43:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,13 +1,42 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import { defineProps, defineSlots } from 'vue';
+
+defineProps<{
+  for?: string,
+  label?: string,
+  description?: string,
+  hint?: string,
+  showHint?: boolean,
+  invalid?: boolean,
+}>()
+
+defineSlots<{
+  default(): void
+  label(): void
+  description(): void
+}>();
path: 05-ts/20-UiFormGroup/UiFormGroup.vue
user: login: ShGKme
body: Это работает, так как в большинстве случаев точный тип для описания слотов не требуется, но:
1. Все слоты необязательные и описываются с `?:`
2. Функции в слотах на самом деле возвращают не `void`, а массив виртуальных узлов `VNode[]`. Во Vue уже есть тип для такой функции - это `Slot`

Потому корректный тип, это:
```ts
import type { Slot } from 'vue'
defineSlots<{
  default?: Slot
  label?: Slot
  description?: Slot
}>()
```

created_at: 2025-04-03T18:46:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,13 +1,42 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import { defineProps, defineSlots } from 'vue';
+
+defineProps<{
+  for?: string,
+  label?: string,
+  description?: string,
+  hint?: string,
+  showHint?: boolean,
+  invalid?: boolean,
+}>()
+
+defineSlots<{
+  default(): void
+  label(): void
+  description(): void
+}>();
+</script>
 
 <template>
   <div class="form-group">
     <div class="form-group__label-wrapper">
-      <label for="FOR" class="form-group__label">LABEL</label>
-      <div class="form-group__description">DESCRIPTION</div>
+      <label :for="$props.for" class="form-group__label">
+        <slot name="label">
+          {{ label ? label : 'LABEL'}}
+        </slot>
+      </label>
+      <div class="form-group__description">
+        <slot name="description">
+          {{ description ? description : 'DESCRIPTION'}}
+        </slot>
+      </div>   
+    </div>
+    <div class="form-group__control">
+      <slot name="default">CONTENT</slot>
+    </div>
+    <div v-if="hint !== undefined" class="form-group__hint" :class="{'form-group__hint--invalid': invalid}">
+      <span v-if="showHint || invalid">{{ hint ? hint : 'HINT' }}</span>
     </div>
path: 05-ts/20-UiFormGroup/UiFormGroup.vue
user: login: ShGKme
body: Строки `LABEL`, `DESCRIPTION` и т.д. - просто примеры в начальной вёрстке, чтобы было понятно "где что", но они не должны выводиться в компоненте (в них нет смысла для пользователя).
created_at: 2025-04-03T18:47:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +22,6 @@ function handleUpdate(newValue: SelectValue) {}
   <UiContainer>
     <p>Выбрано: {{ selected }}</p>
     <!-- Если тип компонента будет некорректным, использование handleUpdate приведёт здесь к ошибке TS -->
-    <UiSelect v-model="selected" :options @update:model-value="handleUpdate" />
+    <UiSelect v-model="selected" :options />
path: 05-ts/30-UiSelect/App.vue
user: login: ShGKme
body: Обработчик события тут добавлен намеренно, чтобы проверить работу компонента.
created_at: 2025-04-03T18:47:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,9 +1,23 @@
-<script setup lang="ts"></script>
+<script setup lang="ts" generic="T">
+import { defineModel, defineProps } from 'vue';
+
+type Option = {
+  value: string;
+  text: string;
+};
+
+const modelValue = defineModel<T>({ required: true });
+
+defineProps<{
+  options: Option[],
+}>();
path: 05-ts/30-UiSelect/UiSelect.vue
user: login: ShGKme
body: Сейчас нет связи между типом модели (выбранного варианта) и списком вариантов.
Согласно типы - `value` - это любая строка, а модель - вообще любое значение, в том числе любая (другая) строка.

generic тип нужен, чтобы связать значение вариантов и значение модели - у них должен быть один generic тип.

created_at: 2025-04-03T18:48:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,20 +22,20 @@ defineSlots<{
     <div class="form-group__label-wrapper">
       <label :for="$props.for" class="form-group__label">
         <slot name="label">
-          {{ label ? label : 'LABEL'}}
+          {{ label }}
         </slot>
       </label>
       <div class="form-group__description">
         <slot name="description">
-          {{ description ? description : 'DESCRIPTION'}}
+          {{ description }}
         </slot>
       </div>   
     </div>
     <div class="form-group__control">
-      <slot name="default">CONTENT</slot>
+      <slot name="default" />
path: 05-ts/20-UiFormGroup/UiFormGroup.vue
user: login: ShGKme
body: Имя `default` можно не указывать - это значение по умолчанию.
created_at: 2025-04-04T19:02:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,9 +1,21 @@
-<script setup lang="ts"></script>
+<script setup lang="ts" generic="T">
+type Option = {
+  value: string;
+  text: string;
+};
+
+const modelValue = defineModel<T>({ required: true });
+
+defineProps<{
+  options: Option[],
+}>();
path: 05-ts/30-UiSelect/UiSelect.vue
user: login: ShGKme
body: Здесь проблема осталась...


created_at: 2025-04-04T20:00:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: jsru-1
created_at: 2025-04-03T10:58:32Z
body: Добавляю преподавателя (@ShGKme) для код-ревью.
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-04-04T14:00:20Z
body: Добавляю преподавателя (@ShGKme) для код-ревью.
author_association: CONTRIBUTOR
user: login: KatyaFront
created_at: 2025-04-04T14:00:32Z
body: замечания исправлены
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-04-16T08:04:03Z
body: Добавляю преподавателя (@ShGKme) для код-ревью.
author_association: CONTRIBUTOR
user: login: KatyaFront
created_at: 2025-04-16T08:04:39Z
body: замечания исправлены
author_association: CONTRIBUTOR