erudit 4.1.1 → 4.2.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.
- package/app/assets/icons/update.svg +3 -0
- package/app/components/Prose.vue +7 -7
- package/app/components/SmartMedia.vue +4 -4
- package/app/components/aside/major/contentNav/PaneBookNav.vue +1 -4
- package/app/components/aside/minor/content/Toc.vue +24 -1
- package/app/components/aside/minor/content/TocItem.vue +2 -1
- package/app/components/aside/minor/news/AsideMinorNews.vue +2 -4
- package/app/components/aside/minor/news/NewsItem.vue +3 -4
- package/app/components/aside/minor/news/RenderNewsElement.vue +4 -11
- package/app/components/aside/minor/news/elements/Mix.vue +2 -3
- package/app/components/aside/minor/news/elements/P.vue +3 -3
- package/app/components/aside/minor/news/elements/Ref.vue +3 -3
- package/app/components/aside/minor/news/elements/Text.vue +2 -2
- package/app/components/main/MainContentChild.vue +3 -3
- package/app/components/main/{MainQuickLink.vue → MainKeyLink.vue} +11 -11
- package/app/components/main/{MainQuickLinks.vue → MainKeyLinks.vue} +7 -7
- package/app/components/main/MainQuoteLoader.vue +2 -4
- package/app/components/main/MainStickyHeader.vue +1 -1
- package/app/components/main/MainStickyHeaderPreamble.vue +6 -2
- package/app/components/main/MainTopicPartPage.vue +8 -3
- package/app/components/main/connections/DepUnique.vue +45 -0
- package/app/components/main/connections/Deps.vue +14 -2
- package/app/components/main/connections/Externals.vue +1 -1
- package/app/components/main/connections/MainConnections.vue +1 -0
- package/app/components/main/contentStats/ItemLastChanged.vue +68 -0
- package/app/components/main/contentStats/MainContentStats.vue +36 -28
- package/app/components/preview/PreviewScreen.vue +2 -2
- package/app/components/preview/screen/ContentPage.vue +1 -4
- package/app/components/preview/screen/Unique.vue +3 -5
- package/app/composables/appElements.ts +2 -4
- package/app/composables/asideMajorPane.ts +3 -3
- package/app/composables/fetchJson.ts +4 -0
- package/app/composables/lastChanged.ts +28 -0
- package/app/composables/mainContent.ts +1 -4
- package/app/composables/og.ts +43 -35
- package/app/composables/phrases.ts +2 -3
- package/app/composables/scrollUp.ts +1 -1
- package/app/pages/book/[...bookId].vue +5 -1
- package/app/pages/contributor/[contributorId].vue +3 -5
- package/app/pages/contributors.vue +1 -1
- package/app/pages/group/[...groupId].vue +5 -1
- package/app/pages/index.vue +1 -1
- package/app/pages/page/[...pageId].vue +8 -3
- package/app/pages/sponsors.vue +1 -1
- package/app/plugins/appSetup/index.ts +0 -5
- package/app/plugins/fetchJson.ts +11 -0
- package/app/plugins/prerender.server.ts +1 -1
- package/app/router.options.ts +1 -1
- package/modules/erudit/globals/prose.ts +3 -4
- package/modules/erudit/setup/elements/appTemplate.ts +6 -7
- package/modules/erudit/setup/elements/{globalTypes.ts → elementGlobalTypes.ts} +21 -21
- package/modules/erudit/setup/elements/globalTemplate.ts +29 -23
- package/modules/erudit/setup/elements/setup.ts +18 -16
- package/modules/erudit/setup/elements/shared.ts +2 -2
- package/modules/erudit/setup/elements/tagsTable.ts +1 -1
- package/modules/erudit/setup/runtimeConfig.ts +2 -0
- package/nuxt.config.ts +2 -2
- package/package.json +14 -13
- package/server/api/main/content/[...contentTypePath].ts +5 -4
- package/server/api/prerender/content.ts +1 -3
- package/server/api/preview/contentPage/[...contentTypePath].ts +1 -2
- package/server/api/preview/contentUnique/[...contentTypePathUnique].ts +16 -31
- package/server/api/problemScript/[...problemScriptPath].ts +81 -4
- package/server/erudit/content/global/build.ts +64 -10
- package/server/erudit/content/nav/build.ts +4 -4
- package/server/erudit/content/repository/children.ts +3 -3
- package/server/erudit/content/repository/deps.ts +110 -13
- package/server/erudit/content/repository/elementSnippets.ts +16 -16
- package/server/erudit/content/repository/stats.ts +30 -22
- package/server/erudit/content/repository/topicParts.ts +1 -1
- package/server/erudit/content/repository/unique.ts +14 -15
- package/server/erudit/content/resolve/page.ts +15 -35
- package/server/erudit/content/resolve/topic.ts +33 -164
- package/server/erudit/content/resolve/utils/insertContentResolved.ts +59 -31
- package/server/erudit/content/search.ts +5 -22
- package/server/erudit/contributors/build.ts +7 -8
- package/server/erudit/db/repository/pushFile.ts +10 -3
- package/server/erudit/db/repository/pushProblemScript.ts +14 -3
- package/server/erudit/db/schema/contentDeps.ts +3 -0
- package/server/erudit/db/schema/contentSnippets.ts +3 -3
- package/server/erudit/db/schema/contentUniques.ts +2 -2
- package/server/erudit/db/schema/contributors.ts +2 -2
- package/server/erudit/db/schema/news.ts +2 -2
- package/server/erudit/db/schema/pages.ts +2 -2
- package/server/erudit/db/schema/topics.ts +4 -4
- package/server/erudit/global.ts +4 -0
- package/server/erudit/importer.ts +16 -8
- package/server/erudit/index.ts +0 -3
- package/server/erudit/language/list/en.ts +1 -0
- package/server/erudit/language/list/ru.ts +1 -0
- package/server/erudit/news/build.ts +6 -6
- package/server/erudit/news/repository/batch.ts +2 -2
- package/server/erudit/prose/repository/finalize.ts +22 -25
- package/server/erudit/prose/repository/get.ts +3 -5
- package/server/erudit/prose/repository/rawToProse.ts +31 -0
- package/server/erudit/prose/storage/callout.ts +9 -7
- package/server/erudit/prose/storage/image.ts +8 -11
- package/server/erudit/prose/storage/link.ts +24 -32
- package/server/erudit/prose/storage/problemScript.ts +8 -14
- package/server/erudit/prose/storage/video.ts +9 -7
- package/server/erudit/repository.ts +4 -4
- package/server/routes/file/[...path].ts +1 -1
- package/shared/types/contentChildren.ts +5 -2
- package/shared/types/contentConnections.ts +9 -0
- package/shared/types/elementSnippet.ts +1 -1
- package/shared/types/indexPage.ts +3 -0
- package/shared/types/language.ts +1 -83
- package/shared/types/mainContent.ts +11 -5
- package/shared/types/news.ts +2 -2
- package/shared/types/preview.ts +3 -2
- package/shared/types/runtimeConfig.ts +1 -0
- package/shared/types/search.ts +2 -0
- package/shared/utils/pages.ts +4 -2
- package/shared/utils/stringColor.ts +16 -6
- package/server/erudit/prose/repository/resolve.ts +0 -17
- 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>
|
package/app/components/Prose.vue
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import type {
|
|
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
|
|
10
|
-
storage:
|
|
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.
|
|
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
|
|
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️⃣
|
|
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="'
|
|
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
|
|
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!;
|
|
@@ -100,7 +98,7 @@ const phrase = await usePhrases('news', 'no_news', 'show_more');
|
|
|
100
98
|
<div class="flex h-full w-full flex-col">
|
|
101
99
|
<AsideMinorPlainHeader
|
|
102
100
|
icon="bell"
|
|
103
|
-
:title="phrase
|
|
101
|
+
:title="phrase.news"
|
|
104
102
|
:count="newsTotal === 0 ? undefined : newsTotal"
|
|
105
103
|
/>
|
|
106
104
|
<section v-if="newsItems.length === 0">
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {
|
|
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
|
-
|
|
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.
|
|
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
|
|
11
|
+
const { element } = defineProps<{ element: ProseElement }>();
|
|
18
12
|
|
|
19
13
|
const ElementComponent = (() => {
|
|
20
|
-
switch (element.
|
|
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.
|
|
27
|
+
`<${element.schema.name} />`,
|
|
35
28
|
);
|
|
36
29
|
}
|
|
37
30
|
})();
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type { MixSchema, ToProseElement } from 'tsprose';
|
|
4
3
|
import RenderNewsElement from '../RenderNewsElement.vue';
|
|
5
4
|
|
|
6
|
-
defineProps<{ element:
|
|
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 {
|
|
3
|
-
import type {
|
|
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:
|
|
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 {
|
|
4
|
-
import type {
|
|
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:
|
|
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 {
|
|
3
|
+
import type { TextSchema, ToProseElement } from 'tsprose';
|
|
4
4
|
|
|
5
|
-
const { element } = defineProps<{ element:
|
|
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.
|
|
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.
|
|
37
|
-
<
|
|
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 {
|
|
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(
|
|
24
|
+
const elementIcon = await getElementIcon(keyLink.schemaName);
|
|
25
25
|
|
|
26
26
|
const title = computed(() => {
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
27
|
+
if (keyLink.key?.title) {
|
|
28
|
+
return keyLink.key.title;
|
|
29
29
|
} else {
|
|
30
|
-
return
|
|
30
|
+
return keyLink.title;
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
const description = computed(() => {
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
35
|
+
if (keyLink.key?.description) {
|
|
36
|
+
return keyLink.key.description;
|
|
37
37
|
} else {
|
|
38
|
-
return
|
|
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="
|
|
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-(--
|
|
51
|
-
|
|
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
|
|
7
|
+
const keyLinks = (() => {
|
|
8
8
|
if (!elementSnippets) {
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const filtered = elementSnippets.filter((snippet) => !!snippet.
|
|
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="
|
|
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="{ '--
|
|
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
|
-
<
|
|
29
|
+
<MainKeyLink v-for="keyLink of keyLinks" :keyLink />
|
|
30
30
|
</div>
|
|
31
31
|
</section>
|
|
32
32
|
<div
|
|
33
33
|
v-else
|
|
34
|
-
:style="{ '--
|
|
34
|
+
:style="{ '--keyBg': 'var(--color-bg-main)' }"
|
|
35
35
|
class="gap-small text-main-sm flex flex-wrap"
|
|
36
36
|
>
|
|
37
|
-
<
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
<
|
|
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
|
|
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
|
-
<
|
|
60
|
+
<MainKeyLinks mode="single" :elementSnippets="mainContent.snippets" />
|
|
61
61
|
<MainConnections :connections="mainContent.connections" />
|
|
62
|
-
<MainContentStats
|
|
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.
|
|
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
|
|
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>
|
|
@@ -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>
|