itube-specs 0.0.902 → 0.0.910

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/assets/icons/common/flag.svg +1 -1
  2. package/assets/icons/common/heart-stroke.svg +1 -1
  3. package/assets/icons/common/heart.svg +1 -1
  4. package/assets/icons/common/share.svg +1 -1
  5. package/assets/icons/common/thumbs-down-filled.svg +1 -0
  6. package/assets/icons/common/thumbs-down.svg +1 -1
  7. package/assets/icons/common/thumbs-up-filled.svg +1 -0
  8. package/assets/icons/common/thumbs-up.svg +1 -1
  9. package/assets/scss/themes/dark/_theme.scss +49 -19
  10. package/assets/scss/themes/light/_theme.scss +46 -16
  11. package/assets/scss/vars/_palette.scss +5 -0
  12. package/assets/scss/vars/_vars.scss +1 -0
  13. package/components/auth/login.vue +2 -2
  14. package/components/auth/recovery.vue +2 -2
  15. package/components/auth/register.vue +2 -2
  16. package/components/card/collections-more.vue +133 -0
  17. package/components/card/playlist.vue +6 -1
  18. package/components/grid/playlists.vue +3 -0
  19. package/components/info/main.vue +18 -14
  20. package/components/library/feed.vue +20 -39
  21. package/components/library/follows.vue +19 -4
  22. package/components/player/interface.vue +132 -58
  23. package/components/player/like.vue +82 -60
  24. package/components/playlist/add.vue +2 -2
  25. package/components/playlist/delete-video.vue +2 -2
  26. package/components/playlist/edit.vue +3 -3
  27. package/components/profile/delete.vue +2 -2
  28. package/components/profile/setup.vue +2 -2
  29. package/components/report/main.vue +9 -4
  30. package/components/search/input.vue +9 -9
  31. package/components/section/collections.vue +110 -0
  32. package/components/ui/follow-btn.vue +162 -0
  33. package/components/ui/section-title.vue +2 -0
  34. package/components/ui/snackbar.vue +93 -44
  35. package/composables/__tests__/use-snackbar.test.ts +126 -20
  36. package/composables/use-snackbar.ts +75 -20
  37. package/composables/use-user.ts +4 -4
  38. package/package.json +1 -1
  39. package/pages/contacts.vue +2 -2
  40. package/pages/library/index.vue +1 -1
  41. package/pages/playlists/index.vue +55 -4
  42. package/assets/icons/common/crest-in-circle.svg +0 -1
@@ -28,7 +28,7 @@ import { AuthorizationApiService } from '../../services/api/authorization.servic
28
28
 
29
29
  const { t } = useI18n();
30
30
  const { signOut } = useUser(AuthorizationApiService);
31
- const { showSuccess } = useSnackbar();
31
+ const { show } = useSnackbar();
32
32
 
33
33
  const loading = ref(false);
34
34
  const isConfirmOpen = ref(false);
@@ -38,7 +38,7 @@ async function deleteProfile() {
38
38
  loading.value = true;
39
39
  await AuthorizationApiService.deleteProfile();
40
40
  isConfirmOpen.value = false;
41
- showSuccess('profile_deleted');
41
+ show('profile_deleted');
42
42
  signOut();
43
43
  } catch (err) {
44
44
  console.log(err, 'error delete profile');
@@ -187,7 +187,7 @@ function setDate(string: string) {
187
187
  }
188
188
 
189
189
  const deletingAvatar = ref(false);
190
- const { showSuccess } = useSnackbar();
190
+ const { show } = useSnackbar();
191
191
 
192
192
  async function deleteAvatar() {
193
193
  deletingAvatar.value = true;
@@ -196,7 +196,7 @@ async function deleteAvatar() {
196
196
  form.value.avatar = '';
197
197
  await getProfile(true);
198
198
  avatarPreview.value = '';
199
- showSuccess('avatar_deleted');
199
+ show('avatar_deleted');
200
200
  } finally {
201
201
  deletingAvatar.value = false;
202
202
  }
@@ -147,8 +147,9 @@
147
147
 
148
148
  <script setup lang="ts">
149
149
  import { reportFormsScheme } from '../../lib';
150
- import { ReportFormsSubjects, validateEmail, validatePhone } from '../../runtime';
150
+ import { checkDeviceWidth, ReportFormsSubjects, validateEmail, validatePhone } from '../../runtime';
151
151
  import type { InputTypes, IReportForm, IReportRequest } from '../../types';
152
+ import { CSS_BREAKPOINTS } from '../../config/css-breakpoints';
152
153
 
153
154
  const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
154
155
 
@@ -180,7 +181,11 @@ const {
180
181
  onBack,
181
182
  } = useReportPopup();
182
183
 
183
- const showBack = computed(() => activeStep.value !== 'list' || !!onBack.value);
184
+ const { isTablet } = checkDeviceWidth(CSS_BREAKPOINTS);
185
+
186
+ /* Возврат в меню карточки имеет смысл только там, где оно само — шит (до md);
187
+ на десктопе стрелка остаётся только для перехода со второго шага на список причин */
188
+ const showBack = computed(() => activeStep.value !== 'list' || (!!onBack.value && isTablet.value));
184
189
 
185
190
  function handleBack() {
186
191
  const callback = onBack.value;
@@ -243,7 +248,7 @@ function onFieldEnter(value: string) {
243
248
  else inputRefs.value[ list[ index + 1 ] ]?.focus();
244
249
  }
245
250
 
246
- const { showSuccess, showError, resetSnackbar } = useSnackbar();
251
+ const { show, showError, resetSnackbar } = useSnackbar();
247
252
 
248
253
  function resetForm() {
249
254
  Object.keys(form.value).forEach((key) => {
@@ -320,7 +325,7 @@ async function submit() {
320
325
  });
321
326
  closeReportPopup();
322
327
  resetForm();
323
- showSuccess('email_send');
328
+ show('email_send');
324
329
  } catch (error) {
325
330
  console.log('error send email', error);
326
331
  showError('email_error');
@@ -97,7 +97,7 @@ $icon-size: 16px;
97
97
  .search-input {
98
98
  position: relative;
99
99
  display: block;
100
- border-radius: $radius;
100
+ border-radius: $radius-12;
101
101
 
102
102
  @include to-br(sm) {
103
103
  background-color: $search-bg--mobile;
@@ -144,15 +144,15 @@ $icon-size: 16px;
144
144
  }
145
145
 
146
146
  .search-field__input::placeholder {
147
- @include font-sm;
147
+ @include to-br(sm) {
148
+ color: $search-placeholder--mobile;
149
+ }
148
150
 
149
- @include to-br(sm) {
150
- color: $search-placeholder--mobile;
151
- }
151
+ @include from-br(sm) {
152
+ @include font-sm;
152
153
 
153
- @include from-br(sm) {
154
- color: $search-placeholder;
155
- }
154
+ color: $search-placeholder;
155
+ }
156
156
  }
157
157
 
158
158
  .search-field__search {
@@ -165,7 +165,7 @@ $icon-size: 16px;
165
165
  color: $input-text;
166
166
  padding: 10px;
167
167
  background-color: $search-button-bg;
168
- border-radius: $radius - 2px;
168
+ border-radius: $radius-md;
169
169
 
170
170
  @include from-br(sm) {
171
171
  cursor: pointer;
@@ -0,0 +1,110 @@
1
+ <!-- Блок редакционных подборок: заголовок с бейджем, ссылка «все» и сетка карточек + плитка «ещё» -->
2
+ <template>
3
+ <section
4
+ v-if="items.length > 0"
5
+ class="section-collections"
6
+ >
7
+ <header class="section-collections__header">
8
+ <UiSectionTitle
9
+ :title="t('collections_by')"
10
+ title-tag="h2"
11
+ size="xl"
12
+ >
13
+ <span class="section-collections__badge">{{ t('editorial') }}</span>
14
+ </UiSectionTitle>
15
+
16
+ <UiLink
17
+ :to="generateLink(COLLECTIONS_LINK)"
18
+ theme="secondary"
19
+ class="section-collections__link"
20
+ >
21
+ {{ t('following.view_all') }}
22
+ <UiIcon
23
+ name="arrow-right"
24
+ size="16"
25
+ />
26
+ </UiLink>
27
+ </header>
28
+
29
+ <GridPlaylists
30
+ :items="items"
31
+ no-actions
32
+ >
33
+ <CardCollectionsMore
34
+ :to="COLLECTIONS_LINK"
35
+ :text="t('show_more_collections')"
36
+ :thumbs="restThumbs"
37
+ :count="restCount"
38
+ />
39
+ </GridPlaylists>
40
+ </section>
41
+ </template>
42
+
43
+ <script setup lang="ts">
44
+ import type { IPlaylistCard, IThumbUrls } from '../../types';
45
+
46
+ const props = withDefaults(defineProps<{
47
+ dataItems: IPlaylistCard[]
48
+ /** Сколько карточек показать до плитки «ещё» */
49
+ limit?: number
50
+ }>(), {
51
+ limit: 3,
52
+ });
53
+
54
+ const COLLECTIONS_LINK = '/playlists/collections';
55
+
56
+ /** Сетка 4×4 в плитке «ещё» */
57
+ const THUMBS_LIMIT = 16;
58
+
59
+ const { t } = useI18n();
60
+
61
+ const { generateLink } = useGenerateLink();
62
+
63
+ const items = computed(() => (props.dataItems || []).slice(0, props.limit));
64
+
65
+ const rest = computed(() => (props.dataItems || []).slice(props.limit));
66
+
67
+ const restCount = computed(() => rest.value.length);
68
+
69
+ /** Обложки для плитки «ещё»: сначала по первой от каждой скрытой подборки, потом по вторым и так далее */
70
+ const restThumbs = computed(() => {
71
+ const byIndex = (index: number) => rest.value
72
+ .map(item => item.thumbUrls?.[index])
73
+ .filter((thumb): thumb is IThumbUrls => Boolean(thumb));
74
+
75
+ return [0, 1, 2, 3].flatMap(index => byIndex(index)).slice(0, THUMBS_LIMIT);
76
+ });
77
+ </script>
78
+
79
+ <style lang="scss">
80
+ .section-collections {
81
+ margin-bottom: 40px;
82
+ }
83
+
84
+ .section-collections__header {
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: space-between;
88
+ column-gap: 16px;
89
+ margin-bottom: 24px;
90
+ }
91
+
92
+ .section-collections__badge {
93
+ @include font-3xs;
94
+
95
+ display: inline-flex;
96
+ align-items: center;
97
+ vertical-align: middle;
98
+ padding-block: 2px;
99
+ padding-inline: 8px;
100
+ font-weight: 600;
101
+ color: $collections-badge-text;
102
+ background-color: $collections-badge-bg;
103
+ border: 1px solid $collections-badge-border;
104
+ border-radius: $radius-full;
105
+ }
106
+
107
+ .section-collections__link {
108
+ flex-shrink: 0;
109
+ }
110
+ </style>
@@ -0,0 +1,162 @@
1
+ <!-- Кнопка подписки: «Follow» → «Following», при наведении на подписку становится красной «Unfollow». На мобиле только иконка. Отписка сама показывает снекбар с undo -->
2
+ <template>
3
+ <button
4
+ :aria-label="ariaLabel"
5
+ :class="[`--${size}`, { '--following': following }]"
6
+ class="ui-follow-btn"
7
+ type="button"
8
+ @click="onClick"
9
+ >
10
+ <UiIcon
11
+ :name="following ? 'check' : 'plus'"
12
+ class="ui-follow-btn__icon"
13
+ size="16"
14
+ />
15
+ <span class="ui-follow-btn__label">{{ following ? t('library.following_state') : t('follow') }}</span>
16
+
17
+ <template v-if="following">
18
+ <UiIcon
19
+ class="ui-follow-btn__icon-hover"
20
+ name="minus"
21
+ size="16"
22
+ />
23
+ <span class="ui-follow-btn__label-hover">{{ t('unfollow') }}</span>
24
+ </template>
25
+ </button>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ const props = withDefaults(
30
+ defineProps<{
31
+ /** Имя модели / канала / категории — уходит в aria-label, чтобы кнопки в списке различались на слух */
32
+ name: string
33
+ following?: boolean
34
+ /** `s` — под размеры `UiBtn size="s"` в шапках карточек */
35
+ size?: 'm' | 's'
36
+ }>(),
37
+ {
38
+ size: 'm',
39
+ }
40
+ );
41
+
42
+ const emit = defineEmits<{
43
+ (eventName: 'toggle'): void,
44
+ }>();
45
+
46
+ const { t } = useI18n();
47
+ const { show } = useSnackbar();
48
+
49
+ /**
50
+ * Снекбар с undo вшит в кнопку: отменять хочется только отписку, и текст у неё всегда
51
+ * один и тот же — раскладывать эту механику по каждому вызову смысла нет.
52
+ * Само состояние остаётся у родителя, undo просто переключает подписку обратно.
53
+ */
54
+ function onClick() {
55
+ const wasFollowing = props.following;
56
+
57
+ emit('toggle');
58
+
59
+ if (!wasFollowing) return;
60
+
61
+ show('unfollowed', {
62
+ icon: 'minus',
63
+ highlight: props.name,
64
+ onAction: () => emit('toggle'),
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Доступное имя повторяет видимую подпись (Following / Follow), а не действие: иначе
70
+ * ломается WCAG «Label in Name», ведь на мобиле подписи нет вообще.
71
+ */
72
+ const ariaLabel = computed(() => props.following
73
+ ? t('following_aria', { name: props.name })
74
+ : t('follow_aria', { name: props.name }));
75
+ </script>
76
+
77
+ <style lang="scss">
78
+ .ui-follow-btn {
79
+ @include font-sm;
80
+
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ column-gap: 6px;
85
+ flex-shrink: 0;
86
+ width: 36px;
87
+ height: 36px;
88
+ font-weight: 500;
89
+ border-radius: $radius-md;
90
+ background-color: $follow-btn-bg;
91
+ border: 1px solid $follow-btn-border;
92
+ color: $follow-btn-text;
93
+
94
+ @include from-br(sm) {
95
+ width: 124px;
96
+ height: auto;
97
+ padding: 8px 16px;
98
+ }
99
+
100
+ @include hover(background-color, border-color) {
101
+ background-color: $follow-btn-bg--hover;
102
+ border-color: $follow-btn-border--hover;
103
+ }
104
+ }
105
+
106
+ /* Размер под соседние `UiBtn size="s"`: без фиксированной ширины — в шапке кнопки жмутся по контенту */
107
+ .ui-follow-btn.--s {
108
+ width: 32px;
109
+ height: 32px;
110
+ border-radius: $radius-lg;
111
+
112
+ @include from-br(sm) {
113
+ width: max-content;
114
+ height: auto;
115
+ min-height: 32px;
116
+ padding: 6px 12px;
117
+ }
118
+ }
119
+
120
+ .ui-follow-btn.--following {
121
+ background-color: $follow-btn-following-bg;
122
+ border-color: $follow-btn-following-border;
123
+ color: $follow-btn-following-text;
124
+
125
+ @include hover(background-color, border-color, color) {
126
+ background-color: $follow-btn-following-bg--hover;
127
+ border-color: $follow-btn-following-border--hover;
128
+ color: $follow-btn-following-text--hover;
129
+ }
130
+ }
131
+
132
+ .ui-follow-btn__label {
133
+ display: none;
134
+
135
+ @include from-br(sm) {
136
+ display: block;
137
+ }
138
+ }
139
+
140
+ /* Подмена подписи и иконки — чистым CSS: на тач-устройствах ховера нет, а JS-стейт там «залипал» бы после тапа */
141
+ .ui-follow-btn__icon-hover,
142
+ .ui-follow-btn__label-hover {
143
+ display: none;
144
+ }
145
+
146
+ @media (hover: hover) {
147
+ .ui-follow-btn.--following:is(:hover, :focus-visible) .ui-follow-btn__icon,
148
+ .ui-follow-btn.--following:is(:hover, :focus-visible) .ui-follow-btn__label {
149
+ display: none;
150
+ }
151
+
152
+ .ui-follow-btn.--following:is(:hover, :focus-visible) .ui-follow-btn__icon-hover {
153
+ display: block;
154
+ }
155
+
156
+ @include from-br(sm) {
157
+ .ui-follow-btn.--following:is(:hover, :focus-visible) .ui-follow-btn__label-hover {
158
+ display: block;
159
+ }
160
+ }
161
+ }
162
+ </style>
@@ -26,6 +26,8 @@
26
26
  :icon="icon"
27
27
  class="ui-section-title__count"
28
28
  >{{ count }}</UiCount>
29
+ <!-- Доп. бейдж рядом с заголовком (например «editorial») -->
30
+ <slot/>
29
31
  </div>
30
32
  </template>
31
33
 
@@ -1,33 +1,68 @@
1
1
  <template>
2
2
  <div
3
- :class="`--${snackbarTheme}`"
3
+ aria-live="polite"
4
4
  class="ui-snackbar"
5
+ role="status"
6
+ @mouseenter="pauseDismiss"
7
+ @mouseleave="resumeDismiss"
8
+ @focusin="pauseDismiss"
9
+ @focusout="resumeDismiss"
5
10
  >
6
- <div class="ui-snackbar__icon-wrapper">
11
+ <span class="ui-snackbar__icon-wrapper">
7
12
  <UiIcon
8
13
  :name="snackbarIcon"
9
14
  class="ui-snackbar__icon"
10
15
  size="16"
11
16
  />
12
- </div>
13
- {{
14
- te(`snackbar.${convertSnackKey}`)
15
- ? t(`snackbar.${convertSnackKey}`)
16
- : t('snackbar.something_went_wrong')
17
- }}
17
+ </span>
18
+ <p class="ui-snackbar__text">
19
+ <span class="ui-snackbar__message">{{ message }}</span>
20
+ <strong
21
+ v-if="snackbarHighlight"
22
+ class="ui-snackbar__highlight"
23
+ >{{ snackbarHighlight }}</strong>
24
+ </p>
25
+ <button
26
+ v-if="snackbarActionText"
27
+ class="ui-snackbar__action"
28
+ type="button"
29
+ @click="runSnackbarAction"
30
+ >
31
+ {{ actionLabel }}
32
+ </button>
18
33
  </div>
19
34
  </template>
20
35
 
21
36
  <script setup lang="ts">
22
37
  import { convertString } from '../../runtime';
23
38
 
24
- const { snackbarText, snackbarIcon, snackbarTheme } = useSnackbar();
39
+ const {
40
+ snackbarText,
41
+ snackbarIcon,
42
+ snackbarHighlight,
43
+ snackbarActionText,
44
+ runSnackbarAction,
45
+ pauseDismiss,
46
+ resumeDismiss,
47
+ } = useSnackbar();
25
48
 
26
49
  const { t, te } = useI18n();
27
50
 
28
51
  const convertSnackKey = computed(() =>
29
52
  convertString().toSnakeCase(snackbarText.value)
30
53
  );
54
+
55
+ const message = computed(() =>
56
+ te(`snackbar.${convertSnackKey.value}`)
57
+ ? t(`snackbar.${convertSnackKey.value}`)
58
+ : t('snackbar.something_went_wrong')
59
+ );
60
+
61
+ const actionLabel = computed(() =>
62
+ te(`snackbar.${snackbarActionText.value}`)
63
+ ? t(`snackbar.${snackbarActionText.value}`)
64
+ : t('snackbar.undo')
65
+ );
31
66
  </script>
32
67
 
33
68
  <style lang="scss">
@@ -36,36 +71,38 @@ const convertSnackKey = computed(() =>
36
71
 
37
72
  position: fixed;
38
73
  z-index: $z-index-popup;
39
- left: 50%;
40
- transform: translateX(-50%);
41
- top: 16px;
42
- min-width: 100px;
43
- width: max-content;
44
- max-width: 90vw;
74
+ bottom: 16px;
75
+ left: 16px;
76
+ right: 16px;
45
77
  display: grid;
46
- grid-template-columns: auto 1fr auto;
78
+ grid-template-columns: auto minmax(0, 1fr) auto;
47
79
  align-items: center;
48
80
  column-gap: 12px;
49
81
  padding: 12px 16px;
50
- border-radius: $radius-lg;
82
+ border: 1px solid $snackbar-border;
83
+ border-radius: $radius-md;
84
+ background-color: $snackbar-bg;
51
85
  color: $snackbar-text;
86
+ box-shadow: $snackbar-shadow;
52
87
 
53
88
  @include from-br(sm) {
54
- top: 24px;
55
- left: auto;
56
- right: 24px;
57
- transform: none;
89
+ left: 0;
90
+ right: 0;
91
+ width: max-content;
92
+ min-width: 300px;
93
+ max-width: 448px;
94
+ margin-inline: auto;
58
95
  }
59
- }
60
96
 
61
- .ui-snackbar__close {
62
- padding: 4px;
63
- border-radius: 4px;
64
- color: $snackbar-close;
97
+ &.v-enter-active,
98
+ &.v-leave-active {
99
+ transition: opacity 300ms ease, transform 300ms ease;
100
+ }
65
101
 
66
- @include hover(color, background-color) {
67
- color: $snackbar-close--hover;
68
- background-color: $snackbar-close-bg--hover;
102
+ &.v-enter-from,
103
+ &.v-leave-to {
104
+ opacity: 0;
105
+ transform: translateY(16px);
69
106
  }
70
107
  }
71
108
 
@@ -75,31 +112,43 @@ const convertSnackKey = computed(() =>
75
112
  width: 32px;
76
113
  height: 32px;
77
114
  border-radius: $radius-full;
115
+ background-color: $snackbar-icon-bg;
78
116
  }
79
117
 
80
- .ui-snackbar.--error {
81
- background-color: $snackbar-error-bg;
82
- color: $snackbar-error;
118
+ .ui-snackbar__icon {
119
+ color: $snackbar-icon;
83
120
  }
84
121
 
85
- .ui-snackbar.--error .ui-snackbar__icon-wrapper {
86
- background-color: $snackbar-error-icon-bg;
122
+ .ui-snackbar__text {
123
+ display: flex;
124
+ align-items: center;
125
+ column-gap: 4px;
126
+ min-width: 0;
87
127
  }
88
128
 
89
- .ui-snackbar.--error .ui-snackbar__icon {
90
- color: $snackbar-error-icon;
129
+ .ui-snackbar__message {
130
+ flex-shrink: 0;
91
131
  }
92
132
 
93
- .ui-snackbar.--success {
94
- background-color: $snackbar-success-bg;
95
- color: $snackbar-success;
133
+ .ui-snackbar__highlight {
134
+ overflow: hidden;
135
+ white-space: nowrap;
136
+ text-overflow: ellipsis;
137
+ font-weight: 500;
138
+ color: $snackbar-highlight;
96
139
  }
97
140
 
98
- .ui-snackbar.--success .ui-snackbar__icon-wrapper {
99
- background-color: $snackbar-success-icon-bg;
100
- }
141
+ .ui-snackbar__action {
142
+ @include font-sm;
101
143
 
102
- .ui-snackbar.--success .ui-snackbar__icon {
103
- color: $snackbar-success-icon;
144
+ padding: 6px 12px;
145
+ border-radius: $radius-md;
146
+ font-weight: 600;
147
+ background-color: $snackbar-action-bg;
148
+ color: $snackbar-action-text;
149
+
150
+ @include hover(background-color) {
151
+ background-color: $snackbar-action-bg--hover;
152
+ }
104
153
  }
105
154
  </style>