erudit 3.0.0-dev.18 → 3.0.0-dev.19

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 (67) hide show
  1. package/app/assets/icons/graduation.svg +3 -0
  2. package/app/components/aside/AsideMinor.vue +41 -17
  3. package/app/components/aside/major/panes/Pages.vue +11 -14
  4. package/app/components/aside/minor/{Contribute.vue → AsideMinorContribute.vue} +35 -5
  5. package/app/components/aside/minor/content/AsideMinorContent.vue +7 -10
  6. package/app/components/aside/minor/contributor/AsideMinorContributor.vue +78 -0
  7. package/app/components/aside/minor/contributor/BookContribution.vue +64 -0
  8. package/app/components/aside/minor/topic/AsideMinorTopic.vue +1 -4
  9. package/app/components/aside/minor/topic/TopicContributors.vue +3 -3
  10. package/app/components/aside/minor/topic/TopicNav.vue +6 -6
  11. package/app/components/aside/minor/topic/TopicToc.vue +1 -2
  12. package/app/components/bitran/BitranContent.vue +15 -16
  13. package/app/components/contributor/ContributorAvatar.vue +3 -3
  14. package/app/components/main/MainBitranContent.vue +41 -0
  15. package/app/components/main/{utils/Breadcrumb.vue → MainBreadcrumb.vue} +12 -19
  16. package/app/components/main/MainDescription.vue +24 -0
  17. package/app/components/main/{utils/ContentTitle.vue → MainTitle.vue} +11 -2
  18. package/app/components/main/content/ContentBreadcrumb.vue +28 -0
  19. package/app/components/main/{utils → content}/ContentSection.vue +2 -2
  20. package/app/components/main/topic/MainTopic.vue +15 -20
  21. package/app/components/main/topic/TopicPartSwitch.vue +9 -3
  22. package/app/components/preview/display/Unique.vue +2 -11
  23. package/app/composables/adsAllowed.ts +1 -1
  24. package/app/composables/majorPane.ts +3 -2
  25. package/app/composables/phrases.ts +21 -9
  26. package/app/pages/book/[...bookId].vue +6 -11
  27. package/app/pages/contributor/[contributorId].vue +225 -0
  28. package/app/pages/contributors.vue +183 -0
  29. package/app/pages/group/[...groupId].vue +11 -19
  30. package/app/scripts/preview/data/unique.ts +18 -21
  31. package/languages/en.ts +12 -3
  32. package/languages/ru.ts +12 -3
  33. package/package.json +5 -5
  34. package/server/api/aside/minor/book/[...bookId].ts +18 -0
  35. package/server/api/aside/minor/contributor/[contributorId].ts +18 -0
  36. package/server/api/aside/minor/group/[...groupId].ts +18 -0
  37. package/server/api/aside/minor/news.ts +2 -2
  38. package/server/api/aside/minor/topic.ts +36 -0
  39. package/server/api/bitran/content/[...location].ts +4 -1
  40. package/server/api/contributor/list.ts +44 -0
  41. package/server/api/contributor/page/[contributorId].ts +14 -0
  42. package/server/api/preview/unique/[...location].ts +10 -23
  43. package/server/plugin/bitran/content.ts +34 -19
  44. package/server/plugin/bitran/location.ts +6 -2
  45. package/server/plugin/build/jobs/contributors.ts +3 -0
  46. package/server/plugin/db/entities/Contributor.ts +9 -0
  47. package/server/plugin/repository/asideMinor.ts +51 -0
  48. package/server/plugin/repository/book.ts +16 -0
  49. package/server/plugin/repository/contributor.ts +90 -0
  50. package/shared/aside/minor.ts +32 -28
  51. package/shared/bitran/content.ts +9 -0
  52. package/shared/breadcrumb.ts +7 -0
  53. package/shared/contributor.ts +28 -0
  54. package/shared/types/language.ts +8 -3
  55. package/app/components/aside/minor/AsideMinorContributor.vue +0 -5
  56. package/app/components/main/utils/ContentDescription.vue +0 -19
  57. package/app/composables/bitranContent.ts +0 -96
  58. package/app/pages/members.vue +0 -5
  59. package/server/api/aside/minor/path.ts +0 -82
  60. package/shared/bitran/stringContent.ts +0 -6
  61. /package/app/components/main/{utils → content}/ContentDecoration.vue +0 -0
  62. /package/app/components/main/{utils → content}/ContentPopover.vue +0 -0
  63. /package/app/components/main/{utils → content}/ContentPopovers.vue +0 -0
  64. /package/app/components/main/{utils → content}/ContentReferences.vue +0 -0
  65. /package/app/components/main/{utils → content}/reference/ReferenceGroup.vue +0 -0
  66. /package/app/components/main/{utils → content}/reference/ReferenceItem.vue +0 -0
  67. /package/app/components/main/{utils → content}/reference/ReferenceSource.vue +0 -0
@@ -0,0 +1,24 @@
1
+ <script lang="ts" setup>
2
+ defineProps<{ description: string; html?: boolean }>();
3
+ const pretty = useFormatText();
4
+ </script>
5
+
6
+ <template>
7
+ <section
8
+ v-if="html"
9
+ :class="$style.contentDescription"
10
+ v-html="description"
11
+ ></section>
12
+ <section v-else :class="$style.contentDescription">
13
+ {{ pretty(description) }}
14
+ </section>
15
+ </template>
16
+
17
+ <style lang="scss" module>
18
+ .contentDescription {
19
+ padding: var(--_pMainY) var(--_pMainX);
20
+ font-weight: 500;
21
+ font-size: 1.1em;
22
+ color: var(--text);
23
+ }
24
+ </style>
@@ -6,12 +6,18 @@ defineProps<{ icon: string; title: string; hint?: string }>();
6
6
 
7
7
  <template>
8
8
  <section :class="$style.contentTitle">
9
- <MyIcon v-if="isMyIcon(icon)" :name="icon as any" :title="hint" />
9
+ <MyIcon
10
+ v-if="isMyIcon(icon)"
11
+ :name="icon as any"
12
+ :title="hint"
13
+ :class="{ [$style.hasHint]: !!hint }"
14
+ />
10
15
  <MyRuntimeIcon
11
16
  v-else
12
17
  name="content-title-icon"
13
18
  :svg="icon"
14
19
  :title="hint"
20
+ :class="{ [$style.hasHint]: !!hint }"
15
21
  />
16
22
  <h1>{{ title }}</h1>
17
23
  </section>
@@ -24,7 +30,7 @@ defineProps<{ icon: string; title: string; hint?: string }>();
24
30
  display: flex;
25
31
  align-items: center;
26
32
  gap: var(--gap);
27
- padding: 0 var(--_pMainX);
33
+ padding: var(--_pMainY) var(--_pMainX);
28
34
 
29
35
  [my-icon] {
30
36
  flex-shrink: 0;
@@ -32,6 +38,9 @@ defineProps<{ icon: string; title: string; hint?: string }>();
32
38
  color: var(--textMuted);
33
39
  position: relative;
34
40
  top: 1px;
41
+ }
42
+
43
+ .hasHint {
35
44
  cursor: help;
36
45
  }
37
46
 
@@ -0,0 +1,28 @@
1
+ <script lang="ts" setup>
2
+ import type { MyIconName } from '#my-icons';
3
+ import type { BreadcrumbItem } from '@shared/breadcrumb';
4
+ import type { Context } from '@shared/content/context';
5
+
6
+ defineProps<{ context: Context }>();
7
+ </script>
8
+
9
+ <template>
10
+ <MainBreadcrumb
11
+ v-if="context?.length > 1"
12
+ :items="
13
+ context.reduce((acc, item, i, arr) => {
14
+ if (item.hidden || i === arr.length - 1) {
15
+ return acc;
16
+ }
17
+
18
+ acc.push({
19
+ title: item.title,
20
+ icon: item.icon as MyIconName,
21
+ link: item.href,
22
+ });
23
+
24
+ return acc;
25
+ }, [] as BreadcrumbItem[])
26
+ "
27
+ />
28
+ </template>
@@ -23,7 +23,7 @@
23
23
  content: '';
24
24
  }
25
25
 
26
- &:nth-of-type(even) {
26
+ &:nth-of-type(odd) {
27
27
  &::before {
28
28
  @include shade;
29
29
  }
@@ -32,7 +32,7 @@
32
32
  }
33
33
  }
34
34
 
35
- &:nth-of-type(odd) {
35
+ &:nth-of-type(even) {
36
36
  &::before {
37
37
  @include simple;
38
38
  }
@@ -1,22 +1,20 @@
1
1
  <script lang="ts" setup>
2
- import { NO_ALIASES, type TopicPart } from '@erudit-js/cog/schema';
2
+ import { type BitranLocation, type TopicPart } from '@erudit-js/cog/schema';
3
3
 
4
4
  import eruditConfig from '#erudit/config';
5
5
 
6
- import { type ContentTopicData } from '@erudit/shared/content/data/type/topic';
6
+ import { type ContentTopicData } from '@shared/content/data/type/topic';
7
+ import { TOPIC_PART_ICON } from '@shared/icons';
7
8
  import { topicLocation } from '@app/scripts/aside/minor/topic';
8
9
 
9
- import ContentDecoration from '../utils/ContentDecoration.vue';
10
- import ContentTitle from '../utils/ContentTitle.vue';
11
- import ContentDescription from '../utils/ContentDescription.vue';
10
+ import ContentBreadcrumb from '@app/components/main/content/ContentBreadcrumb.vue';
11
+ import ContentDecoration from '@app/components/main/content/ContentDecoration.vue';
12
+ import ContentPopovers from '@app/components/main/content/ContentPopovers.vue';
13
+ import ContentReferences from '@app/components/main/content/ContentReferences.vue';
14
+ import ContentSection from '@app/components/main/content/ContentSection.vue';
12
15
  import TopicPartSwitch from './TopicPartSwitch.vue';
13
- import Breadcrumb from '../utils/Breadcrumb.vue';
14
- import ContentPopovers from '../utils/ContentPopovers.vue';
15
- import ContentReferences from '../utils/ContentReferences.vue';
16
- import ContentSection from '../utils/ContentSection.vue';
17
- import { TOPIC_PART_ICON } from '@erudit/shared/icons';
18
16
 
19
- const location = useBitranLocation();
17
+ const location = useBitranLocation() as Ref<BitranLocation>;
20
18
  const topicPart = computed(() => location.value?.type as TopicPart);
21
19
 
22
20
  const topicData = await useContentData<ContentTopicData>();
@@ -24,8 +22,6 @@ await useContentPage(topicData);
24
22
 
25
23
  const phrase = await usePhrases('article', 'summary', 'practice');
26
24
 
27
- const content = await useBitranContent(location);
28
-
29
25
  onMounted(() => {
30
26
  watchEffect(() => {
31
27
  // Telling live toc that content is mounted
@@ -40,12 +36,9 @@ onMounted(() => {
40
36
  :decoration="topicData.generic.decoration"
41
37
  />
42
38
 
43
- <Breadcrumb
44
- v-if="topicData.generic.context?.length > 1"
45
- :context="topicData.generic.context"
46
- />
39
+ <ContentBreadcrumb :context="topicData.generic.context" />
47
40
 
48
- <ContentTitle
41
+ <MainTitle
49
42
  :title="
50
43
  topicData.generic?.title ||
51
44
  topicData.generic.contentId.split('/').pop()!
@@ -54,7 +47,7 @@ onMounted(() => {
54
47
  :hint="phrase[location!.type as TopicPart]"
55
48
  />
56
49
 
57
- <ContentDescription
50
+ <MainDescription
58
51
  v-if="topicData.generic?.description"
59
52
  :description="topicData.generic?.description"
60
53
  />
@@ -68,7 +61,9 @@ onMounted(() => {
68
61
 
69
62
  <div style="clear: both"></div>
70
63
 
71
- <BitranContent :content :context="{ location, aliases: NO_ALIASES() }" />
64
+ <ContentSection>
65
+ <MainBitranContent :location />
66
+ </ContentSection>
72
67
 
73
68
  <ContentSection v-if="topicData.generic.references">
74
69
  <ContentReferences :references="topicData.generic.references" />
@@ -33,17 +33,23 @@ const Link = defineNuxtLink({ prefetch: false });
33
33
  .topicPartSwitch {
34
34
  --height: 50px;
35
35
 
36
+ position: relative;
37
+ top: 65px;
38
+
36
39
  display: flex;
37
40
  align-items: end;
38
41
  justify-content: center;
39
42
  gap: var(--gapBig);
40
43
  margin: var(--_pMainY) 0;
41
- margin-top: var(--gapBig);
44
+ margin-top: -30px;
42
45
 
43
46
  width: 100%;
44
47
  height: var(--height);
45
- background: linear-gradient(to bottom, transparent, var(--bgAside));
46
- border-bottom: 2px solid var(--border);
48
+ border-bottom: 2px solid transparent;
49
+
50
+ @include bp.below('mobile') {
51
+ top: 56px;
52
+ }
47
53
  }
48
54
 
49
55
  .partButton {
@@ -5,9 +5,6 @@ import type { PreviewDataUnique } from '@app/scripts/preview/data/unique';
5
5
  import type { PreviewDisplayProps } from '@app/scripts/preview/display';
6
6
 
7
7
  const { data } = defineProps<PreviewDisplayProps<PreviewDataUnique>>();
8
-
9
- const bitranTranspiler = await useBitranTranspiler();
10
- const root = await bitranTranspiler.parser.parse(data.bitran.content.biCode);
11
8
  </script>
12
9
 
13
10
  <template>
@@ -15,16 +12,10 @@ const root = await bitranTranspiler.parser.parse(data.bitran.content.biCode);
15
12
  <div
16
13
  :class="[
17
14
  $style.bitranPreviewContent,
18
- data.productName === headingName && $style.heading,
15
+ data.elementName === headingName && $style.heading,
19
16
  ]"
20
17
  >
21
- <BitranContent
22
- :content="{
23
- root,
24
- renderDataStorage: data.bitran.content.renderDataStorage,
25
- }"
26
- :context="data.bitran.context"
27
- />
18
+ <BitranContent :rawContent="data.rawBitranContent" />
28
19
  </div>
29
20
  </PreviewDisplay>
30
21
  </template>
@@ -2,7 +2,7 @@ import eruditConfig from '#erudit/config';
2
2
 
3
3
  export function adsAllowed() {
4
4
  if (typeof eruditConfig?.debug?.ads === 'undefined') {
5
- return false;
5
+ return import.meta.dev ? false : true;
6
6
  } else if (!Boolean(eruditConfig?.debug?.ads)) {
7
7
  return false;
8
8
  }
@@ -44,8 +44,9 @@ export function useMajorPane() {
44
44
  const route = useRoute();
45
45
 
46
46
  const activePane = useState<MajorPaneKey>('major-pane', () => {
47
- switch (route.path) {
48
- case '/members':
47
+ switch (true) {
48
+ case route.path.startsWith('/contributors'):
49
+ case route.path.startsWith('/contributor/'):
49
50
  return 'pages';
50
51
  default:
51
52
  return 'index';
@@ -9,7 +9,7 @@ interface LanguagePayload {
9
9
  strFunctions: Record<string, string>;
10
10
  }
11
11
 
12
- const functions: Record<string, Function> = {};
12
+ let functions: Record<string, Function>;
13
13
  const functionPhrases: Record<string, Function> = {};
14
14
 
15
15
  const phraseApiRoute = (phraseId: EruditPhraseId) =>
@@ -42,9 +42,14 @@ export function usePhrases<T extends EruditPhraseId[]>(
42
42
 
43
43
  if (strPhrase.startsWith('~!~FUNC~!~')) {
44
44
  const strFunction = strPhrase.replace('~!~FUNC~!~', '');
45
- functionPhrases[phraseId] ||= new Function(strFunction).bind(
46
- functions,
47
- )();
45
+ functionPhrases[phraseId] ||= new Function(
46
+ 'funcs',
47
+ `
48
+ with(funcs) {
49
+ ${strFunction}
50
+ }
51
+ `,
52
+ )(functions);
48
53
  phraseCaller[phraseId] = functionPhrases[phraseId];
49
54
  } else {
50
55
  phraseCaller[phraseId] = strPhrase;
@@ -56,10 +61,17 @@ export function usePhrases<T extends EruditPhraseId[]>(
56
61
  }
57
62
 
58
63
  async function prepareFunctions(payload: LanguagePayload) {
59
- if (payload?.strFunctions) return;
64
+ payload.strFunctions ||= await $fetch(functionsApiRoute, {
65
+ responseType: 'json',
66
+ });
60
67
 
61
- payload.strFunctions = await $fetch(functionsApiRoute);
62
-
63
- for (const [funcName, strFunc] of Object.entries(payload.strFunctions))
64
- functions[funcName] = new Function(strFunc)();
68
+ functions ||= (() => {
69
+ const _functions: Record<string, Function> = {};
70
+ for (const [funcName, strFunc] of Object.entries(
71
+ payload.strFunctions,
72
+ )) {
73
+ _functions[funcName] = new Function(strFunc)();
74
+ }
75
+ return _functions;
76
+ })();
65
77
  }
@@ -2,11 +2,9 @@
2
2
  import type { ContentBookData } from '@erudit/shared/content/data/type/book';
3
3
  import type { MyIconName } from '#my-icons';
4
4
 
5
- import ContentDecoration from '@app/components/main/utils/ContentDecoration.vue';
6
- import Breadcrumb from '@app/components/main/utils/Breadcrumb.vue';
7
- import ContentTitle from '@app/components/main/utils/ContentTitle.vue';
8
- import ContentDescription from '@app/components/main/utils/ContentDescription.vue';
9
- import ContentPopovers from '@app/components/main/utils/ContentPopovers.vue';
5
+ import ContentBreadcrumb from '@app/components/main/content/ContentBreadcrumb.vue';
6
+ import ContentDecoration from '@app/components/main/content/ContentDecoration.vue';
7
+ import ContentPopovers from '@app/components/main/content/ContentPopovers.vue';
10
8
 
11
9
  const bookData = await useContentData<ContentBookData>();
12
10
  await useContentPage(bookData);
@@ -20,12 +18,9 @@ const phrase = await usePhrases('book');
20
18
  :decoration="bookData.generic.decoration"
21
19
  />
22
20
 
23
- <Breadcrumb
24
- v-if="bookData.generic.context?.length > 1"
25
- :context="bookData.generic.context"
26
- />
21
+ <ContentBreadcrumb :context="bookData.generic.context" />
27
22
 
28
- <ContentTitle
23
+ <MainTitle
29
24
  :title="
30
25
  bookData.generic?.title ||
31
26
  bookData.generic.contentId.split('/').pop()!
@@ -34,7 +29,7 @@ const phrase = await usePhrases('book');
34
29
  :hint="phrase.book"
35
30
  />
36
31
 
37
- <ContentDescription
32
+ <MainDescription
38
33
  v-if="bookData.generic?.description"
39
34
  :description="bookData.generic?.description"
40
35
  />
@@ -0,0 +1,225 @@
1
+ <script lang="ts" setup>
2
+ import eruditConfig from '#erudit/config';
3
+ import { type PageContributor } from '@shared/contributor';
4
+ import ContentSection from '@app/components/main/content/ContentSection.vue';
5
+
6
+ const route = useRoute();
7
+ const nuxtApp = useNuxtApp();
8
+ const contributor = shallowRef<PageContributor>(null as any);
9
+ const contributorColor = ref<string>('');
10
+ const resolved = computed(() => {
11
+ const title = (() => {
12
+ return contributor.value.displayName || contributor.value.contributorId;
13
+ })();
14
+
15
+ return {
16
+ title,
17
+ };
18
+ });
19
+
20
+ let requestCounter = 0;
21
+
22
+ function getPayloadCache() {
23
+ const payloadKey = 'contributor';
24
+ return (nuxtApp.static.data[payloadKey] ||= nuxtApp.payload.data[
25
+ payloadKey
26
+ ] ||=
27
+ {});
28
+ }
29
+
30
+ async function fetchContributorData(contributorId: string, requestId: number) {
31
+ const payloadCache = getPayloadCache();
32
+
33
+ if (!payloadCache[contributorId]) {
34
+ try {
35
+ const data = await $fetch(`/api/contributor/page/${contributorId}`);
36
+
37
+ if (requestId === requestCounter) {
38
+ payloadCache[contributorId] = data;
39
+ }
40
+ } catch (error) {
41
+ console.error(
42
+ `Error fetching contributor ${contributorId}:`,
43
+ error,
44
+ );
45
+ }
46
+ }
47
+
48
+ contributor.value = payloadCache[contributorId];
49
+ contributorColor.value = stringColor(contributor.value.contributorId);
50
+ }
51
+
52
+ await fetchContributorData(
53
+ route.params.contributorId as string,
54
+ ++requestCounter,
55
+ );
56
+
57
+ const phrase = await usePhrases(
58
+ 'contributors',
59
+ 'contributor',
60
+ 'contributor_description',
61
+ 'editor',
62
+ );
63
+
64
+ useHead({
65
+ title:
66
+ resolved.value.title +
67
+ ' | ' +
68
+ (contributor.value.isEditor ? phrase.editor : phrase.contributor) +
69
+ ' - ' +
70
+ (eruditConfig.seo?.title || eruditConfig.site?.title),
71
+ });
72
+
73
+ useSeoMeta({
74
+ ogTitle:
75
+ resolved.value.title +
76
+ ' | ' +
77
+ (contributor.value.isEditor ? phrase.editor : phrase.contributor) +
78
+ ' - ' +
79
+ (eruditConfig.seo?.title || eruditConfig.site?.title),
80
+ description: phrase.contributor_description(resolved.value.title),
81
+ });
82
+ </script>
83
+
84
+ <template>
85
+ <MainBreadcrumb
86
+ :items="[
87
+ {
88
+ title: phrase.contributors,
89
+ icon: 'users',
90
+ link: '/contributors',
91
+ },
92
+ ]"
93
+ />
94
+ <header
95
+ :class="$style.header"
96
+ :style="{ ['--contributorColor']: contributorColor }"
97
+ >
98
+ <div style="position: relative">
99
+ <ContributorAvatar
100
+ :class="$style.avatar"
101
+ :contributorId="contributor.contributorId"
102
+ :avatar="contributor.avatar"
103
+ />
104
+ <MyIcon
105
+ v-if="contributor.isEditor"
106
+ name="graduation"
107
+ :class="$style.editorIcon"
108
+ :title="phrase.editor"
109
+ />
110
+ </div>
111
+ <h1 :class="$style.name">
112
+ {{ resolved.title }}
113
+ </h1>
114
+ <div v-if="contributor.slogan" :class="$style.slogan">
115
+ {{ contributor.slogan }}
116
+ </div>
117
+ <div v-if="contributor.links" :class="$style.links">
118
+ <div v-for="(link, label) of contributor.links">
119
+ <a
120
+ :href="link"
121
+ target="_blank"
122
+ rel="noopener noreferrer"
123
+ :class="$style.link"
124
+ >
125
+ {{ label }}
126
+ </a>
127
+ </div>
128
+ </div>
129
+ </header>
130
+ <ContentSection v-if="contributor.hasDescription">
131
+ <MainBitranContent
132
+ :location="{ type: 'contributor', path: contributor.contributorId }"
133
+ />
134
+ </ContentSection>
135
+ </template>
136
+
137
+ <style lang="scss" module>
138
+ @use '$/def/bp';
139
+
140
+ .header {
141
+ display: flex;
142
+ flex-direction: column;
143
+ align-items: center;
144
+ gap: var(--gap);
145
+ padding-top: 20px;
146
+ padding-left: var(--gapBig);
147
+ padding-right: var(--gapBig);
148
+
149
+ .avatar {
150
+ --_avatarSize: 110px;
151
+ border: 2px solid var(--bgMain);
152
+ outline: 2px solid var(--contributorColor);
153
+ box-shadow: 0 0 100px 100px
154
+ color-mix(in srgb, var(--contributorColor), transparent 90%);
155
+ }
156
+
157
+ .editorIcon {
158
+ position: absolute;
159
+ right: 50%;
160
+ transform: translate(50%, -50%);
161
+ color: color-mix(in srgb, var(--text), var(--contributorColor) 50%);
162
+ font-size: 16px;
163
+ background: var(--bgMain);
164
+ padding: 4px;
165
+ border-radius: 50%;
166
+ outline: 2px solid var(--contributorColor);
167
+ cursor: help;
168
+ }
169
+
170
+ .name,
171
+ .slogan,
172
+ .links {
173
+ text-align: center;
174
+ }
175
+
176
+ .name {
177
+ padding-top: 6px;
178
+ line-height: 1.2;
179
+ font-size: 2em;
180
+ color: color-mix(in srgb, var(--textDeep), var(--contributorColor) 15%);
181
+ text-shadow: 2px 2px
182
+ color-mix(in srgb, var(--contributorColor), transparent 80%);
183
+
184
+ @include bp.below('mobile') {
185
+ font-size: 1.8em;
186
+ }
187
+ }
188
+
189
+ .slogan {
190
+ font-size: 1.2em;
191
+ font-weight: 600;
192
+ padding-bottom: 8px;
193
+ color: var(--textMuted);
194
+ }
195
+
196
+ .links {
197
+ display: flex;
198
+ gap: var(--gap);
199
+ justify-content: center;
200
+ flex-wrap: wrap;
201
+
202
+ .link {
203
+ line-height: 2;
204
+ font-size: 1em;
205
+ font-weight: 600;
206
+ color: color-mix(in srgb, var(--text), var(--contributorColor) 20%);
207
+ border: 1.5px solid
208
+ color-mix(in srgb, var(--contributorColor), transparent 50%);
209
+ border-radius: 3px;
210
+ padding: 4px 10px;
211
+ text-decoration: none;
212
+
213
+ @include transition(background);
214
+
215
+ &:hover {
216
+ background: color-mix(
217
+ in srgb,
218
+ var(--contributorColor),
219
+ transparent 90%
220
+ );
221
+ }
222
+ }
223
+ }
224
+ }
225
+ </style>