erudit 4.1.1 → 4.2.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 (116) hide show
  1. package/app/assets/icons/update.svg +3 -0
  2. package/app/components/Prose.vue +7 -7
  3. package/app/components/SmartMedia.vue +4 -4
  4. package/app/components/aside/major/contentNav/PaneBookNav.vue +1 -4
  5. package/app/components/aside/minor/content/Toc.vue +24 -1
  6. package/app/components/aside/minor/content/TocItem.vue +2 -1
  7. package/app/components/aside/minor/news/AsideMinorNews.vue +1 -3
  8. package/app/components/aside/minor/news/NewsItem.vue +3 -4
  9. package/app/components/aside/minor/news/RenderNewsElement.vue +4 -11
  10. package/app/components/aside/minor/news/elements/Mix.vue +2 -3
  11. package/app/components/aside/minor/news/elements/P.vue +3 -3
  12. package/app/components/aside/minor/news/elements/Ref.vue +3 -3
  13. package/app/components/aside/minor/news/elements/Text.vue +2 -2
  14. package/app/components/main/MainContentChild.vue +3 -3
  15. package/app/components/main/{MainQuickLink.vue → MainKeyLink.vue} +11 -11
  16. package/app/components/main/{MainQuickLinks.vue → MainKeyLinks.vue} +7 -7
  17. package/app/components/main/MainQuoteLoader.vue +2 -4
  18. package/app/components/main/MainStickyHeader.vue +1 -1
  19. package/app/components/main/MainStickyHeaderPreamble.vue +6 -2
  20. package/app/components/main/MainTopicPartPage.vue +8 -3
  21. package/app/components/main/connections/DepUnique.vue +45 -0
  22. package/app/components/main/connections/Deps.vue +14 -2
  23. package/app/components/main/connections/Externals.vue +1 -1
  24. package/app/components/main/connections/MainConnections.vue +1 -0
  25. package/app/components/main/contentStats/ItemLastChanged.vue +68 -0
  26. package/app/components/main/contentStats/MainContentStats.vue +36 -28
  27. package/app/components/preview/PreviewScreen.vue +2 -2
  28. package/app/components/preview/screen/ContentPage.vue +1 -4
  29. package/app/components/preview/screen/Unique.vue +3 -5
  30. package/app/composables/appElements.ts +2 -4
  31. package/app/composables/asideMajorPane.ts +3 -3
  32. package/app/composables/fetchJson.ts +4 -0
  33. package/app/composables/lastChanged.ts +28 -0
  34. package/app/composables/mainContent.ts +1 -4
  35. package/app/composables/og.ts +43 -35
  36. package/app/composables/phrases.ts +0 -3
  37. package/app/composables/scrollUp.ts +1 -1
  38. package/app/pages/book/[...bookId].vue +5 -1
  39. package/app/pages/contributor/[contributorId].vue +3 -5
  40. package/app/pages/contributors.vue +1 -1
  41. package/app/pages/group/[...groupId].vue +5 -1
  42. package/app/pages/index.vue +1 -1
  43. package/app/pages/page/[...pageId].vue +8 -3
  44. package/app/pages/sponsors.vue +1 -1
  45. package/app/plugins/appSetup/index.ts +0 -5
  46. package/app/plugins/fetchJson.ts +11 -0
  47. package/app/plugins/prerender.server.ts +1 -1
  48. package/app/router.options.ts +1 -1
  49. package/modules/erudit/globals/prose.ts +3 -4
  50. package/modules/erudit/setup/elements/appTemplate.ts +6 -7
  51. package/modules/erudit/setup/elements/{globalTypes.ts → elementGlobalTypes.ts} +21 -21
  52. package/modules/erudit/setup/elements/globalTemplate.ts +29 -23
  53. package/modules/erudit/setup/elements/setup.ts +18 -16
  54. package/modules/erudit/setup/elements/shared.ts +2 -2
  55. package/modules/erudit/setup/elements/tagsTable.ts +1 -1
  56. package/modules/erudit/setup/runtimeConfig.ts +2 -0
  57. package/nuxt.config.ts +2 -2
  58. package/package.json +14 -13
  59. package/server/api/main/content/[...contentTypePath].ts +5 -4
  60. package/server/api/prerender/content.ts +1 -3
  61. package/server/api/preview/contentPage/[...contentTypePath].ts +1 -2
  62. package/server/api/preview/contentUnique/[...contentTypePathUnique].ts +16 -31
  63. package/server/api/problemScript/[...problemScriptPath].ts +73 -4
  64. package/server/erudit/content/global/build.ts +21 -7
  65. package/server/erudit/content/nav/build.ts +4 -4
  66. package/server/erudit/content/repository/children.ts +3 -3
  67. package/server/erudit/content/repository/deps.ts +101 -13
  68. package/server/erudit/content/repository/elementSnippets.ts +16 -16
  69. package/server/erudit/content/repository/stats.ts +30 -22
  70. package/server/erudit/content/repository/topicParts.ts +1 -1
  71. package/server/erudit/content/repository/unique.ts +14 -15
  72. package/server/erudit/content/resolve/page.ts +15 -35
  73. package/server/erudit/content/resolve/topic.ts +33 -164
  74. package/server/erudit/content/resolve/utils/insertContentResolved.ts +74 -31
  75. package/server/erudit/content/search.ts +5 -22
  76. package/server/erudit/contributors/build.ts +7 -8
  77. package/server/erudit/db/repository/pushFile.ts +10 -3
  78. package/server/erudit/db/repository/pushProblemScript.ts +14 -3
  79. package/server/erudit/db/schema/contentDeps.ts +3 -0
  80. package/server/erudit/db/schema/contentSnippets.ts +3 -3
  81. package/server/erudit/db/schema/contentUniques.ts +2 -2
  82. package/server/erudit/db/schema/contributors.ts +2 -2
  83. package/server/erudit/db/schema/news.ts +2 -2
  84. package/server/erudit/db/schema/pages.ts +2 -2
  85. package/server/erudit/db/schema/topics.ts +4 -4
  86. package/server/erudit/global.ts +4 -0
  87. package/server/erudit/importer.ts +16 -8
  88. package/server/erudit/index.ts +0 -3
  89. package/server/erudit/language/list/en.ts +1 -0
  90. package/server/erudit/language/list/ru.ts +1 -0
  91. package/server/erudit/news/build.ts +6 -6
  92. package/server/erudit/news/repository/batch.ts +2 -2
  93. package/server/erudit/prose/repository/finalize.ts +22 -25
  94. package/server/erudit/prose/repository/get.ts +3 -5
  95. package/server/erudit/prose/repository/rawToProse.ts +31 -0
  96. package/server/erudit/prose/storage/callout.ts +9 -7
  97. package/server/erudit/prose/storage/image.ts +8 -11
  98. package/server/erudit/prose/storage/link.ts +24 -32
  99. package/server/erudit/prose/storage/problemScript.ts +8 -14
  100. package/server/erudit/prose/storage/video.ts +9 -7
  101. package/server/erudit/repository.ts +4 -4
  102. package/server/routes/file/[...path].ts +1 -1
  103. package/shared/types/contentChildren.ts +5 -2
  104. package/shared/types/contentConnections.ts +9 -0
  105. package/shared/types/elementSnippet.ts +1 -1
  106. package/shared/types/indexPage.ts +3 -0
  107. package/shared/types/language.ts +1 -83
  108. package/shared/types/mainContent.ts +11 -5
  109. package/shared/types/news.ts +2 -2
  110. package/shared/types/preview.ts +3 -2
  111. package/shared/types/runtimeConfig.ts +1 -0
  112. package/shared/types/search.ts +2 -0
  113. package/shared/utils/pages.ts +4 -2
  114. package/shared/utils/stringColor.ts +16 -6
  115. package/server/erudit/prose/repository/resolve.ts +0 -17
  116. package/server/erudit/prose/transform/bundleProblemScript.ts +0 -6
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
2
+ <path d="M480-120q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T120-480q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-840q82 0 155.5 35T760-706v-94h80v240H600v-80h110q-41-56-101-88t-129-32q-117 0-198.5 81.5T200-480q0 117 81.5 198.5T480-200q105 0 183.5-68T756-440h82q-15 137-117.5 228.5T480-120Zm112-192L440-464v-216h80v184l128 128-56 56Z"/>
3
+ </svg>
@@ -1,14 +1,15 @@
1
1
  <script lang="ts" setup>
2
- import type { AnySchema, GenericStorage, ProseElement } from '@jsprose/core';
2
+ import type { ProseElement, ProseStorage } from 'tsprose';
3
3
  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
7
 
8
- const { element, storage, useHashUrl } = defineProps<{
9
- element: ProseElement<AnySchema>;
10
- storage: GenericStorage;
8
+ const { element, storage, useHashUrl, setHtmlIds } = defineProps<{
9
+ element: ProseElement;
10
+ storage: ProseStorage;
11
11
  useHashUrl: boolean;
12
+ setHtmlIds: boolean;
12
13
  }>();
13
14
 
14
15
  const runtimeConfig = useRuntimeConfig();
@@ -18,9 +19,7 @@ const route = useRoute();
18
19
 
19
20
  const hashUrl = computed(() => {
20
21
  return useHashUrl
21
- ? route.hash
22
- ? route.hash.slice(1)
23
- : undefined
22
+ ? ((route.query.element as string | undefined) ?? undefined)
24
23
  : undefined;
25
24
  });
26
25
 
@@ -28,6 +27,7 @@ const { setPreview, closePreview } = usePreview();
28
27
 
29
28
  const context: ProseContext = {
30
29
  appElements,
30
+ setHtmlIds,
31
31
  mode: runtimeConfig.public.eruditMode as EruditMode,
32
32
  languageCode: ERUDIT.config.language.current,
33
33
  formatText,
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import { isMyIcon, type MyIconName } from '#my-icons';
3
- import { computed, ref, watch } from 'vue';
3
+ import { computed, nextTick, ref, watch } from 'vue';
4
4
 
5
5
  const { url } = defineProps<{ url?: MyIconName | (string & {}) }>();
6
6
 
@@ -77,7 +77,7 @@ onMounted(() => {
77
77
  text-(--_mediaColor) transition-[color,background]"
78
78
  >
79
79
  <MyIcon
80
- @vue:mounted="loaded = true"
80
+ @vue:mounted="nextTick(() => (loaded = true))"
81
81
  :name="url as MyIconName"
82
82
  class="h-2/3 w-2/3"
83
83
  />
@@ -87,13 +87,13 @@ onMounted(() => {
87
87
  v-if="mediaType === 'image'"
88
88
  :src="url"
89
89
  loading="lazy"
90
- @load="loaded = true"
90
+ @load="nextTick(() => (loaded = true))"
91
91
  class="absolute block h-full w-full object-cover"
92
92
  />
93
93
  <video
94
94
  v-else-if="mediaType === 'video'"
95
95
  :src="url"
96
- @loadeddata="loaded = true"
96
+ @loadeddata="nextTick(() => (loaded = true))"
97
97
  autoplay
98
98
  muted
99
99
  loop
@@ -28,11 +28,8 @@ async function bookChange() {
28
28
  const fetchPayload = async (): Promise<BookPayloadValue> => {
29
29
  return {
30
30
  shortBookId: shortBookId.value!,
31
- frontNav: await $fetch(
31
+ frontNav: await fetchJson(
32
32
  `/api/aside/major/frontNav/book/${shortBookId.value}`,
33
- {
34
- responseType: 'json',
35
- },
36
33
  ),
37
34
  };
38
35
  };
@@ -138,7 +138,30 @@ onMounted(() => {
138
138
  elements.forEach(({ el }) => observer.observe(el));
139
139
 
140
140
  /**
141
- * 6️⃣ Cleanup
141
+ * 6️⃣ Re-sync active heading when ?element navigation jumps past headings.
142
+ * IntersectionObserver only fires on gradual scrolls; programmatic
143
+ * scrollIntoView skips headings without triggering intersection events.
144
+ */
145
+ const route = useRoute();
146
+ watch(
147
+ () => route.query.element,
148
+ async (elementId) => {
149
+ if (!elementId) return;
150
+ // Wait for scrollIntoView to finish (it's synchronous but DOM
151
+ // layout needs a tick + frame to reflect the new scrollY)
152
+ await nextTick();
153
+ requestAnimationFrame(() => {
154
+ lastAboveViewportId = findLastHeadingAboveViewport(elements);
155
+ visibleIds.clear();
156
+ activeHeadingIds.value = lastAboveViewportId
157
+ ? new Set([lastAboveViewportId])
158
+ : new Set();
159
+ });
160
+ },
161
+ );
162
+
163
+ /**
164
+ * 7️⃣ Cleanup
142
165
  */
143
166
  onUnmounted(() => {
144
167
  observer.disconnect();
@@ -4,6 +4,7 @@ import { headingSchema } from '@erudit-js/prose/elements/heading/core';
4
4
 
5
5
  const { item, level } = defineProps<{ item: ResolvedTocItem; level: number }>();
6
6
 
7
+ const route = useRoute();
7
8
  const elementIcon = await (async () => {
8
9
  let schemaName =
9
10
  item.type === 'heading' ? headingSchema.name : item.schemaName;
@@ -57,7 +58,7 @@ onMounted(() => {
57
58
  :icon="elementIcon"
58
59
  :main="formatText(item.title)"
59
60
  :state="active ? 'active' : undefined"
60
- :to="'#' + item.elementId"
61
+ :to="route.path + '?element=' + item.elementId"
61
62
  />
62
63
  <div
63
64
  v-if="item.type === 'heading'"
@@ -42,9 +42,7 @@ async function fetchNews(index: number) {
42
42
  newsLoading.value = true;
43
43
 
44
44
  try {
45
- const newsBatch = await $fetch<NewsBatch>(`/api/news/batch/${index}`, {
46
- responseType: 'json',
47
- });
45
+ const newsBatch = await fetchJson<NewsBatch>(`/api/news/batch/${index}`);
48
46
 
49
47
  if (index === 0) {
50
48
  newsTotal.value = newsBatch.total!;
@@ -1,6 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { walkElements } from '@jsprose/core';
3
-
2
+ import { walkPreSync } from 'tsprose';
4
3
  import RenderNewsElement from './RenderNewsElement.vue';
5
4
 
6
5
  const { item, isNew } = defineProps<{
@@ -26,7 +25,7 @@ const dateOptions: Intl.DateTimeFormatOptions = {
26
25
  const formattedTitle = itemDate.toLocaleDateString(undefined, dateOptions);
27
26
 
28
27
  // Prepopulate storage values in order not to fuck with provide/inject shit
29
- await walkElements(item.content.proseElement, async (element) => {
28
+ walkPreSync(item.content.prose, (element) => {
30
29
  if (element.storageKey) {
31
30
  const storageKey = element.storageKey;
32
31
  if (item.content.storage[storageKey] !== undefined) {
@@ -63,7 +62,7 @@ await walkElements(item.content.proseElement, async (element) => {
63
62
  }"
64
63
  class="gap-small flex flex-col text-[0.95em]"
65
64
  >
66
- <RenderNewsElement :element="item.content.proseElement" />
65
+ <RenderNewsElement :element="item.content.prose" />
67
66
  </div>
68
67
  </div>
69
68
  </template>
@@ -1,11 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import {
3
- inlinersSchema,
4
- mixSchema,
5
- textSchema,
6
- type AnySchema,
7
- type ProseElement,
8
- } from '@jsprose/core';
2
+ import { mixSchema, textSchema, type ProseElement } from 'tsprose';
9
3
  import { paragraphSchema } from '@erudit-js/prose/elements/paragraph/core';
10
4
 
11
5
  import Mix from './elements/Mix.vue';
@@ -14,10 +8,10 @@ import Text from './elements/Text.vue';
14
8
  import { refSchema } from '@erudit-js/prose/elements/link/reference/core';
15
9
  import Ref from './elements/Ref.vue';
16
10
 
17
- const { element } = defineProps<{ element: ProseElement<AnySchema> }>();
11
+ const { element } = defineProps<{ element: ProseElement }>();
18
12
 
19
13
  const ElementComponent = (() => {
20
- switch (element.schemaName) {
14
+ switch (element.schema.name) {
21
15
  case textSchema.name:
22
16
  return Text;
23
17
  case paragraphSchema.name:
@@ -25,13 +19,12 @@ const ElementComponent = (() => {
25
19
  case refSchema.name:
26
20
  return Ref;
27
21
  case mixSchema.name:
28
- case inlinersSchema.name:
29
22
  return Mix;
30
23
  default:
31
24
  return h(
32
25
  'span',
33
26
  { class: 'text-red-500 font-semibold font-mono' },
34
- `<${element.schemaName} />`,
27
+ `<${element.schema.name} />`,
35
28
  );
36
29
  }
37
30
  })();
@@ -1,9 +1,8 @@
1
1
  <script lang="ts" setup>
2
- import type { mixSchema, ProseElement } from '@jsprose/core';
3
-
2
+ import type { MixSchema, ToProseElement } from 'tsprose';
4
3
  import RenderNewsElement from '../RenderNewsElement.vue';
5
4
 
6
- defineProps<{ element: ProseElement<typeof mixSchema> }>();
5
+ defineProps<{ element: ToProseElement<MixSchema> }>();
7
6
  </script>
8
7
 
9
8
  <template>
@@ -1,10 +1,10 @@
1
1
  <script lang="ts" setup>
2
- import type { ProseElement } from '@jsprose/core';
3
- import type { paragraphSchema } from '@erudit-js/prose/elements/paragraph/core';
2
+ import type { ToProseElement } from 'tsprose';
3
+ import type { ParagraphSchema } from '@erudit-js/prose/elements/paragraph/core';
4
4
 
5
5
  import RenderNewsElement from '../RenderNewsElement.vue';
6
6
 
7
- defineProps<{ element: ProseElement<typeof paragraphSchema> }>();
7
+ defineProps<{ element: ToProseElement<ParagraphSchema> }>();
8
8
  </script>
9
9
 
10
10
  <template>
@@ -1,12 +1,12 @@
1
1
  <script lang="ts" setup>
2
2
  import { onMounted } from 'vue';
3
- import type { ProseElement } from '@jsprose/core';
4
- import type { refSchema } from '@erudit-js/prose/elements/link/reference/core';
3
+ import type { ToProseElement } from 'tsprose';
4
+ import type { RefSchema } from '@erudit-js/prose/elements/link/reference/core';
5
5
  import type { LinkStorage } from '@erudit-js/prose/elements/link/storage';
6
6
 
7
7
  const withBaseUrl = useBaseUrl();
8
8
  const { closePreview, setPreview } = usePreview();
9
- const { element } = defineProps<{ element: ProseElement<typeof refSchema> }>();
9
+ const { element } = defineProps<{ element: ToProseElement<RefSchema> }>();
10
10
  const linkStorage = (element as any).storageValue as LinkStorage;
11
11
 
12
12
  const doubleClick = {
@@ -1,8 +1,8 @@
1
1
  <script lang="ts" setup>
2
2
  import { Text, h } from 'vue';
3
- import type { ProseElement, textSchema } from '@jsprose/core';
3
+ import type { TextSchema, ToProseElement } from 'tsprose';
4
4
 
5
- const { element } = defineProps<{ element: ProseElement<typeof textSchema> }>();
5
+ const { element } = defineProps<{ element: ToProseElement<TextSchema> }>();
6
6
 
7
7
  const originalText = element.data;
8
8
  const leadingSpace = originalText.match(/^(\s*)/)?.[1] ? ' ' : '';
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  const { child } = defineProps<{ child: MainContentChildrenItem }>();
3
3
 
4
- const hasExtra = child.stats || child.quickLinks;
4
+ const hasExtra = child.stats || child.keyLinks;
5
5
  </script>
6
6
 
7
7
  <template>
@@ -33,8 +33,8 @@ const hasExtra = child.stats || child.quickLinks;
33
33
  v-if="hasExtra"
34
34
  class="border-t-border p-normal gap-normal flex flex-col border-t"
35
35
  >
36
- <div v-if="child.quickLinks" class="relative top-[1px]">
37
- <MainQuickLinks :elementSnippets="child.quickLinks" mode="children" />
36
+ <div v-if="child.keyLinks" class="relative top-[1px]">
37
+ <MainKeyLinks :elementSnippets="child.keyLinks" mode="children" />
38
38
  </div>
39
39
  <div v-if="child.stats">
40
40
  <MainContentStats :stats="child.stats" mode="children" />
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import { autoUpdate, shift } from '@floating-ui/vue';
3
3
 
4
- const { quickLink } = defineProps<{ quickLink: ElementSnippet }>();
4
+ const { keyLink } = defineProps<{ keyLink: ElementSnippet }>();
5
5
 
6
6
  const containerElement = useTemplateRef('container');
7
7
  const toggleElement = useTemplateRef('toggle');
@@ -21,21 +21,21 @@ const { popupVisible, popupStyles } = usePopup(
21
21
  },
22
22
  );
23
23
 
24
- const elementIcon = await getElementIcon(quickLink.schemaName);
24
+ const elementIcon = await getElementIcon(keyLink.schemaName);
25
25
 
26
26
  const title = computed(() => {
27
- if (quickLink.quick?.title) {
28
- return quickLink.quick.title;
27
+ if (keyLink.key?.title) {
28
+ return keyLink.key.title;
29
29
  } else {
30
- return quickLink.title;
30
+ return keyLink.title;
31
31
  }
32
32
  });
33
33
 
34
34
  const description = computed(() => {
35
- if (quickLink.quick?.description) {
36
- return quickLink.quick.description;
35
+ if (keyLink.key?.description) {
36
+ return keyLink.key.description;
37
37
  } else {
38
- return quickLink.description;
38
+ return keyLink.description;
39
39
  }
40
40
  });
41
41
  </script>
@@ -44,11 +44,11 @@ const description = computed(() => {
44
44
  <div ref="container">
45
45
  <div ref="toggle">
46
46
  <EruditLink
47
- :to="quickLink.link"
47
+ :to="keyLink.link"
48
48
  class="gap-small border-border px-small text-text-muted text-main-sm
49
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]"
50
+ items-center rounded border bg-(--keyBg) py-1 ring-2 ring-transparent
51
+ transition-[color,border,box-shadow]"
52
52
  >
53
53
  <MaybeMyIcon :name="elementIcon" class="-mr-0.5 text-[1.2em]" />
54
54
  <span>{{ formatText(title) }}</span>
@@ -4,12 +4,12 @@ const { elementSnippets } = defineProps<{
4
4
  elementSnippets?: ElementSnippet[];
5
5
  }>();
6
6
 
7
- const quickLinks = (() => {
7
+ const keyLinks = (() => {
8
8
  if (!elementSnippets) {
9
9
  return;
10
10
  }
11
11
 
12
- const filtered = elementSnippets.filter((snippet) => !!snippet.quick);
12
+ const filtered = elementSnippets.filter((snippet) => !!snippet.key);
13
13
 
14
14
  return filtered.length > 0 ? filtered : undefined;
15
15
  })();
@@ -18,23 +18,23 @@ const phrase = await usePhrases('key_elements');
18
18
  </script>
19
19
 
20
20
  <template>
21
- <template v-if="quickLinks">
21
+ <template v-if="keyLinks">
22
22
  <section v-if="mode === 'single'" class="px-main py-main-half">
23
23
  <MainSubTitle :title="phrase.key_elements + ':'" />
24
24
  <div
25
- :style="{ '--quickBg': 'var(--color-bg-aside)' }"
25
+ :style="{ '--keyBg': 'var(--color-bg-aside)' }"
26
26
  class="gap-small micro:gap-normal micro:justify-start flex flex-wrap
27
27
  justify-center"
28
28
  >
29
- <MainQuickLink v-for="quickLink of quickLinks" :quickLink />
29
+ <MainKeyLink v-for="keyLink of keyLinks" :keyLink />
30
30
  </div>
31
31
  </section>
32
32
  <div
33
33
  v-else
34
- :style="{ '--quickBg': 'var(--color-bg-main)' }"
34
+ :style="{ '--keyBg': 'var(--color-bg-main)' }"
35
35
  class="gap-small text-main-sm flex flex-wrap"
36
36
  >
37
- <MainQuickLink v-for="quickLink of quickLinks" :quickLink />
37
+ <MainKeyLink v-for="keyLink of keyLinks" :keyLink />
38
38
  </div>
39
39
  </template>
40
40
  </template>
@@ -6,7 +6,7 @@ const payloadKey = 'quote-ids';
6
6
  const payloadValue: QuoteIds =
7
7
  (nuxtApp.static.data[payloadKey] ||=
8
8
  nuxtApp.payload.data[payloadKey] ||=
9
- await $fetch('/api/quote/ids', { responseType: 'json' }));
9
+ await fetchJson('/api/quote/ids'));
10
10
 
11
11
  // Calculate total number of quotes
12
12
  const totalQuotes = computed(() => {
@@ -56,9 +56,7 @@ async function nextQuote() {
56
56
 
57
57
  const nextQuoteId = getRandomQuoteId();
58
58
  currentQuoteId.value = nextQuoteId;
59
- const nextQuote = await $fetch(`/api/quote/data/${nextQuoteId}`, {
60
- responseType: 'json',
61
- });
59
+ const nextQuote = await fetchJson<Quote>(`/api/quote/data/${nextQuoteId}`);
62
60
  quote.value = nextQuote;
63
61
  quoteKey.value++;
64
62
 
@@ -10,7 +10,7 @@ const hasPreamble = computed(() => {
10
10
  let hasKeyElements = false;
11
11
  if (mainContent.type === 'topic' || mainContent.type === 'page') {
12
12
  hasKeyElements =
13
- mainContent.snippets?.some((snippet) => !!snippet.quick) ?? false;
13
+ mainContent.snippets?.some((snippet) => !!snippet.key) ?? false;
14
14
  }
15
15
 
16
16
  return (
@@ -11,13 +11,17 @@ const { mainContent } = defineProps<{ mainContent: MainContent }>();
11
11
  :description="mainContent.description"
12
12
  class="text-main!"
13
13
  />
14
- <MainQuickLinks
14
+ <MainKeyLinks
15
15
  v-if="mainContent.type === 'topic' || mainContent.type === 'page'"
16
16
  mode="single"
17
17
  :elementSnippets="mainContent.snippets"
18
18
  />
19
19
  <MainConnections mode="single" :connections="mainContent.connections" />
20
- <MainContentStats mode="single" :stats="mainContent.stats" />
20
+ <MainContentStats
21
+ mode="single"
22
+ :stats="mainContent.stats"
23
+ :contentRelativePath="mainContent.contentRelativePath"
24
+ />
21
25
  <div class="h-main-half"></div>
22
26
  </div>
23
27
  </template>
@@ -57,9 +57,13 @@ await useContentSeo({
57
57
  <MainTitle :icon="ICONS[mainContent.part]" :title="mainContent.title" />
58
58
  <MainFlags :flags="mainContent.flags" />
59
59
  <MainDescription :description="mainContent.description" />
60
- <MainQuickLinks mode="single" :elementSnippets="mainContent.snippets" />
60
+ <MainKeyLinks mode="single" :elementSnippets="mainContent.snippets" />
61
61
  <MainConnections :connections="mainContent.connections" />
62
- <MainContentStats mode="single" :stats="mainContent.stats" />
62
+ <MainContentStats
63
+ mode="single"
64
+ :stats="mainContent.stats"
65
+ :contentRelativePath="mainContent.contentRelativePath"
66
+ />
63
67
  <div class="h-main-half"></div>
64
68
  <MainQuoteLoader />
65
69
  <div class="h-main-half"></div>
@@ -75,10 +79,11 @@ await useContentSeo({
75
79
  </template>
76
80
  <template #default>
77
81
  <Prose
78
- :element="mainContent.proseElement"
82
+ :element="mainContent.prose"
79
83
  :storage="mainContent.storage"
80
84
  :urlPath="'/' + mainContent.fullId"
81
85
  :useHashUrl="true"
86
+ :setHtmlIds="true"
82
87
  @vue:mounted="proseMounted"
83
88
  />
84
89
  </template>
@@ -0,0 +1,45 @@
1
+ <script lang="ts" setup>
2
+ import type { MaybeMyIconName } from '#my-icons';
3
+
4
+ const props = defineProps<{
5
+ type: 'dependency' | 'dependent';
6
+ unique: ContentDepUnique;
7
+ }>();
8
+
9
+ const loadingSvg = useLoadingSvg();
10
+
11
+ const iconKey = ref(0);
12
+ const icon = ref<MaybeMyIconName>(loadingSvg);
13
+
14
+ watch(icon, () => {
15
+ iconKey.value += 1;
16
+ });
17
+
18
+ onMounted(async () => {
19
+ try {
20
+ icon.value = await getElementIcon(props.unique.schemaName);
21
+ } catch {
22
+ icon.value = '__missing';
23
+ }
24
+ });
25
+ </script>
26
+
27
+ <template>
28
+ <div class="gap-small flex items-center">
29
+ <MyIcon
30
+ name="arrow/up-to-right"
31
+ :class="[
32
+ 'text-text-dimmed shrink-0',
33
+ type === 'dependent' && 'relative -top-[3px] -scale-x-100 rotate-90',
34
+ ]"
35
+ />
36
+ <MaybeMyIcon :key="iconKey" :name="icon" class="text-main-sm shrink-0" />
37
+ <EruditLink :to="unique.link" class="text-hover-underline text-main-sm">
38
+ {{ formatText(unique.title ?? unique.name) }}
39
+ <MyIcon
40
+ name="arrow/outward"
41
+ class="text-text-disabled relative -top-1 inline text-[0.8em]"
42
+ />
43
+ </EruditLink>
44
+ </div>
45
+ </template>
@@ -1,7 +1,8 @@
1
- <script setup lang="ts">
1
+ <script lang="ts" setup>
2
2
  import ScrollPane from './ScrollPane.vue';
3
+ import DepUnique from './DepUnique.vue';
3
4
 
4
- defineProps<{ deps: ContentDep[] }>();
5
+ defineProps<{ type: 'dependency' | 'dependent'; deps: ContentDep[] }>();
5
6
  </script>
6
7
 
7
8
  <template>
@@ -24,6 +25,17 @@ defineProps<{ deps: ContentDep[] }>();
24
25
  >
25
26
  {{ formatText(dep.reason) }}
26
27
  </div>
28
+ <div
29
+ v-if="dep.type === 'auto' && dep.uniques?.length"
30
+ class="mt-small flex flex-col gap-0.5"
31
+ >
32
+ <DepUnique
33
+ v-for="unique in dep.uniques"
34
+ :type
35
+ :key="unique.name"
36
+ :unique="unique"
37
+ />
38
+ </div>
27
39
  </div>
28
40
  </div>
29
41
  </ScrollPane>
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script lang="ts" setup>
2
2
  import type { ContentExternals } from '@erudit-js/core/content/externals';
3
3
  import ScrollPane from './ScrollPane.vue';
4
4
 
@@ -82,6 +82,7 @@ const parentExternalsCount = computed(() => {
82
82
  <template v-if="currentType && connections[currentType]">
83
83
  <Deps
84
84
  v-if="currentType !== 'externals'"
85
+ :type="currentType === 'autoDependencies' ? 'dependency' : 'dependent'"
85
86
  :deps="connections[currentType]!"
86
87
  />
87
88
  <Externals v-else :externals="connections[currentType]!" />
@@ -0,0 +1,68 @@
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);
9
+
10
+ const dateOptions: Intl.DateTimeFormatOptions = {
11
+ year: 'numeric',
12
+ month: 'short',
13
+ day: 'numeric',
14
+ };
15
+
16
+ const isWithinThreeMonths = computed(() => {
17
+ if (!date.value) return false;
18
+ const now = new Date();
19
+ const threeMonthsAgo = new Date(now);
20
+ threeMonthsAgo.setMonth(now.getMonth() - 3);
21
+ return date.value >= threeMonthsAgo && date.value <= now;
22
+ });
23
+
24
+ const formattedTitle = computed(() =>
25
+ date.value ? date.value.toLocaleDateString(undefined, dateOptions) : '',
26
+ );
27
+
28
+ 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
+ </script>
51
+
52
+ <template>
53
+ <div
54
+ v-if="date"
55
+ class="gap-small px-small text-main-sm border-border bg-bg-aside flex
56
+ items-center rounded-xl border py-1"
57
+ >
58
+ <MyIcon name="update" class="text-text-dimmed -mr-0.5 text-[1.2em]" />
59
+ <span class="text-text-muted">{{ phrase.updated }}</span>
60
+ <NuxtTime
61
+ :datetime="date"
62
+ v-bind="dateOptions"
63
+ :relative="isWithinThreeMonths"
64
+ :title="formattedTitle"
65
+ class="text-text-muted cursor-help font-bold"
66
+ />
67
+ </div>
68
+ </template>