erudit 4.2.0 → 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.
Files changed (59) hide show
  1. package/app/components/Prose.vue +2 -0
  2. package/app/components/aside/major/contentNav/items/ContentNavTopic.vue +12 -1
  3. package/app/components/aside/major/search/SearchResult.vue +16 -2
  4. package/app/components/aside/minor/contributor/AsideMinorContributor.vue +7 -1
  5. package/app/components/main/MainStickyHeader.vue +5 -2
  6. package/app/components/main/MainStickyHeaderPreamble.vue +5 -2
  7. package/app/components/main/MainTopicPartPage.vue +3 -2
  8. package/app/components/main/MainTopicPartSwitch.vue +18 -7
  9. package/app/components/main/connections/Deps.vue +1 -4
  10. package/app/components/main/connections/MainConnections.vue +9 -3
  11. package/app/components/main/contentStats/ItemLastChanged.vue +3 -32
  12. package/app/components/main/contentStats/MainContentStats.vue +3 -4
  13. package/app/components/preview/Preview.vue +8 -6
  14. package/app/components/preview/PreviewScreen.vue +9 -7
  15. package/app/components/preview/screen/Unique.vue +3 -2
  16. package/app/composables/ads.ts +1 -1
  17. package/app/composables/analytics.ts +1 -1
  18. package/app/composables/lastChanged.ts +38 -5
  19. package/app/composables/og.ts +5 -5
  20. package/app/composables/scrollUp.ts +3 -1
  21. package/app/pages/book/[...bookId].vue +3 -2
  22. package/app/pages/group/[...groupId].vue +3 -2
  23. package/app/pages/page/[...pageId].vue +4 -2
  24. package/app/plugins/appSetup/config.ts +1 -0
  25. package/app/plugins/appSetup/global.ts +3 -0
  26. package/app/plugins/appSetup/index.ts +4 -1
  27. package/app/plugins/devReload.client.ts +13 -0
  28. package/app/router.options.ts +17 -3
  29. package/app/styles/main.css +2 -2
  30. package/modules/erudit/dependencies.ts +16 -0
  31. package/modules/erudit/index.ts +8 -1
  32. package/modules/erudit/setup/autoImports.ts +143 -0
  33. package/modules/erudit/setup/elements/globalTemplate.ts +10 -2
  34. package/modules/erudit/setup/elements/setup.ts +8 -14
  35. package/modules/erudit/setup/elements/tagsTable.ts +2 -18
  36. package/modules/erudit/setup/fullRestart.ts +5 -3
  37. package/modules/erudit/setup/namesTable.ts +33 -0
  38. package/modules/erudit/setup/problemChecks/setup.ts +60 -0
  39. package/modules/erudit/setup/problemChecks/shared.ts +4 -0
  40. package/modules/erudit/setup/problemChecks/template.ts +33 -0
  41. package/modules/erudit/setup/runtimeConfig.ts +12 -7
  42. package/nuxt.config.ts +14 -6
  43. package/package.json +5 -6
  44. package/server/api/problemScript/[...problemScriptPath].ts +245 -60
  45. package/server/erudit/build.ts +10 -4
  46. package/server/erudit/content/nav/build.ts +5 -5
  47. package/server/erudit/content/nav/front.ts +1 -0
  48. package/server/erudit/content/repository/deps.ts +33 -3
  49. package/server/erudit/content/resolve/index.ts +3 -3
  50. package/server/erudit/content/resolve/utils/contentError.ts +2 -2
  51. package/server/erudit/content/resolve/utils/insertContentResolved.ts +22 -5
  52. package/server/erudit/global.ts +5 -1
  53. package/server/erudit/importer.ts +69 -0
  54. package/server/erudit/index.ts +2 -2
  55. package/server/erudit/logger.ts +18 -10
  56. package/server/erudit/reloadSignal.ts +14 -0
  57. package/server/routes/_reload.ts +27 -0
  58. package/shared/types/contentConnections.ts +1 -0
  59. package/shared/types/frontContentNav.ts +2 -0
@@ -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="array-lines"
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
- await router.replace({ ...route, hash: '' });
99
- await nextTick();
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="contributions?.length"
24
+ :count="totalCount"
19
25
  />
20
26
  <ScrollHolder v-if="contributions" class="flex-1">
21
27
  <TreeContainer>
@@ -1,5 +1,8 @@
1
1
  <script lang="ts" setup>
2
- const { mainContent } = defineProps<{ mainContent: MainContent }>();
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<{ mainContent: MainContent }>();
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
- :contentRelativePath="mainContent.contentRelativePath"
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
- :contentRelativePath="mainContent.contentRelativePath"
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
- border-border relative -bottom-[2px] flex h-(--switchHeight)
47
- items-center gap-1 rounded rounded-b-none border-2 transition-[color]`,
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 border-b-border cursor-not-allowed',
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
- `text-text-muted hocus:text-text bg-bg-aside
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('connections');
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 === 'autoDependencies' ? 'dependency' : 'dependent'"
89
+ :type="currentType === 'dependents' ? 'dependent' : 'dependency'"
86
90
  :deps="connections[currentType]!"
87
91
  />
88
- <Externals v-else :externals="connections[currentType]!" />
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
- type LastChangedSource = NonNullable<
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.value >= threeMonthsAgo && date.value <= now;
14
+ return date >= threeMonthsAgo && date <= now;
22
15
  });
23
16
 
24
17
  const formattedTitle = computed(() =>
25
- date.value ? date.value.toLocaleDateString(undefined, dateOptions) : '',
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
- contentRelativePath?: string;
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 || lastChangedSource)"
17
+ v-if="mode === 'single' && (stats || lastChangedDate)"
19
18
  class="px-main py-main-half"
20
19
  >
21
20
  <MainSubTitle :title="phrase.stats + ':'" />
@@ -35,7 +34,7 @@ const lastChangedSource = useLastChangedSource(() => props.contentRelativePath);
35
34
  :count
36
35
  mode="detailed"
37
36
  />
38
- <ItemLastChanged v-if="lastChangedSource" :source="lastChangedSource" />
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 bg-bg-main micro:max-h-[70dvh] border-border
112
- pointer-events-auto bottom-0 z-5 max-h-[90dvh] touch-auto overflow-hidden
113
- rounded-[25px] rounded-b-none border-t
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 absolute bottom-0 flex h-full w-full items-center
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-[50px]" />
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 micro:h-[60px] px-main
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="text-text-muted micro:text-[34px] shrink-0 text-[30px]"
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
- <div
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="to-bg-main pointer-events-none absolute bottom-0 left-0 h-full
47
- w-full touch-none bg-linear-to-b from-transparent"
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>
@@ -11,7 +11,7 @@ export function adsAllowed() {
11
11
  return false;
12
12
  }
13
13
 
14
- return ERUDIT.config.mode === 'static';
14
+ return ERUDIT.mode === 'static';
15
15
  }
16
16
 
17
17
  export function adsAsideAllowed() {
@@ -13,7 +13,7 @@ export function initAnalytics() {
13
13
  return debugValue;
14
14
  }
15
15
 
16
- return ERUDIT.config.mode === 'static' ? true : false;
16
+ return ERUDIT.mode === 'static' ? true : false;
17
17
  })();
18
18
 
19
19
  if (!analytics || !analyticsEnabled) {
@@ -2,21 +2,21 @@ type LastChangedSource =
2
2
  | { type: 'date'; value: string }
3
3
  | { type: 'github'; url: string; path: string };
4
4
 
5
- export function useLastChangedSource(
5
+ function useLastChangedSource(
6
6
  contentRelativePath: MaybeRefOrGetter<string | undefined>,
7
7
  ) {
8
- return computed((): LastChangedSource | null => {
8
+ return computed((): LastChangedSource | undefined => {
9
9
  const path = toValue(contentRelativePath);
10
- if (!path) return null;
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 null;
17
+ if (!repo || repo.type !== 'github') return undefined;
18
18
  const parts = repo.name.split('/');
19
- if (parts.length !== 2) return null;
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
+ }
@@ -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 = (() => seoSnippet.title)();
157
+ const fullTitle = seoSnippet.title;
158
+ const refinedTitle = seoSnippet.titleInherited
159
+ ? `${fullTitle} [${elementPhrase.element_name}]`
160
+ : fullTitle;
161
161
  setupSeo({
162
- title: `${fullTitle} [${elementPhrase.element_name}] - ${seoSiteTitle}`,
162
+ title: `${refinedTitle} - ${seoSiteTitle}`,
163
163
  description: quickDescription || '',
164
164
  urlPath: snippet.link,
165
165
  });
@@ -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
- :contentRelativePath="mainContent.contentRelativePath"
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
- :contentRelativePath="mainContent.contentRelativePath"
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
- :contentRelativePath="mainContent.contentRelativePath"
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
@@ -3,4 +3,5 @@ export type EruditAppConfig = EruditRuntimeConfig;
3
3
  export async function setupAppRuntimeConfig() {
4
4
  const runtimeConfig = useRuntimeConfig();
5
5
  ERUDIT.config = runtimeConfig.public.erudit as any;
6
+ ERUDIT.mode = runtimeConfig.public.eruditMode as any;
6
7
  }
@@ -1,3 +1,6 @@
1
+ import type { EruditMode } from '@erudit-js/core/mode';
2
+
1
3
  export const ERUDIT: {
4
+ mode: EruditMode;
2
5
  config: EruditPublicRuntimeConfig;
3
6
  } = {} as any;
@@ -13,7 +13,10 @@ export default defineNuxtPlugin({
13
13
  hooks: {
14
14
  'app:mounted': async () => {
15
15
  const { registerProseGlobals } = await import('#erudit/prose/global');
16
- registerProseGlobals();
16
+ await registerProseGlobals();
17
+
18
+ const { registerAutoImportGlobals } = await import('#erudit/autoImports');
19
+ await registerAutoImportGlobals();
17
20
 
18
21
  await setupHtmlBranding();
19
22
  await setupWelcomeMessage();
@@ -0,0 +1,13 @@
1
+ import { isDevLikeMode } from '@erudit-js/core/mode';
2
+
3
+ export default defineNuxtPlugin({
4
+ name: 'erudit-dev-reload',
5
+ setup() {
6
+ const runtimeConfig = useRuntimeConfig().public;
7
+ const mode = runtimeConfig.eruditMode as any;
8
+ if (!isDevLikeMode(mode) || !runtimeConfig.eruditReload) return;
9
+
10
+ const es = new EventSource('/_reload');
11
+ es.onmessage = () => window.location.reload();
12
+ },
13
+ });