erudit 4.2.0-dev.1 → 4.3.0-dev.1
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.
- package/app/components/Prose.vue +2 -0
- package/app/components/aside/major/contentNav/items/ContentNavTopic.vue +12 -1
- package/app/components/aside/major/search/SearchResult.vue +16 -2
- package/app/components/aside/minor/contributor/AsideMinorContributor.vue +7 -1
- package/app/components/aside/minor/news/AsideMinorNews.vue +1 -1
- package/app/components/main/MainStickyHeader.vue +5 -2
- package/app/components/main/MainStickyHeaderPreamble.vue +5 -2
- package/app/components/main/MainTopicPartPage.vue +3 -2
- package/app/components/main/MainTopicPartSwitch.vue +18 -7
- package/app/components/main/connections/Deps.vue +1 -4
- package/app/components/main/connections/MainConnections.vue +9 -3
- package/app/components/main/contentStats/ItemLastChanged.vue +3 -32
- package/app/components/main/contentStats/MainContentStats.vue +3 -4
- package/app/components/preview/Preview.vue +8 -6
- package/app/components/preview/PreviewScreen.vue +9 -7
- package/app/components/preview/screen/Unique.vue +3 -2
- package/app/composables/ads.ts +1 -1
- package/app/composables/analytics.ts +1 -1
- package/app/composables/lastChanged.ts +38 -5
- package/app/composables/og.ts +5 -5
- package/app/composables/phrases.ts +2 -0
- package/app/composables/scrollUp.ts +3 -1
- package/app/pages/book/[...bookId].vue +3 -2
- package/app/pages/group/[...groupId].vue +3 -2
- package/app/pages/page/[...pageId].vue +4 -2
- package/app/plugins/appSetup/config.ts +1 -0
- package/app/plugins/appSetup/global.ts +3 -0
- package/app/plugins/appSetup/index.ts +4 -1
- package/app/plugins/devReload.client.ts +13 -0
- package/app/router.options.ts +17 -3
- package/app/styles/main.css +2 -2
- package/modules/erudit/dependencies.ts +16 -0
- package/modules/erudit/index.ts +8 -1
- package/modules/erudit/setup/autoImports.ts +143 -0
- package/modules/erudit/setup/elements/globalTemplate.ts +10 -2
- package/modules/erudit/setup/elements/setup.ts +8 -14
- package/modules/erudit/setup/elements/tagsTable.ts +2 -18
- package/modules/erudit/setup/fullRestart.ts +5 -3
- package/modules/erudit/setup/namesTable.ts +33 -0
- package/modules/erudit/setup/problemChecks/setup.ts +60 -0
- package/modules/erudit/setup/problemChecks/shared.ts +4 -0
- package/modules/erudit/setup/problemChecks/template.ts +33 -0
- package/modules/erudit/setup/runtimeConfig.ts +12 -7
- package/nuxt.config.ts +14 -6
- package/package.json +5 -6
- package/server/api/problemScript/[...problemScriptPath].ts +245 -52
- package/server/erudit/build.ts +10 -4
- package/server/erudit/content/global/build.ts +43 -3
- package/server/erudit/content/nav/build.ts +5 -5
- package/server/erudit/content/nav/front.ts +1 -0
- package/server/erudit/content/repository/deps.ts +45 -6
- package/server/erudit/content/resolve/index.ts +3 -3
- package/server/erudit/content/resolve/utils/contentError.ts +2 -2
- package/server/erudit/content/resolve/utils/insertContentResolved.ts +29 -27
- package/server/erudit/global.ts +5 -1
- package/server/erudit/importer.ts +69 -0
- package/server/erudit/index.ts +2 -2
- package/server/erudit/logger.ts +18 -10
- package/server/erudit/reloadSignal.ts +14 -0
- package/server/routes/_reload.ts +27 -0
- package/shared/types/contentConnections.ts +1 -0
- package/shared/types/frontContentNav.ts +2 -0
package/app/components/Prose.vue
CHANGED
|
@@ -4,6 +4,7 @@ import type { EruditMode } from '@erudit-js/core/mode';
|
|
|
4
4
|
import { Prose, type ProseContext } from '@erudit-js/prose/app';
|
|
5
5
|
|
|
6
6
|
import { EruditLink, MaybeMyIcon, TransitionFade } from '#components';
|
|
7
|
+
import { problemCheckers } from '#erudit/checks';
|
|
7
8
|
|
|
8
9
|
const { element, storage, useHashUrl, setHtmlIds } = defineProps<{
|
|
9
10
|
element: ProseElement;
|
|
@@ -35,6 +36,7 @@ const context: ProseContext = {
|
|
|
35
36
|
baseUrl: runtimeConfig.app.baseURL,
|
|
36
37
|
hashUrl,
|
|
37
38
|
eruditIcons: ICONS,
|
|
39
|
+
problemCheckers,
|
|
38
40
|
EruditIcon: MaybeMyIcon,
|
|
39
41
|
EruditTransition: TransitionFade,
|
|
40
42
|
EruditLink,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
import { topicParts } from '@erudit-js/core/content/topic';
|
|
3
|
+
|
|
2
4
|
import ItemTemplate from './ItemTemplate.vue';
|
|
3
5
|
|
|
4
6
|
const { navItem } = defineProps<{ navItem: FrontContentNavTopic }>();
|
|
@@ -7,11 +9,20 @@ const { shortContentId } = useContentId();
|
|
|
7
9
|
const active = computed(() => {
|
|
8
10
|
return navItem.shortId === shortContentId.value;
|
|
9
11
|
});
|
|
12
|
+
|
|
13
|
+
const topicIcon = computed(() => {
|
|
14
|
+
// Article > Summary > Practice
|
|
15
|
+
if (navItem.parts?.length) {
|
|
16
|
+
for (const part of topicParts) {
|
|
17
|
+
if (navItem.parts.includes(part)) return ICONS[part];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
10
21
|
</script>
|
|
11
22
|
|
|
12
23
|
<template>
|
|
13
24
|
<ItemTemplate
|
|
14
|
-
icon="
|
|
25
|
+
:icon="topicIcon"
|
|
15
26
|
:navItem
|
|
16
27
|
:state="active ? 'active' : undefined"
|
|
17
28
|
/>
|
|
@@ -95,8 +95,22 @@ const secondaryTitle = computed(() =>
|
|
|
95
95
|
);
|
|
96
96
|
|
|
97
97
|
async function searchResultClick() {
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const linkUrl = new URL(result.link, 'http://x');
|
|
99
|
+
const elementId = linkUrl.searchParams.get('element');
|
|
100
|
+
|
|
101
|
+
if (elementId) {
|
|
102
|
+
// Clear the element param first so watchers re-trigger scroll/highlight.
|
|
103
|
+
linkUrl.searchParams.delete('element');
|
|
104
|
+
const withoutElement =
|
|
105
|
+
linkUrl.pathname + (linkUrl.search || '') + (linkUrl.hash || '');
|
|
106
|
+
await router.replace({
|
|
107
|
+
...route,
|
|
108
|
+
query: { ...route.query, element: undefined },
|
|
109
|
+
});
|
|
110
|
+
await navigateTo(withoutElement);
|
|
111
|
+
await nextTick();
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
await navigateTo(result.link);
|
|
101
115
|
}
|
|
102
116
|
</script>
|
|
@@ -7,6 +7,12 @@ const contributions = (asideMinorState.value as AsideMinorContributor)
|
|
|
7
7
|
.contributions;
|
|
8
8
|
|
|
9
9
|
const phrase = await usePhrases('contribution', 'no_contribution');
|
|
10
|
+
|
|
11
|
+
const totalCount = contributions?.reduce((sum, item) => {
|
|
12
|
+
if (item.type === 'book') return sum + item.items.length;
|
|
13
|
+
if (item.type === 'topic' || item.type === 'page') return sum + 1;
|
|
14
|
+
return sum;
|
|
15
|
+
}, 0);
|
|
10
16
|
</script>
|
|
11
17
|
|
|
12
18
|
<template>
|
|
@@ -15,7 +21,7 @@ const phrase = await usePhrases('contribution', 'no_contribution');
|
|
|
15
21
|
<AsideMinorPlainHeader
|
|
16
22
|
icon="draw"
|
|
17
23
|
:title="phrase.contribution"
|
|
18
|
-
:count="
|
|
24
|
+
:count="totalCount"
|
|
19
25
|
/>
|
|
20
26
|
<ScrollHolder v-if="contributions" class="flex-1">
|
|
21
27
|
<TreeContainer>
|
|
@@ -98,7 +98,7 @@ const phrase = await usePhrases('news', 'no_news', 'show_more');
|
|
|
98
98
|
<div class="flex h-full w-full flex-col">
|
|
99
99
|
<AsideMinorPlainHeader
|
|
100
100
|
icon="bell"
|
|
101
|
-
:title="phrase
|
|
101
|
+
:title="phrase.news"
|
|
102
102
|
:count="newsTotal === 0 ? undefined : newsTotal"
|
|
103
103
|
/>
|
|
104
104
|
<section v-if="newsItems.length === 0">
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
const { mainContent } = defineProps<{
|
|
2
|
+
const { mainContent, lastChangedDate } = defineProps<{
|
|
3
|
+
mainContent: MainContent;
|
|
4
|
+
lastChangedDate?: Date;
|
|
5
|
+
}>();
|
|
3
6
|
|
|
4
7
|
const hasPreamble = computed(() => {
|
|
5
8
|
const hasBreadcrumbs = mainContent.breadcrumbs.length > 0;
|
|
@@ -136,7 +139,7 @@ onMounted(() => {
|
|
|
136
139
|
class="text-main nice-scrollbars max-h-[70dvh] overflow-auto"
|
|
137
140
|
>
|
|
138
141
|
<Suspense>
|
|
139
|
-
<MainStickyHeaderPreamble :mainContent />
|
|
142
|
+
<MainStickyHeaderPreamble :mainContent :lastChangedDate />
|
|
140
143
|
</Suspense>
|
|
141
144
|
</div>
|
|
142
145
|
</div>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
const { mainContent } = defineProps<{
|
|
2
|
+
const { mainContent, lastChangedDate } = defineProps<{
|
|
3
|
+
mainContent: MainContent;
|
|
4
|
+
lastChangedDate?: Date;
|
|
5
|
+
}>();
|
|
3
6
|
</script>
|
|
4
7
|
|
|
5
8
|
<template>
|
|
@@ -20,7 +23,7 @@ const { mainContent } = defineProps<{ mainContent: MainContent }>();
|
|
|
20
23
|
<MainContentStats
|
|
21
24
|
mode="single"
|
|
22
25
|
:stats="mainContent.stats"
|
|
23
|
-
:
|
|
26
|
+
:lastChangedDate
|
|
24
27
|
/>
|
|
25
28
|
<div class="h-main-half"></div>
|
|
26
29
|
</div>
|
|
@@ -24,6 +24,7 @@ const phrase = await usePhrases(
|
|
|
24
24
|
'summary_seo_description',
|
|
25
25
|
'practice_seo_description',
|
|
26
26
|
);
|
|
27
|
+
const lastChangedDate = useLastChanged(() => mainContent.contentRelativePath);
|
|
27
28
|
|
|
28
29
|
await useContentSeo({
|
|
29
30
|
title: mainContent.title,
|
|
@@ -62,12 +63,12 @@ await useContentSeo({
|
|
|
62
63
|
<MainContentStats
|
|
63
64
|
mode="single"
|
|
64
65
|
:stats="mainContent.stats"
|
|
65
|
-
:
|
|
66
|
+
:lastChangedDate
|
|
66
67
|
/>
|
|
67
68
|
<div class="h-main-half"></div>
|
|
68
69
|
<MainQuoteLoader />
|
|
69
70
|
<div class="h-main-half"></div>
|
|
70
|
-
<MainStickyHeader :mainContent />
|
|
71
|
+
<MainStickyHeader :mainContent :lastChangedDate />
|
|
71
72
|
</MainSectionPreamble>
|
|
72
73
|
<MainSection>
|
|
73
74
|
<template #header>
|
|
@@ -43,17 +43,28 @@ const data: Record<TopicPart, TopicPartSwitchData> = {
|
|
|
43
43
|
:to="partData.state !== 'missing' ? partData.link : undefined"
|
|
44
44
|
:class="[
|
|
45
45
|
`micro:[--switchHeight:50px] micro:gap-small px-small micro:px-normal
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
relative -bottom-[2px] flex h-(--switchHeight) items-center gap-1
|
|
47
|
+
overflow-clip rounded rounded-b-none transition-[color]`,
|
|
48
48
|
partData.state === 'missing' &&
|
|
49
|
-
'text-text-disabled/75
|
|
50
|
-
partData.state === 'active' &&
|
|
51
|
-
'text-brand bg-bg-main border-b-transparent',
|
|
49
|
+
'text-text-disabled/75 cursor-not-allowed',
|
|
50
|
+
partData.state === 'active' && 'text-brand bg-bg-main',
|
|
52
51
|
partData.state === 'inactive' &&
|
|
53
|
-
|
|
54
|
-
border-b-[color-mix(in_srgb,var(--color-border),var(--color-bg-main)_65%)]`,
|
|
52
|
+
'text-text-muted hocus:text-text bg-bg-aside',
|
|
55
53
|
]"
|
|
56
54
|
>
|
|
55
|
+
<div
|
|
56
|
+
:class="[
|
|
57
|
+
'border-border absolute top-0 left-0 h-full w-full rounded-t border-2',
|
|
58
|
+
partData.state === 'active' && 'border-b-transparent',
|
|
59
|
+
partData.state === 'inactive' &&
|
|
60
|
+
'border-b-[color-mix(in_srgb,var(--color-border),var(--color-bg-main)_65%)]',
|
|
61
|
+
]"
|
|
62
|
+
></div>
|
|
63
|
+
<div
|
|
64
|
+
v-if="partData.state === 'active'"
|
|
65
|
+
class="from-brand/80 via-brand/10 absolute top-0 left-0 z-0 h-full
|
|
66
|
+
w-full bg-linear-to-b via-5% to-transparent"
|
|
67
|
+
></div>
|
|
57
68
|
<MyIcon :name="ICONS[partKey]" class="max-micro:mx-1 text-[1.2em]" />
|
|
58
69
|
<span
|
|
59
70
|
:class="[
|
|
@@ -25,10 +25,7 @@ defineProps<{ type: 'dependency' | 'dependent'; deps: ContentDep[] }>();
|
|
|
25
25
|
>
|
|
26
26
|
{{ formatText(dep.reason) }}
|
|
27
27
|
</div>
|
|
28
|
-
<div
|
|
29
|
-
v-if="dep.type === 'auto' && dep.uniques?.length"
|
|
30
|
-
class="mt-small flex flex-col gap-0.5"
|
|
31
|
-
>
|
|
28
|
+
<div v-if="dep.uniques?.length" class="mt-small flex flex-col gap-0.5">
|
|
32
29
|
<DepUnique
|
|
33
30
|
v-for="unique in dep.uniques"
|
|
34
31
|
:type
|
|
@@ -4,7 +4,11 @@ import Externals from './Externals.vue';
|
|
|
4
4
|
|
|
5
5
|
const { connections } = defineProps<{ connections?: ContentConnections }>();
|
|
6
6
|
|
|
7
|
-
const phrase = await usePhrases(
|
|
7
|
+
const phrase = await usePhrases(
|
|
8
|
+
'connections',
|
|
9
|
+
'externals_own',
|
|
10
|
+
'externals_from',
|
|
11
|
+
);
|
|
8
12
|
|
|
9
13
|
const currentType = ref<keyof ContentConnections | undefined>(
|
|
10
14
|
'hardDependencies',
|
|
@@ -82,10 +86,12 @@ const parentExternalsCount = computed(() => {
|
|
|
82
86
|
<template v-if="currentType && connections[currentType]">
|
|
83
87
|
<Deps
|
|
84
88
|
v-if="currentType !== 'externals'"
|
|
85
|
-
:type="currentType === '
|
|
89
|
+
:type="currentType === 'dependents' ? 'dependent' : 'dependency'"
|
|
86
90
|
:deps="connections[currentType]!"
|
|
87
91
|
/>
|
|
88
|
-
<
|
|
92
|
+
<Suspense v-else>
|
|
93
|
+
<Externals :externals="connections[currentType]!" />
|
|
94
|
+
</Suspense>
|
|
89
95
|
</template>
|
|
90
96
|
</section>
|
|
91
97
|
</template>
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
|
|
3
|
-
ReturnType<typeof useLastChangedSource>['value']
|
|
4
|
-
>;
|
|
5
|
-
|
|
6
|
-
const { source } = defineProps<{ source: LastChangedSource }>();
|
|
7
|
-
|
|
8
|
-
const date = ref<Date | null>(null);
|
|
2
|
+
const { date } = defineProps<{ date: Date }>();
|
|
9
3
|
|
|
10
4
|
const dateOptions: Intl.DateTimeFormatOptions = {
|
|
11
5
|
year: 'numeric',
|
|
@@ -14,44 +8,21 @@ const dateOptions: Intl.DateTimeFormatOptions = {
|
|
|
14
8
|
};
|
|
15
9
|
|
|
16
10
|
const isWithinThreeMonths = computed(() => {
|
|
17
|
-
if (!date.value) return false;
|
|
18
11
|
const now = new Date();
|
|
19
12
|
const threeMonthsAgo = new Date(now);
|
|
20
13
|
threeMonthsAgo.setMonth(now.getMonth() - 3);
|
|
21
|
-
return date
|
|
14
|
+
return date >= threeMonthsAgo && date <= now;
|
|
22
15
|
});
|
|
23
16
|
|
|
24
17
|
const formattedTitle = computed(() =>
|
|
25
|
-
date.
|
|
18
|
+
date.toLocaleDateString(undefined, dateOptions),
|
|
26
19
|
);
|
|
27
20
|
|
|
28
21
|
const phrase = await usePhrases('updated');
|
|
29
|
-
|
|
30
|
-
onMounted(async () => {
|
|
31
|
-
if (source.type === 'date') {
|
|
32
|
-
date.value = new Date(source.value);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (source.type === 'github') {
|
|
37
|
-
try {
|
|
38
|
-
const data = await $fetch<any[]>(source.url, {
|
|
39
|
-
query: { path: source.path, per_page: 1 },
|
|
40
|
-
responseType: 'json',
|
|
41
|
-
});
|
|
42
|
-
if (Array.isArray(data) && data[0]?.commit?.committer?.date) {
|
|
43
|
-
date.value = new Date(data[0].commit.committer.date);
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
// silently ignore API errors
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
22
|
</script>
|
|
51
23
|
|
|
52
24
|
<template>
|
|
53
25
|
<div
|
|
54
|
-
v-if="date"
|
|
55
26
|
class="gap-small px-small text-main-sm border-border bg-bg-aside flex
|
|
56
27
|
items-center rounded-xl border py-1"
|
|
57
28
|
>
|
|
@@ -6,16 +6,15 @@ import ItemMaterials from './ItemMaterials.vue';
|
|
|
6
6
|
const props = defineProps<{
|
|
7
7
|
mode: 'single' | 'children';
|
|
8
8
|
stats?: ContentStats;
|
|
9
|
-
|
|
9
|
+
lastChangedDate?: Date;
|
|
10
10
|
}>();
|
|
11
11
|
|
|
12
12
|
const phrase = await usePhrases('stats');
|
|
13
|
-
const lastChangedSource = useLastChangedSource(() => props.contentRelativePath);
|
|
14
13
|
</script>
|
|
15
14
|
|
|
16
15
|
<template>
|
|
17
16
|
<section
|
|
18
|
-
v-if="mode === 'single' && (stats ||
|
|
17
|
+
v-if="mode === 'single' && (stats || lastChangedDate)"
|
|
19
18
|
class="px-main py-main-half"
|
|
20
19
|
>
|
|
21
20
|
<MainSubTitle :title="phrase.stats + ':'" />
|
|
@@ -23,7 +22,6 @@ const lastChangedSource = useLastChangedSource(() => props.contentRelativePath);
|
|
|
23
22
|
class="micro:justify-start gap-small micro:gap-normal flex flex-wrap
|
|
24
23
|
justify-center"
|
|
25
24
|
>
|
|
26
|
-
<ItemLastChanged v-if="lastChangedSource" :source="lastChangedSource" />
|
|
27
25
|
<ItemMaterials
|
|
28
26
|
v-if="stats?.materials"
|
|
29
27
|
:count="stats.materials"
|
|
@@ -36,6 +34,7 @@ const lastChangedSource = useLastChangedSource(() => props.contentRelativePath);
|
|
|
36
34
|
:count
|
|
37
35
|
mode="detailed"
|
|
38
36
|
/>
|
|
37
|
+
<ItemLastChanged v-if="lastChangedDate" :date="lastChangedDate" />
|
|
39
38
|
</div>
|
|
40
39
|
</section>
|
|
41
40
|
<div
|
|
@@ -108,9 +108,10 @@ await usePhrases(
|
|
|
108
108
|
<div
|
|
109
109
|
ref="preview"
|
|
110
110
|
:class="[
|
|
111
|
-
`fixed-main
|
|
112
|
-
pointer-events-auto bottom-0 z-5
|
|
113
|
-
rounded-[25px] rounded-b-none
|
|
111
|
+
`fixed-main micro:max-h-[70dvh] border-border bg-bg-aside from-brand/8
|
|
112
|
+
dark:from-brand/10 to-brand/2 pointer-events-auto bottom-0 z-5
|
|
113
|
+
max-h-[90dvh] touch-auto overflow-hidden rounded-[25px] rounded-b-none
|
|
114
|
+
border-t bg-linear-to-t
|
|
114
115
|
transition-[max-height,height,translate,box-shadow,left,width]`,
|
|
115
116
|
previewState.opened
|
|
116
117
|
? `translate-y-0
|
|
@@ -136,10 +137,11 @@ await usePhrases(
|
|
|
136
137
|
<TransitionFade>
|
|
137
138
|
<div
|
|
138
139
|
v-if="loading"
|
|
139
|
-
class="bg-bg-main
|
|
140
|
-
justify-center
|
|
140
|
+
class="bg-bg-main from-brand/8 dark:from-brand/10 to-brand/2 absolute
|
|
141
|
+
bottom-0 flex h-full w-full items-center justify-center
|
|
142
|
+
bg-linear-to-t"
|
|
141
143
|
>
|
|
142
|
-
<Loading class="text-text-dimmed text-[
|
|
144
|
+
<Loading class="text-text-dimmed text-[65px]" />
|
|
143
145
|
</div>
|
|
144
146
|
</TransitionFade>
|
|
145
147
|
|
|
@@ -18,20 +18,22 @@ const { closePreview, hasPreviousRequest, setPreviousPreview } = usePreview();
|
|
|
18
18
|
<slot></slot>
|
|
19
19
|
</div>
|
|
20
20
|
<div
|
|
21
|
-
class="border-border gap-small micro:gap-normal
|
|
22
|
-
flex h-[54px] shrink-0 items-center border-t
|
|
21
|
+
class="border-brand/20 dark:border-brand/14 gap-small micro:gap-normal
|
|
22
|
+
micro:h-[60px] px-main flex h-[54px] shrink-0 items-center border-t
|
|
23
|
+
[box-shadow:0px_2px_10px_var(--color-border)]
|
|
24
|
+
dark:[box-shadow:0px_2px_10px_var(--color-bg-aside)]"
|
|
23
25
|
>
|
|
24
26
|
<MaybeMyIcon
|
|
25
27
|
:name="icon"
|
|
26
|
-
class="
|
|
28
|
+
class="micro:text-[34px] shrink-0 text-[30px]
|
|
29
|
+
text-[color-mix(in_srgb,var(--color-brand),var(--color-text)_70%)]"
|
|
27
30
|
/>
|
|
28
31
|
<div class="flex flex-1 flex-col justify-center overflow-hidden">
|
|
29
|
-
<
|
|
32
|
+
<FancyBold
|
|
33
|
+
:text="main"
|
|
30
34
|
class="micro:text-sm overflow-hidden text-xs font-bold text-nowrap
|
|
31
35
|
overflow-ellipsis"
|
|
32
|
-
|
|
33
|
-
{{ formatText(main) }}
|
|
34
|
-
</div>
|
|
36
|
+
/>
|
|
35
37
|
<div
|
|
36
38
|
v-if="secondary"
|
|
37
39
|
class="text-text-muted text-tiny micro:text-xs overflow-hidden
|
|
@@ -43,8 +43,9 @@ const secondary = (() => {
|
|
|
43
43
|
/>
|
|
44
44
|
<div
|
|
45
45
|
v-if="previewData.fadeOverlay"
|
|
46
|
-
class="
|
|
47
|
-
|
|
46
|
+
class="pointer-events-none absolute bottom-0 left-0 h-full w-full
|
|
47
|
+
touch-none bg-linear-to-b from-transparent
|
|
48
|
+
to-[color-mix(in_srgb,var(--color-bg-aside),var(--color-brand)_5%)]"
|
|
48
49
|
></div>
|
|
49
50
|
</div>
|
|
50
51
|
</PreviewScreen>
|
package/app/composables/ads.ts
CHANGED
|
@@ -2,21 +2,21 @@ type LastChangedSource =
|
|
|
2
2
|
| { type: 'date'; value: string }
|
|
3
3
|
| { type: 'github'; url: string; path: string };
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
function useLastChangedSource(
|
|
6
6
|
contentRelativePath: MaybeRefOrGetter<string | undefined>,
|
|
7
7
|
) {
|
|
8
|
-
return computed((): LastChangedSource |
|
|
8
|
+
return computed((): LastChangedSource | undefined => {
|
|
9
9
|
const path = toValue(contentRelativePath);
|
|
10
|
-
if (!path) return
|
|
10
|
+
if (!path) return undefined;
|
|
11
11
|
|
|
12
12
|
const debug = ERUDIT.config.debug.fakeApi.lastChanged;
|
|
13
13
|
if (debug === true) return { type: 'date', value: '2024-01-15T12:00:00Z' };
|
|
14
14
|
if (typeof debug === 'string') return { type: 'date', value: debug };
|
|
15
15
|
|
|
16
16
|
const repo = ERUDIT.config.repository;
|
|
17
|
-
if (!repo || repo.type !== 'github') return
|
|
17
|
+
if (!repo || repo.type !== 'github') return undefined;
|
|
18
18
|
const parts = repo.name.split('/');
|
|
19
|
-
if (parts.length !== 2) return
|
|
19
|
+
if (parts.length !== 2) return undefined;
|
|
20
20
|
const [owner, repoName] = parts;
|
|
21
21
|
|
|
22
22
|
return {
|
|
@@ -26,3 +26,36 @@ export function useLastChangedSource(
|
|
|
26
26
|
};
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
export function useLastChanged(
|
|
31
|
+
contentRelativePath: MaybeRefOrGetter<string | undefined>,
|
|
32
|
+
) {
|
|
33
|
+
const source = useLastChangedSource(contentRelativePath);
|
|
34
|
+
const date = ref<Date | undefined>(undefined);
|
|
35
|
+
|
|
36
|
+
onMounted(async () => {
|
|
37
|
+
const s = source.value;
|
|
38
|
+
if (!s) return;
|
|
39
|
+
|
|
40
|
+
if (s.type === 'date') {
|
|
41
|
+
date.value = new Date(s.value);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (s.type === 'github') {
|
|
46
|
+
try {
|
|
47
|
+
const data = await $fetch<any[]>(s.url, {
|
|
48
|
+
query: { path: s.path, per_page: 1 },
|
|
49
|
+
responseType: 'json',
|
|
50
|
+
});
|
|
51
|
+
if (Array.isArray(data) && data[0]?.commit?.committer?.date) {
|
|
52
|
+
date.value = new Date(data[0].commit.committer.date);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// silently ignore API errors
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return date;
|
|
61
|
+
}
|
package/app/composables/og.ts
CHANGED
|
@@ -144,8 +144,6 @@ export async function useContentSeo(args: {
|
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
// ── Synchronous: set title/description immediately so there is no
|
|
148
|
-
// flash of the base-page title on first render.
|
|
149
147
|
const seoSnippet = toSeoSnippet(snippet)!;
|
|
150
148
|
const quickTitle = (() => seoSnippet.title)();
|
|
151
149
|
const quickDescription = (() => seoSnippet.description)();
|
|
@@ -155,11 +153,13 @@ export async function useContentSeo(args: {
|
|
|
155
153
|
urlPath: snippet.link,
|
|
156
154
|
});
|
|
157
155
|
|
|
158
|
-
// ── Async: refine title with element-type phrase once loaded.
|
|
159
156
|
const elementPhrase = await getElementPhrase(snippet.schemaName);
|
|
160
|
-
const fullTitle =
|
|
157
|
+
const fullTitle = seoSnippet.title;
|
|
158
|
+
const refinedTitle = seoSnippet.titleInherited
|
|
159
|
+
? `${fullTitle} [${elementPhrase.element_name}]`
|
|
160
|
+
: fullTitle;
|
|
161
161
|
setupSeo({
|
|
162
|
-
title: `${
|
|
162
|
+
title: `${refinedTitle} - ${seoSiteTitle}`,
|
|
163
163
|
description: quickDescription || '',
|
|
164
164
|
urlPath: snippet.link,
|
|
165
165
|
});
|
|
@@ -39,6 +39,7 @@ export function usePhrases<const T extends readonly LanguagePhraseKey[]>(
|
|
|
39
39
|
|
|
40
40
|
const strFunctions = await $fetch<Record<string, string>>(
|
|
41
41
|
'/api/language/functions',
|
|
42
|
+
{ responseType: 'json' },
|
|
42
43
|
);
|
|
43
44
|
|
|
44
45
|
payloadLanguage.functions = strFunctions;
|
|
@@ -69,6 +70,7 @@ export function usePhrases<const T extends readonly LanguagePhraseKey[]>(
|
|
|
69
70
|
try {
|
|
70
71
|
payloadPhraseValue = await $fetch<PayloadLanguagePhraseValue>(
|
|
71
72
|
`/api/language/phrase/${phraseKey}`,
|
|
73
|
+
{ responseType: 'json' },
|
|
72
74
|
);
|
|
73
75
|
|
|
74
76
|
payloadLanguage.phrases[phraseKey] = payloadPhraseValue;
|
|
@@ -85,12 +85,14 @@ export function initScrollUpWatcher() {
|
|
|
85
85
|
addEventListener('resize', resetLayoutEstablished);
|
|
86
86
|
|
|
87
87
|
addEventListener('scroll', () => {
|
|
88
|
+
const currentY = window.scrollY;
|
|
89
|
+
|
|
88
90
|
if (!layoutEstablished) {
|
|
89
91
|
resetLayoutEstablished();
|
|
92
|
+
lastY = currentY;
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
const currentY = window.scrollY;
|
|
94
96
|
const delta = currentY - lastY;
|
|
95
97
|
|
|
96
98
|
sumDelta += delta;
|
|
@@ -19,6 +19,7 @@ if (ERUDIT.config.contributors?.enabled) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const phrase = await usePhrases('begin_learning');
|
|
22
|
+
const lastChangedDate = useLastChanged(() => mainContent.contentRelativePath);
|
|
22
23
|
|
|
23
24
|
await useContentSeo({
|
|
24
25
|
title: mainContent.title,
|
|
@@ -43,7 +44,7 @@ await useContentSeo({
|
|
|
43
44
|
<MainContentStats
|
|
44
45
|
mode="single"
|
|
45
46
|
:stats="mainContent.stats"
|
|
46
|
-
:
|
|
47
|
+
:lastChangedDate
|
|
47
48
|
/>
|
|
48
49
|
<div class="h-main-half"></div>
|
|
49
50
|
<MainAction
|
|
@@ -53,7 +54,7 @@ await useContentSeo({
|
|
|
53
54
|
:link="mainContent.children[0].link"
|
|
54
55
|
/>
|
|
55
56
|
<div class="h-main-half"></div>
|
|
56
|
-
<MainStickyHeader :mainContent />
|
|
57
|
+
<MainStickyHeader :mainContent :lastChangedDate />
|
|
57
58
|
</MainSectionPreamble>
|
|
58
59
|
<MainContentChildren :children="mainContent.children" />
|
|
59
60
|
<MainSection>
|
|
@@ -19,6 +19,7 @@ if (ERUDIT.config.contributors?.enabled) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const phrase = await usePhrases('group', 'begin_learning');
|
|
22
|
+
const lastChangedDate = useLastChanged(() => mainContent.contentRelativePath);
|
|
22
23
|
|
|
23
24
|
await useContentSeo({
|
|
24
25
|
title: mainContent.title,
|
|
@@ -45,7 +46,7 @@ await useContentSeo({
|
|
|
45
46
|
<MainContentStats
|
|
46
47
|
mode="single"
|
|
47
48
|
:stats="mainContent.stats"
|
|
48
|
-
:
|
|
49
|
+
:lastChangedDate
|
|
49
50
|
/>
|
|
50
51
|
<div class="h-main-half"></div>
|
|
51
52
|
<MainAction
|
|
@@ -55,7 +56,7 @@ await useContentSeo({
|
|
|
55
56
|
:link="mainContent.children[0].link"
|
|
56
57
|
/>
|
|
57
58
|
<div class="h-main-half"></div>
|
|
58
|
-
<MainStickyHeader :mainContent />
|
|
59
|
+
<MainStickyHeader :mainContent :lastChangedDate />
|
|
59
60
|
</MainSectionPreamble>
|
|
60
61
|
<MainContentChildren :children="mainContent.children" />
|
|
61
62
|
<MainSection>
|
|
@@ -17,6 +17,8 @@ async function proseMounted() {
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const lastChangedDate = useLastChanged(() => mainContent.contentRelativePath);
|
|
21
|
+
|
|
20
22
|
await useContentSeo({
|
|
21
23
|
title: mainContent.title,
|
|
22
24
|
bookTitle: mainContent.bookTitle,
|
|
@@ -43,12 +45,12 @@ await useContentSeo({
|
|
|
43
45
|
<MainContentStats
|
|
44
46
|
mode="single"
|
|
45
47
|
:stats="mainContent.stats"
|
|
46
|
-
:
|
|
48
|
+
:lastChangedDate
|
|
47
49
|
/>
|
|
48
50
|
<div class="h-main-half"></div>
|
|
49
51
|
<MainQuoteLoader />
|
|
50
52
|
<div class="h-main-half"></div>
|
|
51
|
-
<MainStickyHeader :mainContent />
|
|
53
|
+
<MainStickyHeader :mainContent :lastChangedDate />
|
|
52
54
|
</MainSectionPreamble>
|
|
53
55
|
<MainSection>
|
|
54
56
|
<Prose
|