erudit 4.0.1 → 4.1.0

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 (51) hide show
  1. package/app/app.vue +2 -0
  2. package/app/assets/icons/bookmark-star.svg +3 -0
  3. package/app/components/Prose.vue +1 -0
  4. package/app/components/aside/AsideSwitch.vue +4 -26
  5. package/app/components/aside/major/PaneHolder.vue +0 -18
  6. package/app/components/aside/major/SiteInfo.vue +10 -10
  7. package/app/components/aside/major/contentNav/items/ItemTemplate.vue +1 -1
  8. package/app/components/aside/major/pages/PanePages.vue +1 -1
  9. package/app/components/aside/major/search/PaneSearch.vue +1 -1
  10. package/app/components/aside/major/search/SearchInput.vue +0 -12
  11. package/app/components/aside/major/search/SearchResult.vue +1 -1
  12. package/app/components/aside/minor/content/Toc.vue +0 -2
  13. package/app/components/aside/minor/news/elements/Ref.vue +24 -8
  14. package/app/components/main/MainFlag.vue +22 -44
  15. package/app/components/main/MainQuickLink.vue +31 -47
  16. package/app/components/main/MainSection.vue +5 -2
  17. package/app/components/main/MainStickyHeader.vue +193 -0
  18. package/app/components/main/MainStickyHeaderPreamble.vue +23 -0
  19. package/app/components/main/MainTopicPartPage.vue +1 -0
  20. package/app/components/main/connections/Deps.vue +3 -1
  21. package/app/components/main/connections/Externals.vue +1 -1
  22. package/app/components/preview/Preview.vue +3 -3
  23. package/app/composables/favicon.ts +3 -3
  24. package/app/composables/loading.ts +1 -1
  25. package/app/composables/og.ts +17 -2
  26. package/app/composables/popup.ts +140 -0
  27. package/app/composables/preview.ts +2 -0
  28. package/app/composables/scrollUp.ts +210 -0
  29. package/app/pages/book/[...bookId].vue +3 -1
  30. package/app/pages/group/[...groupId].vue +3 -1
  31. package/app/pages/page/[...pageId].vue +1 -0
  32. package/app/router.options.ts +8 -3
  33. package/app/styles/main.css +8 -1
  34. package/modules/erudit/setup/globals.ts +0 -4
  35. package/modules/erudit/setup/runtimeConfig.ts +9 -14
  36. package/package.json +5 -6
  37. package/server/erudit/content/nav/front.ts +2 -2
  38. package/server/erudit/content/repository/children.ts +5 -0
  39. package/server/erudit/content/repository/contentLink.ts +1 -1
  40. package/server/erudit/content/repository/deps.ts +134 -86
  41. package/server/erudit/content/repository/hidden.ts +38 -0
  42. package/server/erudit/content/repository/stats.ts +5 -2
  43. package/server/erudit/content/resolve/topic.ts +10 -8
  44. package/server/erudit/content/resolve/utils/insertContentResolved.ts +57 -16
  45. package/server/erudit/content/search.ts +58 -51
  46. package/server/erudit/contributors/repository/contributions.ts +1 -1
  47. package/server/erudit/language/list/ru.ts +1 -1
  48. package/server/erudit/prose/storage/link.ts +146 -132
  49. package/server/erudit/repository.ts +2 -0
  50. package/server/routes/sitemap.xml.ts +5 -0
  51. package/shared/types/runtimeConfig.ts +5 -10
package/app/app.vue CHANGED
@@ -1,7 +1,9 @@
1
1
  <script lang="ts" setup>
2
2
  initFavicon();
3
3
  initOgImage();
4
+ initOgSiteName();
4
5
  initAnalytics();
6
+ initScrollUpWatcher();
5
7
 
6
8
  // Watching and setting theme
7
9
  initThemeWatcher();
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
2
+ <path d="m389-400 91-55 91 55-24-104 80-69-105-9-42-98-42 98-105 9 80 69-24 104ZM200-120v-640q0-33 23.5-56.5T280-840h400q33 0 56.5 23.5T760-760v640L480-240 200-120Zm80-122 200-86 200 86v-518H280v518Zm0-518h400-400Z"/>
3
+ </svg>
@@ -40,6 +40,7 @@ const context: ProseContext = {
40
40
  EruditLink,
41
41
  setPreview,
42
42
  closePreview,
43
+ usePopup,
43
44
  loadingSvg,
44
45
  };
45
46
  </script>
@@ -6,32 +6,10 @@ const asideType = inject(asideTypeSymbol);
6
6
  const isMajor = computed(() => asideType === AsideType.Major);
7
7
  const isMinor = computed(() => asideType === AsideType.Minor);
8
8
 
9
- if (import.meta.client) {
10
- callOnce(async () => {
11
- // Wait for possible layout shifts triggering scroll events
12
- await new Promise((resolve) => setTimeout(resolve, 100));
13
-
14
- let lastY = window.scrollY;
15
- let sumDelta = 0;
16
- let scrollTimeout: any;
17
-
18
- window.addEventListener('scroll', () => {
19
- const currentY = window.scrollY;
20
- const delta = currentY - lastY;
21
-
22
- sumDelta += delta;
23
-
24
- asideState.value.scrolledUp = sumDelta <= 5;
25
-
26
- lastY = currentY;
27
-
28
- clearTimeout(scrollTimeout);
29
- scrollTimeout = setTimeout(() => {
30
- sumDelta = 0;
31
- }, 200);
32
- });
33
- });
34
- }
9
+ const scrollUp = useScrollUp();
10
+ watch(scrollUp, () => {
11
+ asideState.value.scrolledUp = scrollUp.value;
12
+ });
35
13
  </script>
36
14
 
37
15
  <template>
@@ -33,18 +33,6 @@ watch(asideMajorPane, (next, prev) => {
33
33
  direction.value = nextIdx > prevIdx ? 'forward' : 'backward';
34
34
  });
35
35
 
36
- const handleKeyDown = (event: KeyboardEvent) => {
37
- if (event.shiftKey && event.code === 'KeyF') {
38
- event.preventDefault();
39
- asideMajorPane.value = AsideMajorPane.Search;
40
- const styles = getComputedStyle(document.documentElement);
41
- const aside1Bp = styles.getPropertyValue('--breakpoint-aside1');
42
- if (window.matchMedia(`(max-width: ${aside1Bp})`).matches) {
43
- asideState.value.opened = AsideType.Major;
44
- }
45
- }
46
- };
47
-
48
36
  onMounted(() => {
49
37
  if (route.query.q) {
50
38
  asideMajorPane.value = AsideMajorPane.Search;
@@ -54,12 +42,6 @@ onMounted(() => {
54
42
  asideState.value.opened = AsideType.Major;
55
43
  }
56
44
  }
57
-
58
- window.addEventListener('keydown', handleKeyDown);
59
- });
60
-
61
- onUnmounted(() => {
62
- window.removeEventListener('keydown', handleKeyDown);
63
45
  });
64
46
 
65
47
  // Prefetch pages data
@@ -1,39 +1,39 @@
1
1
  <script lang="ts" setup>
2
2
  const withBaseUrl = useBaseUrl();
3
3
 
4
- const siteInfo = ERUDIT.config.siteInfo;
4
+ const siteInfo = ERUDIT.config.asideMajor?.siteInfo;
5
5
 
6
6
  const fetchPhrases: LanguagePhraseKey[] = [];
7
7
 
8
- if (!siteInfo.title) {
8
+ if (!siteInfo?.title) {
9
9
  fetchPhrases.push('erudit');
10
10
  }
11
11
 
12
- if (!siteInfo.short && siteInfo.short !== false) {
12
+ if (!siteInfo?.short && siteInfo?.short !== false) {
13
13
  fetchPhrases.push('default_site_info_short');
14
14
  }
15
15
 
16
16
  const phrase = await usePhrases(...fetchPhrases);
17
17
 
18
- const title = siteInfo.title || phrase.erudit;
18
+ const title = siteInfo?.title || phrase.erudit;
19
19
  const short =
20
- siteInfo.short === false
20
+ siteInfo?.short === false
21
21
  ? undefined
22
- : siteInfo.short || phrase.default_site_info_short;
22
+ : siteInfo?.short || phrase.default_site_info_short;
23
23
 
24
24
  const logotype = (() => {
25
- if (siteInfo.logotype === false) {
25
+ if (siteInfo?.logotype === false) {
26
26
  return false;
27
27
  }
28
28
 
29
- if (!siteInfo.logotype) {
29
+ if (!siteInfo?.logotype) {
30
30
  return eruditPublic('logotype.svg');
31
31
  }
32
32
 
33
33
  return String(siteInfo.logotype);
34
34
  })();
35
35
 
36
- const layout = siteInfo.logotype === false ? 'column' : 'row';
36
+ const layout = siteInfo?.logotype === false ? 'column' : 'row';
37
37
  </script>
38
38
 
39
39
  <template>
@@ -45,7 +45,7 @@ const layout = siteInfo.logotype === false ? 'column' : 'row';
45
45
  {
46
46
  'gap-normal items-center': layout === 'row',
47
47
  'gap-small flex-col': layout === 'column',
48
- 'text-center': layout === 'column' || siteInfo.logotype === false,
48
+ 'text-center': layout === 'column' || siteInfo?.logotype === false,
49
49
  },
50
50
  ]"
51
51
  >
@@ -19,7 +19,7 @@ const resolvedTo = computed(() => {
19
19
  </script>
20
20
 
21
21
  <template>
22
- <TreeItem :icon :main="navItem.title" :state :to="resolvedTo">
22
+ <TreeItem :icon :main="formatText(navItem.title)" :state :to="resolvedTo">
23
23
  <template v-slot:secondary v-if="navItem.flags">
24
24
  <Flags :flags="navItem.flags" />
25
25
  </template>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  const routePath = useRoutePath();
3
3
 
4
- const customLinks = ERUDIT.config.customLinks;
4
+ const customLinks = ERUDIT.config.asideMajor?.customLinks;
5
5
 
6
6
  const { data: pagesData } =
7
7
  useNuxtData<AsideMajorPagesData>(asideMajorPagesKey);
@@ -42,7 +42,7 @@ onMounted(() => {
42
42
  baseUrl: withBaseUrl('/'),
43
43
  language: ERUDIT.config.language.current,
44
44
  cacheId: String(
45
- import.meta.dev ? Date.now() : runtimeConfig.public.buildTimestamp,
45
+ import.meta.dev ? Date.now() : runtimeConfig.public.buildTime,
46
46
  ),
47
47
  };
48
48
 
@@ -38,13 +38,6 @@ function insertUrlQuery(q: string | undefined) {
38
38
  router.replace({ ...route, query: { ...route.query, q } });
39
39
  }
40
40
 
41
- function handleKeyDown(event: KeyboardEvent) {
42
- if (event.shiftKey && event.code === 'KeyF') {
43
- event.preventDefault();
44
- onWake();
45
- }
46
- }
47
-
48
41
  watch(normalizedQuery, () => {
49
42
  insertUrlQuery(urlParamQuery.value);
50
43
  scheduleEmit();
@@ -53,11 +46,6 @@ watch(normalizedQuery, () => {
53
46
  onMounted(() => {
54
47
  initFromUrlParam();
55
48
  onWake();
56
- window.addEventListener('keydown', handleKeyDown);
57
- });
58
-
59
- onUnmounted(() => {
60
- window.removeEventListener('keydown', handleKeyDown);
61
49
  });
62
50
 
63
51
  onActivated(() => {
@@ -119,7 +119,7 @@ async function searchResultClick() {
119
119
  {{ formatText(secondaryTitle) }}
120
120
  </div>
121
121
  <div class="gap-small text-text flex items-center">
122
- <div class="relative size-[1.2em]">
122
+ <div class="relative size-[1.2em] shrink-0">
123
123
  <TransitionFade>
124
124
  <MaybeMyIcon
125
125
  :name="icon"
@@ -4,7 +4,6 @@ import type { ResolvedTocItem } from '@erudit-js/prose';
4
4
  import TocItem from './TocItem.vue';
5
5
 
6
6
  const props = defineProps<{ toc?: ResolvedTocItem[] }>();
7
-
8
7
  const phrase = await usePhrases('no_toc');
9
8
 
10
9
  /**
@@ -158,6 +157,5 @@ onMounted(() => {
158
157
  />
159
158
  </TreeContainer>
160
159
  </div>
161
-
162
160
  <AsidePlainMessage v-else :text="phrase.no_toc" />
163
161
  </template>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts" setup>
2
+ import { onMounted } from 'vue';
2
3
  import type { ProseElement } from '@jsprose/core';
3
4
  import type { refSchema } from '@erudit-js/prose/elements/link/reference/core';
4
5
  import type { LinkStorage } from '@erudit-js/prose/elements/link/storage';
@@ -22,37 +23,52 @@ const doubleClick = {
22
23
  };
23
24
 
24
25
  function linkClick() {
25
- if (doubleClick.timeout) {
26
+ if (doubleClick.timeout && linkStorage.type !== 'error') {
26
27
  doubleClick.reset();
27
28
  closePreview();
28
29
  const openUrl =
29
- linkStorage.type === 'direct'
30
+ linkStorage.type === 'external'
30
31
  ? linkStorage.resolvedHref
31
32
  : withBaseUrl(linkStorage.resolvedHref.slice(1));
32
33
  window.open(openUrl, '_blank');
33
34
  return false;
34
35
  }
35
36
 
37
+ if (linkStorage.type === 'error') {
38
+ return false;
39
+ }
40
+
36
41
  setPreview(linkStorage.previewRequest);
37
42
  doubleClick.startTimeout();
38
43
  return false;
39
44
  }
45
+
46
+ onMounted(() => {
47
+ if (linkStorage.type === 'error') {
48
+ console.warn(`Error in link element inside new item: ${linkStorage.error}`);
49
+ }
50
+ });
40
51
  </script>
41
52
 
42
53
  <template>
43
54
  <EruditLink
44
55
  @click.capture.prevent="linkClick"
45
- :to="linkStorage.resolvedHref"
56
+ :to="linkStorage.type === 'error' ? undefined : linkStorage.resolvedHref"
46
57
  :style="{
47
58
  '--tGap': '1px',
48
59
  '--xGap': '4px',
49
60
  '--bGap': '4px',
50
61
  }"
51
- class="hocus:bg-(--linkColor)/15 relative -mx-[calc(var(--xGap)-3px)]
52
- -mt-(--tGap) -mb-(--bGap) rounded-sm bg-transparent px-(--xGap)
53
- pt-(--tGap) pb-(--bGap) text-(--linkColor) underline
54
- decoration-[color-mix(in_srgb,var(--linkColor)30%,transparent)]
55
- decoration-2 underline-offset-2 transition-[background]"
62
+ :class="[
63
+ `relative -mx-[calc(var(--xGap)-3px)] -mt-(--tGap) -mb-(--bGap) rounded-sm
64
+ bg-transparent px-(--xGap) pt-(--tGap) pb-(--bGap) underline decoration-2
65
+ underline-offset-2 transition-[background]`,
66
+ linkStorage.type === 'error'
67
+ ? `hocus:bg-red-400/20 cursor-not-allowed text-red-600
68
+ decoration-red-400/40 dark:text-red-400`
69
+ : `hocus:bg-(--linkColor)/15 text-(--linkColor)
70
+ decoration-[color-mix(in_srgb,var(--linkColor)30%,transparent)]`,
71
+ ]"
56
72
  >
57
73
  {{ formatText(element.data.label) }}
58
74
  </EruditLink>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
- import { autoUpdate, shift, useFloating } from '@floating-ui/vue';
3
- import type { MyIconName } from '#my-icons';
2
+ import { autoUpdate, shift } from '@floating-ui/vue';
4
3
  import type { ContentFlag } from '@erudit-js/core/content/flags';
4
+ import type { MyIconName } from '#my-icons';
5
5
 
6
6
  interface FlagData {
7
7
  icon: MyIconName;
@@ -44,54 +44,32 @@ const flagsData: Record<ContentFlag, FlagData> = {
44
44
 
45
45
  const flagData = flagsData[flag];
46
46
 
47
- const referenceElement = useTemplateRef('reference');
47
+ const containerElement = useTemplateRef('container');
48
+ const toggleElement = useTemplateRef('toggle');
48
49
  const popupElement = useTemplateRef('popup');
49
- const popupVisible = ref(false);
50
- const showTimeout = ref<ReturnType<typeof setTimeout> | null>(null);
51
-
52
- const { floatingStyles } = useFloating(referenceElement, popupElement, {
53
- placement: 'bottom',
54
- whileElementsMounted: autoUpdate,
55
- middleware: [
56
- shift({
57
- boundary: document?.querySelector('[data-erudit-main]') || undefined,
58
- }),
59
- ],
60
- });
61
-
62
- const showPopup = () => {
63
- showTimeout.value = setTimeout(() => {
64
- popupVisible.value = true;
65
- }, 400);
66
- };
67
-
68
- const hidePopup = () => {
69
- if (showTimeout.value) {
70
- clearTimeout(showTimeout.value);
71
- showTimeout.value = null;
72
- }
73
- popupVisible.value = false;
74
- };
75
-
76
- onUnmounted(() => {
77
- if (showTimeout.value) {
78
- clearTimeout(showTimeout.value);
79
- }
80
- });
50
+ const { popupVisible, popupStyles } = usePopup(
51
+ containerElement,
52
+ toggleElement,
53
+ popupElement,
54
+ {
55
+ placement: 'bottom',
56
+ whileElementsMounted: autoUpdate,
57
+ middleware: [
58
+ shift({
59
+ boundary: document?.querySelector('[data-erudit-main]') || undefined,
60
+ }),
61
+ ],
62
+ },
63
+ );
81
64
  </script>
82
65
 
83
66
  <template>
84
- <div
85
- ref="reference"
86
- :style="{ '--flagColor': flagData.color }"
87
- @mouseenter="showPopup"
88
- @mouseleave="hidePopup"
89
- >
67
+ <div ref="container" :style="{ '--flagColor': flagData.color }">
90
68
  <div
91
- @touchstart="popupVisible ? hidePopup() : showPopup()"
69
+ ref="toggle"
92
70
  class="px-small text-main-sm flex cursor-help items-center gap-1 rounded
93
71
  border border-(--flagColor)/30 bg-(--flagColor)/15 py-1
94
- text-(--flagColor)"
72
+ text-(--flagColor) select-none"
95
73
  >
96
74
  <MyIcon :name="flagData.icon" class="text-[1.3em]" />
97
75
  <span>{{ formatText(flagData.title) }}</span>
@@ -99,7 +77,7 @@ onUnmounted(() => {
99
77
  <TransitionFade>
100
78
  <div
101
79
  v-if="popupVisible"
102
- :style="floatingStyles"
80
+ :style="popupStyles"
103
81
  ref="popup"
104
82
  class="z-10 max-w-[320px] p-2"
105
83
  >
@@ -1,42 +1,25 @@
1
1
  <script lang="ts" setup>
2
- import { autoUpdate, shift, useFloating } from '@floating-ui/vue';
2
+ import { autoUpdate, shift } from '@floating-ui/vue';
3
3
 
4
4
  const { quickLink } = defineProps<{ quickLink: ElementSnippet }>();
5
5
 
6
- const referenceElement = useTemplateRef('reference');
6
+ const containerElement = useTemplateRef('container');
7
+ const toggleElement = useTemplateRef('toggle');
7
8
  const popupElement = useTemplateRef('popup');
8
- const popupVisible = ref(false);
9
- const showTimeout = ref<ReturnType<typeof setTimeout> | null>(null);
10
-
11
- const { floatingStyles } = useFloating(referenceElement, popupElement, {
12
- placement: 'bottom',
13
- whileElementsMounted: autoUpdate,
14
- middleware: [
15
- shift({
16
- boundary: document?.querySelector('[data-erudit-main]') || undefined,
17
- }),
18
- ],
19
- });
20
-
21
- const showPopup = () => {
22
- showTimeout.value = setTimeout(() => {
23
- popupVisible.value = true;
24
- }, 400);
25
- };
26
-
27
- const hidePopup = () => {
28
- if (showTimeout.value) {
29
- clearTimeout(showTimeout.value);
30
- showTimeout.value = null;
31
- }
32
- popupVisible.value = false;
33
- };
34
-
35
- onUnmounted(() => {
36
- if (showTimeout.value) {
37
- clearTimeout(showTimeout.value);
38
- }
39
- });
9
+ const { popupVisible, popupStyles } = usePopup(
10
+ containerElement,
11
+ toggleElement,
12
+ popupElement,
13
+ {
14
+ placement: 'bottom',
15
+ whileElementsMounted: autoUpdate,
16
+ middleware: [
17
+ shift({
18
+ boundary: document?.querySelector('[data-erudit-main]') || undefined,
19
+ }),
20
+ ],
21
+ },
22
+ );
40
23
 
41
24
  const elementIcon = await getElementIcon(quickLink.schemaName);
42
25
 
@@ -58,22 +41,23 @@ const description = computed(() => {
58
41
  </script>
59
42
 
60
43
  <template>
61
- <div ref="reference" @mouseenter="showPopup" @mouseleave="hidePopup">
62
- <EruditLink
63
- @touchstart="popupVisible ? hidePopup() : showPopup()"
64
- :to="quickLink.link"
65
- class="gap-small border-border px-small text-text-muted text-main-sm
66
- hocus:text-brand hocus:border-brand hocus:ring-brand/25 flex
67
- items-center rounded border bg-(--quickBg) py-1 ring-2 ring-transparent
68
- transition-[color,border,box-shadow]"
69
- >
70
- <MaybeMyIcon :name="elementIcon" class="-mr-0.5 text-[1.2em]" />
71
- <span>{{ formatText(title) }}</span>
72
- </EruditLink>
44
+ <div ref="container">
45
+ <div ref="toggle">
46
+ <EruditLink
47
+ :to="quickLink.link"
48
+ class="gap-small border-border px-small text-text-muted text-main-sm
49
+ hocus:text-brand hocus:border-brand hocus:ring-brand/25 flex
50
+ items-center rounded border bg-(--quickBg) py-1 ring-2
51
+ ring-transparent transition-[color,border,box-shadow]"
52
+ >
53
+ <MaybeMyIcon :name="elementIcon" class="-mr-0.5 text-[1.2em]" />
54
+ <span>{{ formatText(title) }}</span>
55
+ </EruditLink>
56
+ </div>
73
57
  <TransitionFade>
74
58
  <div
75
59
  v-if="description && popupVisible"
76
- :style="floatingStyles"
60
+ :style="popupStyles"
77
61
  ref="popup"
78
62
  class="z-10 max-w-[300px] p-2"
79
63
  >
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <section :class="[$style.section, 'bg-bg-main']">
2
+ <section :class="[$style.section]">
3
3
  <!-- Section Header -->
4
4
  <div :class="[$style.header, 'border-border relative border-b-2']">
5
5
  <!-- Header Shade -->
@@ -15,7 +15,7 @@
15
15
  </div>
16
16
 
17
17
  <!-- Section Body -->
18
- <div class="relative z-1">
18
+ <div :class="[$style.body, 'bg-bg-main relative z-1']">
19
19
  <slot></slot>
20
20
  </div>
21
21
  </section>
@@ -26,6 +26,9 @@
26
26
  .header {
27
27
  display: none;
28
28
  }
29
+ .body {
30
+ background: transparent;
31
+ }
29
32
  }
30
33
 
31
34
  .section:not(:nth-child(1 of .section)) {