itube-specs 0.0.911 → 0.0.913

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 (44) hide show
  1. package/assets/scss/base/_animations.scss +11 -10
  2. package/assets/scss/mixins/_font-sizes.scss +5 -2
  3. package/assets/scss/themes/dark/_theme.scss +22 -18
  4. package/assets/scss/themes/light/_theme.scss +17 -13
  5. package/assets/scss/vars/_tokens.scss +1 -1
  6. package/assets/scss/vars/_vars.scss +1 -0
  7. package/components/auth/register.vue +0 -4
  8. package/components/card/collections-more.vue +1 -1
  9. package/components/card/dropdown/index.vue +2 -2
  10. package/components/card/playlist.vue +7 -8
  11. package/components/card/video/actions.vue +2 -2
  12. package/components/card/video/index.vue +9 -9
  13. package/components/filter/popup.vue +1 -0
  14. package/components/library/feed.vue +1 -14
  15. package/components/playlist/add.vue +123 -113
  16. package/components/playlist/edit.vue +133 -199
  17. package/components/playlist/input.vue +23 -133
  18. package/components/playlist/main.vue +21 -40
  19. package/components/playlist/visibility.vue +149 -0
  20. package/components/section/collections.vue +2 -2
  21. package/components/section/swiper-cards.vue +3 -3
  22. package/components/ui/chips.vue +7 -2
  23. package/components/ui/confirm.vue +17 -5
  24. package/components/ui/dropdown.vue +17 -8
  25. package/components/ui/expand-row.vue +0 -4
  26. package/components/ui/popup.vue +33 -15
  27. package/components/ui/section-title.vue +6 -6
  28. package/components/ui/tooltip.vue +35 -14
  29. package/composables/__tests__/use-overlay-stack.test.ts +110 -0
  30. package/composables/__tests__/use-video-collections.test.ts +75 -0
  31. package/composables/use-overlay-stack.ts +58 -0
  32. package/composables/use-playlist-edit.ts +13 -3
  33. package/composables/use-snackbar.ts +15 -1
  34. package/composables/use-user.ts +0 -4
  35. package/composables/use-video-collections.ts +27 -5
  36. package/package.json +1 -1
  37. package/pages/following.vue +4 -4
  38. package/pages/history.vue +1 -1
  39. package/pages/library/index.vue +4 -4
  40. package/pages/playlists/index.vue +7 -76
  41. package/runtime/constants/playlist-type.ts +5 -0
  42. package/components/playlist/private-toggle.vue +0 -20
  43. package/composables/use-favorites.ts +0 -20
  44. package/pages/favorites.vue +0 -31
@@ -17,7 +17,7 @@
17
17
  <transition name="fade">
18
18
  <div
19
19
  v-if="isOpen"
20
- :class="{'--right': right}"
20
+ :style="contentStyle"
21
21
  class="ui-tooltip__content"
22
22
  >
23
23
  <slot/>
@@ -31,12 +31,15 @@ import { checkDeviceWidth } from '../../runtime';
31
31
  import { onClickOutside } from '@vueuse/core';
32
32
  import { CSS_BREAKPOINTS } from '../../config/css-breakpoints';
33
33
 
34
- defineProps<{
34
+ const props = defineProps<{
35
35
  right?: boolean
36
36
  }>();
37
37
 
38
+ const OFFSET = 8;
39
+
38
40
  const tooltipRef = ref<HTMLElement>();
39
41
  const isOpen = ref(false);
42
+ const contentStyle = ref<Record<string, string>>({});
40
43
 
41
44
  const breakpoints = CSS_BREAKPOINTS;
42
45
  const { isMobile } = checkDeviceWidth(breakpoints);
@@ -47,9 +50,26 @@ onClickOutside(tooltipRef, () => {
47
50
  }
48
51
  });
49
52
 
53
+ /** Контент в position: fixed — иначе его режет overflow скроллящихся родителей (список в попапе). */
54
+ function open() {
55
+ const rect = tooltipRef.value?.getBoundingClientRect();
56
+ if (!rect) return;
57
+
58
+ // На мобиле привязка к триггеру уводит широкий тултип за правый край — прибиваем к вьюпорту.
59
+ if (isMobile.value) {
60
+ contentStyle.value = { top: `${rect.top - OFFSET}px`, left: `${OFFSET}px` };
61
+ } else {
62
+ contentStyle.value = props.right
63
+ ? { top: `${rect.top - OFFSET}px`, left: `${rect.left}px` }
64
+ : { top: `${rect.top - OFFSET}px`, right: `${window.innerWidth - rect.right}px` };
65
+ }
66
+
67
+ isOpen.value = true;
68
+ }
69
+
50
70
  function onEnter() {
51
71
  if (!isMobile.value) {
52
- isOpen.value = true;
72
+ open();
53
73
  }
54
74
  }
55
75
 
@@ -60,13 +80,20 @@ function onLeave() {
60
80
  }
61
81
 
62
82
  function onClick() {
63
- if (isMobile.value) {
64
- isOpen.value = !isOpen.value;
83
+ if (!isMobile.value) return;
84
+
85
+ if (isOpen.value) {
86
+ isOpen.value = false;
87
+ } else {
88
+ open();
65
89
  }
66
90
  }
67
91
  </script>
68
92
 
69
93
  <style lang="scss">
94
+ /* Держим в паре с OFFSET в скрипте: отступ тултипа от края вьюпорта. */
95
+ $tooltip-viewport-gap: 8px;
96
+
70
97
  .ui-tooltip {
71
98
  position: relative;
72
99
  display: inline-flex;
@@ -80,26 +107,20 @@ function onClick() {
80
107
  }
81
108
 
82
109
  .ui-tooltip__content {
83
- position: absolute;
84
- bottom: 100%;
85
- right: 0;
110
+ position: fixed;
111
+ transform: translateY(-100%);
86
112
  z-index: $z-index-tooltip;
87
113
  display: grid;
88
- margin-bottom: 8px;
89
114
  padding: 12px;
90
115
  background-color: $tooltip-bg;
91
116
  border-radius: $radius-lg;
92
117
  color: $tooltip-text;
93
118
  width: max-content;
119
+ max-width: calc(100vw - #{$tooltip-viewport-gap * 2});
94
120
  pointer-events: none;
95
121
  box-shadow: $tooltip-shadow;
96
122
  }
97
123
 
98
- .ui-tooltip__content.--right {
99
- right: auto;
100
- left: 0;
101
- }
102
-
103
124
  .fade-enter-active,
104
125
  .fade-leave-active {
105
126
  transition: opacity 100ms ease;
@@ -0,0 +1,110 @@
1
+ // @vitest-environment nuxt
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
+ import { useOverlayStack } from '../use-overlay-stack';
4
+
5
+ const NO_ENTER = '--no-enter-anim';
6
+ const NO_LEAVE = '--no-leave-anim';
7
+
8
+ let frames: Array<() => void> = [];
9
+
10
+ function flushFrames() {
11
+ const queued = frames;
12
+ frames = [];
13
+ queued.forEach((cb) => cb());
14
+ }
15
+
16
+ function createOverlay() {
17
+ const el = document.createElement('dialog');
18
+ document.body.appendChild(el);
19
+
20
+ return el;
21
+ }
22
+
23
+ describe('useOverlayStack', () => {
24
+ beforeEach(() => {
25
+ frames = [];
26
+ vi.stubGlobal('requestAnimationFrame', (cb: () => void) => {
27
+ frames.push(cb);
28
+
29
+ return 0;
30
+ });
31
+ });
32
+
33
+ afterEach(() => {
34
+ flushFrames();
35
+ document.body.innerHTML = '';
36
+ vi.unstubAllGlobals();
37
+ });
38
+
39
+ it('первый оверлей анимируется в обе стороны', () => {
40
+ const { registerOverlay } = useOverlayStack();
41
+ const el = createOverlay();
42
+
43
+ registerOverlay(el);
44
+
45
+ expect(el.classList.contains(NO_ENTER)).toBe(false);
46
+ expect(el.classList.contains(NO_LEAVE)).toBe(false);
47
+ });
48
+
49
+ it('второй оверлей открывается без анимации появления', () => {
50
+ const { registerOverlay } = useOverlayStack();
51
+ const first = createOverlay();
52
+ const second = createOverlay();
53
+
54
+ registerOverlay(first);
55
+ registerOverlay(second);
56
+
57
+ expect(second.classList.contains(NO_ENTER)).toBe(true);
58
+ });
59
+
60
+ it('пока оверлеев больше одного, закрытие не анимируется ни у одного', () => {
61
+ const { registerOverlay } = useOverlayStack();
62
+ const first = createOverlay();
63
+ const second = createOverlay();
64
+
65
+ registerOverlay(first);
66
+ registerOverlay(second);
67
+
68
+ expect(first.classList.contains(NO_LEAVE)).toBe(true);
69
+ expect(second.classList.contains(NO_LEAVE)).toBe(true);
70
+ });
71
+
72
+ it('уходящий оверлей остаётся в стеке до конца кадра — сменщик успевает открыться без анимации', () => {
73
+ const { registerOverlay, releaseOverlay } = useOverlayStack();
74
+ const closing = createOverlay();
75
+ const opening = createOverlay();
76
+
77
+ registerOverlay(closing);
78
+ releaseOverlay(closing);
79
+ registerOverlay(opening);
80
+
81
+ expect(opening.classList.contains(NO_ENTER)).toBe(true);
82
+ expect(closing.classList.contains(NO_LEAVE)).toBe(true);
83
+ });
84
+
85
+ it('оставшийся последним снова закрывается с анимацией', () => {
86
+ const { registerOverlay, releaseOverlay } = useOverlayStack();
87
+ const closing = createOverlay();
88
+ const opening = createOverlay();
89
+
90
+ registerOverlay(closing);
91
+ releaseOverlay(closing);
92
+ registerOverlay(opening);
93
+ flushFrames();
94
+
95
+ expect(opening.classList.contains(NO_LEAVE)).toBe(false);
96
+ });
97
+
98
+ it('оверлей, удалённый из DOM без релиза, не блокирует анимации следующему', () => {
99
+ const { registerOverlay } = useOverlayStack();
100
+ const leaked = createOverlay();
101
+ const next = createOverlay();
102
+
103
+ registerOverlay(leaked);
104
+ leaked.remove();
105
+ registerOverlay(next);
106
+
107
+ expect(next.classList.contains(NO_ENTER)).toBe(false);
108
+ expect(next.classList.contains(NO_LEAVE)).toBe(false);
109
+ });
110
+ });
@@ -0,0 +1,75 @@
1
+ // @vitest-environment nuxt
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
+ import { useVideoCollections } from '../use-video-collections';
4
+ import { useSnackbar } from '../use-snackbar';
5
+
6
+ describe('useVideoCollections', () => {
7
+ beforeEach(() => {
8
+ useNuxtApp().payload.state = {};
9
+ vi.useFakeTimers();
10
+ useSnackbar().resetSnackbar();
11
+ });
12
+
13
+ afterEach(() => {
14
+ vi.useRealTimers();
15
+ });
16
+
17
+ it('добавляет в «смотреть позже» и показывает снекбар с кнопкой отмены', () => {
18
+ const { watchLater, isWatchLater, toggleWatchLater } = useVideoCollections();
19
+ const { snackbarText, snackbarIcon, snackbarHighlight, snackbarActionText } = useSnackbar();
20
+
21
+ toggleWatchLater('guid-1', 'Video title');
22
+
23
+ expect(watchLater.value).toEqual([ 'guid-1' ]);
24
+ expect(isWatchLater('guid-1')).toBe(true);
25
+ expect(snackbarText.value).toBe('added');
26
+ expect(snackbarIcon.value).toBe('clock');
27
+ expect(snackbarHighlight.value).toBe('Video title');
28
+ expect(snackbarActionText.value).toBe('undo');
29
+ });
30
+
31
+ it('удаление показывает снекбар «removed»', () => {
32
+ const { watchLater, toggleWatchLater } = useVideoCollections();
33
+ const { snackbarText } = useSnackbar();
34
+
35
+ toggleWatchLater('guid-1', 'Video title');
36
+ toggleWatchLater('guid-1', 'Video title');
37
+
38
+ expect(watchLater.value).toEqual([]);
39
+ expect(snackbarText.value).toBe('removed');
40
+ });
41
+
42
+ it('избранное показывает снекбар с иконкой сердца', () => {
43
+ const { favorites, isFavorite, toggleFavorite } = useVideoCollections();
44
+ const { snackbarIcon, snackbarText } = useSnackbar();
45
+
46
+ toggleFavorite('guid-2', 'Another title');
47
+
48
+ expect(favorites.value).toEqual([ 'guid-2' ]);
49
+ expect(isFavorite('guid-2')).toBe(true);
50
+ expect(snackbarIcon.value).toBe('heart');
51
+ expect(snackbarText.value).toBe('added');
52
+ });
53
+
54
+ it('отмена возвращает список и не показывает новый снекбар', () => {
55
+ const { favorites, toggleFavorite } = useVideoCollections();
56
+ const { runSnackbarAction, snackbarText } = useSnackbar();
57
+
58
+ toggleFavorite('guid-2', 'Another title');
59
+ runSnackbarAction();
60
+
61
+ expect(favorites.value).toEqual([]);
62
+ expect(snackbarText.value).toBe('');
63
+ });
64
+
65
+ it('отмена удаления возвращает ролик обратно', () => {
66
+ const { watchLater, toggleWatchLater } = useVideoCollections();
67
+ const { runSnackbarAction } = useSnackbar();
68
+
69
+ toggleWatchLater('guid-3', 'Third title');
70
+ toggleWatchLater('guid-3', 'Third title');
71
+ runSnackbarAction();
72
+
73
+ expect(watchLater.value).toEqual([ 'guid-3' ]);
74
+ });
75
+ });
@@ -0,0 +1,58 @@
1
+ const NO_ENTER_ANIM_CLASS = '--no-enter-anim';
2
+ const NO_LEAVE_ANIM_CLASS = '--no-leave-anim';
3
+
4
+ // Элементы, а не счётчик: класс вешается прямо на DOM. Уходящий компонент управлять
5
+ // своим `:class` уже не может — Vue гасит его реактивный эффект в unmount, до конца
6
+ // leave-транзишена, поэтому ре-рендер по реактивному флагу не случится.
7
+ const overlays = new Set<HTMLElement>();
8
+
9
+ function syncLeaveAnimation() {
10
+ const chained = overlays.size > 1;
11
+ overlays.forEach((el) => el.classList.toggle(NO_LEAVE_ANIM_CLASS, chained));
12
+ }
13
+
14
+ /**
15
+ * Стек открытых оверлеев (попапы и мобильные шиты дропдауна). Нужен, чтобы анимация
16
+ * появления играла только у первого оверлея, а анимация закрытия — только у последнего:
17
+ * при переходе попап→попап уходящий и приходящий диалоги ехали бы навстречу друг другу
18
+ * и картинка скакала бы.
19
+ */
20
+ export const useOverlayStack = () => {
21
+ /**
22
+ * Решение по анимации появления принимается один раз и дальше не меняется: состав
23
+ * стека успеет измениться до того, как браузер прочитает transition уходящего.
24
+ */
25
+ function registerOverlay(el?: HTMLElement | null) {
26
+ if (!import.meta.client || !el) return;
27
+
28
+ overlays.forEach((item) => {
29
+ if (!item.isConnected) overlays.delete(item);
30
+ });
31
+
32
+ el.classList.toggle(NO_ENTER_ANIM_CLASS, overlays.size > 0);
33
+ overlays.add(el);
34
+ syncLeaveAnimation();
35
+ }
36
+
37
+ /**
38
+ * Выпуск отложен на кадр: следующий оверлей монтируется позже unmount-хука уходящего
39
+ * (а ленивый — ещё и через микротаск). Убрать элемент из стека сразу — уходящий не
40
+ * успеет узнать, что ему на смену открывают другой попап, и проиграет закрытие.
41
+ */
42
+ function releaseOverlay(el?: HTMLElement | null) {
43
+ if (!import.meta.client || !el) return;
44
+
45
+ const release = () => {
46
+ overlays.delete(el);
47
+ syncLeaveAnimation();
48
+ };
49
+
50
+ if (typeof requestAnimationFrame === 'function') {
51
+ requestAnimationFrame(release);
52
+ } else {
53
+ release();
54
+ }
55
+ }
56
+
57
+ return { registerOverlay, releaseOverlay };
58
+ };
@@ -8,6 +8,8 @@ export const usePlaylistEdit = () => {
8
8
  const isPlaylistEditOpen = useState('playlist-edit-open', () => false);
9
9
  const isBackToEdit = useState('playlist-edit-back', () => false);
10
10
  const currentStep = useState<PlaylistStep>('playlist-edit-step', () => PlaylistStep.Edit);
11
+ /** Возврат к попапу, из которого открыли редактирование. */
12
+ const onBack = useState<(() => void) | null>('playlist-edit-on-back', () => null);
11
13
 
12
14
  const openPlaylistEdit = (step: PlaylistStep, back: boolean = false, playlist?: IPlaylistCard, videoCard?: IVideoCard) => {
13
15
  selectedPlaylist.value = playlist;
@@ -17,13 +19,19 @@ export const usePlaylistEdit = () => {
17
19
  deletedVideo.value = videoCard;
18
20
  };
19
21
 
22
+ /** Закрывает попап целиком, минуя возврат на шаг редактирования. */
23
+ const closePlaylistEditAll = () => {
24
+ isPlaylistEditOpen.value = false;
25
+ isBackToEdit.value = false;
26
+ selectedPlaylist.value = undefined;
27
+ onBack.value = null;
28
+ };
29
+
20
30
  const closePlaylistEdit = () => {
21
31
  if (isBackToEdit.value) {
22
32
  openPlaylistEdit(PlaylistStep.Edit, false, selectedPlaylist.value);
23
33
  } else {
24
- isPlaylistEditOpen.value = false;
25
- isBackToEdit.value = false;
26
- selectedPlaylist.value = undefined;
34
+ closePlaylistEditAll();
27
35
  }
28
36
  };
29
37
 
@@ -32,7 +40,9 @@ export const usePlaylistEdit = () => {
32
40
  currentStep,
33
41
  openPlaylistEdit,
34
42
  closePlaylistEdit,
43
+ closePlaylistEditAll,
35
44
  selectedPlaylist,
45
+ onBack,
36
46
  deletedVideo,
37
47
  };
38
48
  };
@@ -36,7 +36,19 @@ export const useSnackbar = () => {
36
36
  const snackbarText = useState('snackbar-text', () => '');
37
37
  const snackbarHighlight = useState('snackbar-highlight', () => '');
38
38
  const snackbarActionText = useState('snackbar-action-text', () => '');
39
- const isSnackBarInPopup = useState('snackbar-in-popup', () => false);
39
+ // Снекбар физически переезжает между открытым попапом и макетом: диалог лежит в top
40
+ // layer, поэтому снекбар из макета оказался бы под ним и под блюром. Счётчик, а не флаг —
41
+ // при закрытии вложенного попапа снекбар должен остаться в том, что ещё открыт.
42
+ const popupDepth = useState('snackbar-popup-depth', () => 0);
43
+ const isSnackBarInPopup = computed(() => popupDepth.value > 0);
44
+
45
+ function enterPopup() {
46
+ popupDepth.value++;
47
+ }
48
+
49
+ function leavePopup() {
50
+ popupDepth.value = Math.max(0, popupDepth.value - 1);
51
+ }
40
52
 
41
53
  function clearTimer() {
42
54
  if (dismissTimer) {
@@ -115,6 +127,8 @@ export const useSnackbar = () => {
115
127
  snackbarHighlight,
116
128
  snackbarActionText,
117
129
  isSnackBarInPopup,
130
+ enterPopup,
131
+ leavePopup,
118
132
  show,
119
133
  showError,
120
134
  setErrorState,
@@ -119,10 +119,6 @@ export const useUser = (apiService) => {
119
119
  const profile = useState<IProfileData | null>('profileData');
120
120
  profile.value = null;
121
121
 
122
- const { favoritesId, favoritesFirstVideoId } = useFavorites();
123
- favoritesId.value = null;
124
- favoritesFirstVideoId.value = null;
125
-
126
122
  const router = useRouter();
127
123
  router.push(generateLink('/'));
128
124
  };
@@ -7,17 +7,39 @@ export const useVideoCollections = () => {
7
7
  const favorites = useState<string[]>('video-collections-favorites', () => []);
8
8
  const watchLater = useState<string[]>('video-collections-watch-later', () => []);
9
9
 
10
+ const { show } = useSnackbar();
11
+
12
+ /** Возвращает true, если ролик добавлен, и false — если убран. */
10
13
  function toggle(list: Ref<string[]>, guid: string) {
11
- list.value = list.value.includes(guid)
12
- ? list.value.filter(item => item !== guid)
13
- : [ ...list.value, guid ];
14
+ const added = !list.value.includes(guid);
15
+
16
+ list.value = added
17
+ ? [ ...list.value, guid ]
18
+ : list.value.filter(item => item !== guid);
19
+
20
+ return added;
21
+ }
22
+
23
+ // Снекбар показывается только на исходное действие: откат возвращает список как был,
24
+ // повторно сообщать об этом нечего — иначе отмена сама выглядит как новое изменение.
25
+ function notify(added: boolean, title: string, icon: string, undo: () => void) {
26
+ show(added ? 'added' : 'removed', {
27
+ icon,
28
+ highlight: title,
29
+ onAction: undo,
30
+ });
14
31
  }
15
32
 
16
33
  const isFavorite = (guid: string) => favorites.value.includes(guid);
17
34
  const isWatchLater = (guid: string) => watchLater.value.includes(guid);
18
35
 
19
- const toggleFavorite = (guid: string) => toggle(favorites, guid);
20
- const toggleWatchLater = (guid: string) => toggle(watchLater, guid);
36
+ const toggleFavorite = (guid: string, title = '') => {
37
+ notify(toggle(favorites, guid), title, 'heart', () => toggle(favorites, guid));
38
+ };
39
+
40
+ const toggleWatchLater = (guid: string, title = '') => {
41
+ notify(toggle(watchLater, guid), title, 'clock', () => toggle(watchLater, guid));
42
+ };
21
43
 
22
44
  return {
23
45
  favorites,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.911",
4
+ "version": "0.0.913",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -17,7 +17,7 @@
17
17
  :header-link-text="t('following.all_models')"
18
18
  all-link="/library/models"
19
19
  icon="user"
20
- size="xl"
20
+ variant="section"
21
21
  placeholder-avatar="/img/model-placeholder.webp"
22
22
  class="following__section"
23
23
  without-count
@@ -32,7 +32,7 @@
32
32
  :header-link-text="t('following.all_channels')"
33
33
  all-link="/library/channels"
34
34
  icon="play-circle"
35
- size="xl"
35
+ variant="section"
36
36
  placeholder-avatar="/img/logo-placeholder.svg"
37
37
  class="following__section"
38
38
  without-count
@@ -45,7 +45,7 @@
45
45
  :count="t('following.updates', { count: categoriesUpdates })"
46
46
  icon="grid"
47
47
  title-tag="h2"
48
- size="xl"
48
+ variant="section"
49
49
  />
50
50
 
51
51
  <!-- На мобиле плитки «все» в гриде нет, поэтому ссылка вынесена в шапку -->
@@ -79,7 +79,7 @@
79
79
  :count="t('following.updates', { count: playlistsUpdates })"
80
80
  icon="playlist"
81
81
  title-tag="h2"
82
- size="xl"
82
+ variant="section"
83
83
  />
84
84
 
85
85
  <UiLink
package/pages/history.vue CHANGED
@@ -55,7 +55,7 @@
55
55
  class="history__group-header"
56
56
  icon="video-camera"
57
57
  title-tag="h2"
58
- size="xl"
58
+ variant="section"
59
59
  />
60
60
 
61
61
  <div class="history__grid">
@@ -25,14 +25,14 @@
25
25
  :title="t('library.continue_watching')"
26
26
  icon="history"
27
27
  title-tag="h2"
28
- size="xl"
28
+ variant="section"
29
29
  />
30
30
 
31
31
  <UiLink
32
32
  :to="generateLink('/history')"
33
33
  theme="secondary"
34
34
  >
35
- {{ t('library.view_all_history') }}
35
+ {{ t('library.history') }}
36
36
  <UiIcon
37
37
  name="arrow-right"
38
38
  size="16"
@@ -61,7 +61,7 @@
61
61
  :title="t('library.collections')"
62
62
  icon="layers"
63
63
  title-tag="h2"
64
- size="xl"
64
+ variant="section"
65
65
  />
66
66
  </header>
67
67
 
@@ -86,7 +86,7 @@
86
86
  :title="t('library.following')"
87
87
  icon="plus"
88
88
  title-tag="h2"
89
- size="xl"
89
+ variant="section"
90
90
  />
91
91
 
92
92
  <UiLink