erudit 4.3.5-dev.1 → 4.3.5

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.
@@ -0,0 +1,42 @@
1
+ <script lang="ts" setup>
2
+ const { src } = defineProps<{
3
+ src: string;
4
+ }>();
5
+ </script>
6
+
7
+ <template>
8
+ <div :class="$style.imageTint">
9
+ <img :src />
10
+ <img :src />
11
+ </div>
12
+ </template>
13
+
14
+ <style module>
15
+ .imageTint {
16
+ position: relative;
17
+ overflow: hidden;
18
+ isolation: isolate;
19
+
20
+ img {
21
+ width: 100%;
22
+ }
23
+
24
+ img:first-child {
25
+ mix-blend-mode: multiply;
26
+ }
27
+
28
+ img:last-child {
29
+ position: absolute;
30
+ inset: 0;
31
+ z-index: -1;
32
+ filter: drop-shadow(
33
+ 0px 1000px 0
34
+ var(--tint, color-mix(in hsl, var(--color-brand), transparent 90%))
35
+ );
36
+ transform: translateY(-1000px);
37
+ opacity: var(--tint-opacity, 1);
38
+ transition: opacity var(--default-transition-timing-function)
39
+ var(--default-transition-duration);
40
+ }
41
+ }
42
+ </style>
@@ -10,30 +10,40 @@ const hasExtra = child.stats || child.keyLinks;
10
10
  dark:bg-bg-aside rounded border bg-neutral-100 ring-2 ring-transparent
11
11
  transition-[border,box-shadow]"
12
12
  >
13
+ <EruditLink
14
+ v-if="child.decoration"
15
+ :to="child.link"
16
+ class="p-normal max-micro:pb-0 not-group-hocus:[--tint-opacity:0]
17
+ micro:float-right block"
18
+ >
19
+ <ImageTint
20
+ :src="fileUrl(child.decoration)"
21
+ class="m-auto block aspect-square max-h-[90px] max-w-[90px]"
22
+ />
23
+ </EruditLink>
24
+
13
25
  <EruditLink :to="child.link" class="p-normal gap-small flex flex-col">
14
- <div class="gap-small micro:gap-normal flex items-center">
26
+ <h2
27
+ class="group-hocus:text-brand micro:text-main-lg micro:text-start
28
+ text-center font-bold transition-[color]"
29
+ >
15
30
  <MyIcon
16
31
  :name="ICONS[child.type]"
17
- class="text-text-muted group-hocus:text-brand shrink-0 text-[1.2em]
18
- transition-[color]"
32
+ class="text-text-muted group-hocus:text-brand mr-small relative
33
+ -top-[1px] inline shrink-0 text-[1.2em] transition-[color]"
19
34
  />
20
- <h2
21
- class="group-hocus:text-brand gap-small micro:text-main-lg flex
22
- items-center font-bold transition-[color]"
23
- >
24
- {{ formatText(child.title) }}
25
- </h2>
26
- </div>
35
+ <span>{{ formatText(child.title) }}</span>
36
+ </h2>
27
37
 
28
- <div v-if="child.description" class="text-text-muted">
38
+ <div
39
+ v-if="child.description"
40
+ class="text-text-muted micro:text-start text-center"
41
+ >
29
42
  {{ formatText(child.description) }}
30
43
  </div>
31
44
  </EruditLink>
32
- <div
33
- v-if="hasExtra"
34
- class="border-t-border p-normal gap-normal flex flex-col border-t"
35
- >
36
- <div v-if="child.keyLinks" class="relative top-[1px]">
45
+ <div v-if="hasExtra" class="px-normal pb-normal gap-normal flex flex-col">
46
+ <div v-if="child.keyLinks">
37
47
  <MainKeyLinks :elementSnippets="child.keyLinks" mode="children" />
38
48
  </div>
39
49
  <div v-if="child.stats">
@@ -1,15 +1,29 @@
1
1
  <script lang="ts" setup>
2
- defineProps<{ decoration?: string }>();
2
+ defineProps<{
3
+ role: 'preamble-float' | 'preamble-static';
4
+ decoration?: string;
5
+ }>();
3
6
  </script>
4
7
 
5
8
  <template>
6
- <section
7
- v-if="decoration"
8
- class="pl-small pb-small pt-main pr-main float-right max-[700px]:hidden"
9
- >
10
- <img
11
- :src="fileUrl(decoration!)"
12
- class="block aspect-square max-h-[125px] w-full max-w-[125px]"
13
- />
14
- </section>
9
+ <template v-if="decoration">
10
+ <section
11
+ v-if="role === 'preamble-float'"
12
+ class="p-small pr-main float-right pt-3 max-[700px]:hidden"
13
+ >
14
+ <ImageTint
15
+ :src="fileUrl(decoration)"
16
+ class="block aspect-square max-h-[125px] max-w-[125px]"
17
+ />
18
+ </section>
19
+ <section
20
+ v-else-if="role === 'preamble-static'"
21
+ class="px-main py-main-half min-[700px]:hidden"
22
+ >
23
+ <ImageTint
24
+ :src="fileUrl(decoration)"
25
+ class="micro:m-0 m-auto block aspect-square max-h-[125px] max-w-[125px]"
26
+ />
27
+ </section>
28
+ </template>
15
29
  </template>
@@ -38,7 +38,8 @@ const phrase = await usePhrases('key_elements');
38
38
  <div
39
39
  v-else
40
40
  :style="{ '--keyBg': 'var(--color-bg-main)' }"
41
- class="gap-small text-main-sm flex flex-wrap"
41
+ class="gap-small text-main-sm micro:justify-start flex flex-wrap
42
+ justify-center"
42
43
  >
43
44
  <MainKeyLink v-for="keyLink of keyLinks" :keyLink />
44
45
  </div>
@@ -5,27 +5,35 @@ const { color = 'var(--color-brand)' } = defineProps<{
5
5
  icon: MaybeMyIconName;
6
6
  title: string;
7
7
  color?: string;
8
+ contentLabel?: string;
8
9
  }>();
9
10
  </script>
10
11
 
11
12
  <template>
12
13
  <section
13
- :style="{ '--titleColor': color }"
14
- class="gap-small micro:gap-normal micro:flex-row micro:justify-start px-main
15
- py-main-half not-first:micro:-my-3 not-first:micro:-top-1.5 relative flex
16
- flex-col items-center justify-center"
14
+ :style="{
15
+ '--titleColor': color,
16
+ '--iconColor':
17
+ 'color-mix(in srgb, var(--titleColor), var(--color-text) 70%)',
18
+ }"
19
+ class="gap-small micro:items-start px-main py-main-half relative flex
20
+ flex-col items-center justify-center leading-none"
17
21
  >
18
22
  <div
19
- class="max-micro:rounded-full max-micro:p-4
20
- max-micro:bg-(--titleColor)/80"
23
+ class="gap-small micro:hidden flex items-center text-[1.1em] font-semibold
24
+ text-(--iconColor)"
21
25
  >
22
- <MaybeMyIcon
23
- :name="icon"
24
- class="max-micro:text-white micro:text-[38px] text-[30px]
25
- text-[color-mix(in_srgb,var(--titleColor),var(--color-text)_70%)]"
26
- />
26
+ <MaybeMyIcon :name="icon" class="text-[1.2em]" />
27
+ <span class="micro:hidden">{{ contentLabel }}</span>
27
28
  </div>
28
29
  <h1 class="text-size-h1 max-micro:text-center">
30
+ <span :title="contentLabel" class="mr-normal cursor-help">
31
+ <MaybeMyIcon
32
+ :name="icon"
33
+ class="max-micro:hidden relative -top-[1px] inline cursor-help
34
+ text-[1.1em] text-(--iconColor)"
35
+ />
36
+ </span>
29
37
  <FancyBold :text="title" :color="color" />
30
38
  </h1>
31
39
  </section>
@@ -18,6 +18,7 @@ async function proseMounted() {
18
18
  }
19
19
 
20
20
  const phrase = await usePhrases(
21
+ 'article',
21
22
  'summary',
22
23
  'practice',
23
24
  'article_seo_description',
@@ -30,21 +31,17 @@ await useContentSeo({
30
31
  title: mainContent.title,
31
32
  bookTitle: mainContent.bookTitle,
32
33
  contentTypeSuffix:
33
- mainContent.part !== 'article' ? phrase[mainContent.part] : undefined,
34
+ mainContent.part !== mainContent.parts[0]
35
+ ? phrase[mainContent.part]
36
+ : undefined,
34
37
  contentTypePath: {
35
38
  type: 'topic',
36
39
  topicPart: mainContent.part,
37
40
  contentId: mainContent.shortId,
38
41
  },
39
42
  description:
40
- mainContent.part === 'article'
41
- ? mainContent.description ||
42
- phrase.article_seo_description(mainContent.title)
43
- : mainContent.part === 'summary'
44
- ? phrase.summary_seo_description(mainContent.title)
45
- : mainContent.part === 'practice'
46
- ? phrase.practice_seo_description(mainContent.title)
47
- : undefined,
43
+ (mainContent.part === mainContent.parts[0] && mainContent.description) ||
44
+ phrase[`${mainContent.part}_seo_description`](mainContent.title),
48
45
  seo: mainContent.seo,
49
46
  snippets: mainContent.snippets,
50
47
  breadcrumbs: mainContent.breadcrumbs,
@@ -57,10 +54,21 @@ await useContentSeo({
57
54
 
58
55
  <template>
59
56
  <MainGlow />
60
- <MainDecoration :decoration="mainContent.decoration" />
61
57
  <MainSectionPreamble>
58
+ <MainDecoration
59
+ role="preamble-float"
60
+ :decoration="mainContent.decoration"
61
+ />
62
62
  <MainBreadcrumbs :breadcrumbs="mainContent.breadcrumbs" />
63
- <MainTitle :icon="ICONS[mainContent.part]" :title="mainContent.title" />
63
+ <MainDecoration
64
+ role="preamble-static"
65
+ :decoration="mainContent.decoration"
66
+ />
67
+ <MainTitle
68
+ :icon="ICONS[mainContent.part]"
69
+ :title="mainContent.title"
70
+ :contentLabel="phrase[mainContent.part]"
71
+ />
64
72
  <MainFlags :flags="mainContent.flags" />
65
73
  <MainDescription :description="mainContent.description" />
66
74
  <MainKeyLinks mode="single" :elementSnippets="mainContent.snippets" />
@@ -38,7 +38,8 @@ const phrase = await usePhrases('stats');
38
38
  </section>
39
39
  <div
40
40
  v-else-if="mode === 'children' && stats"
41
- class="gap-small micro:gap-normal text-main-sm flex flex-wrap"
41
+ class="gap-small micro:gap-normal text-main-sm micro:justify-start flex
42
+ flex-wrap justify-center"
42
43
  >
43
44
  <ItemMaterials
44
45
  v-if="stats.materials"
@@ -18,7 +18,7 @@ if (ERUDIT.config.contributors?.enabled) {
18
18
  showNewsAside();
19
19
  }
20
20
 
21
- const phrase = await usePhrases('begin_learning');
21
+ const phrase = await usePhrases('book', 'begin_learning');
22
22
  const lastChangedDate = useLastChanged(() => mainContent.lastmod);
23
23
 
24
24
  await useContentSeo({
@@ -40,10 +40,21 @@ await useContentSeo({
40
40
 
41
41
  <template>
42
42
  <MainGlow />
43
- <MainDecoration :decoration="mainContent.decoration" />
44
43
  <MainSectionPreamble>
44
+ <MainDecoration
45
+ role="preamble-float"
46
+ :decoration="mainContent.decoration"
47
+ />
45
48
  <MainBreadcrumbs :breadcrumbs="mainContent.breadcrumbs" />
46
- <MainTitle icon="book" :title="mainContent.title" />
49
+ <MainDecoration
50
+ role="preamble-static"
51
+ :decoration="mainContent.decoration"
52
+ />
53
+ <MainTitle
54
+ icon="book"
55
+ :title="mainContent.title"
56
+ :contentLabel="phrase.book"
57
+ />
47
58
  <MainFlags :flags="mainContent.flags" />
48
59
  <MainDescription :description="mainContent.description" />
49
60
  <MainConnections :connections="mainContent.connections" />
@@ -42,10 +42,21 @@ await useContentSeo({
42
42
 
43
43
  <template>
44
44
  <MainGlow />
45
- <MainDecoration :decoration="mainContent.decoration" />
46
45
  <MainSectionPreamble>
46
+ <MainDecoration
47
+ role="preamble-float"
48
+ :decoration="mainContent.decoration"
49
+ />
47
50
  <MainBreadcrumbs :breadcrumbs="mainContent.breadcrumbs" />
48
- <MainTitle icon="folder-open" :title="mainContent.title" />
51
+ <MainDecoration
52
+ role="preamble-static"
53
+ :decoration="mainContent.decoration"
54
+ />
55
+ <MainTitle
56
+ icon="folder-open"
57
+ :title="mainContent.title"
58
+ :contentLabel="phrase.group"
59
+ />
49
60
  <MainFlags :flags="mainContent.flags" />
50
61
  <MainDescription :description="mainContent.description" />
51
62
  <MainConnections :connections="mainContent.connections" />
@@ -35,14 +35,27 @@ await useContentSeo({
35
35
  flags: mainContent.flags,
36
36
  connections: mainContent.connections,
37
37
  });
38
+
39
+ const phrase = await usePhrases('page');
38
40
  </script>
39
41
 
40
42
  <template>
41
43
  <MainGlow />
42
- <MainDecoration :decoration="mainContent.decoration" />
43
44
  <MainSectionPreamble>
45
+ <MainDecoration
46
+ role="preamble-float"
47
+ :decoration="mainContent.decoration"
48
+ />
44
49
  <MainBreadcrumbs :breadcrumbs="mainContent.breadcrumbs" />
45
- <MainTitle icon="lines" :title="mainContent.title" />
50
+ <MainDecoration
51
+ role="preamble-static"
52
+ :decoration="mainContent.decoration"
53
+ />
54
+ <MainTitle
55
+ icon="lines"
56
+ :title="mainContent.title"
57
+ :contentLabel="phrase.page"
58
+ />
46
59
  <MainFlags :flags="mainContent.flags" />
47
60
  <MainDescription :description="mainContent.description" />
48
61
  <MainKeyLinks mode="single" :elementSnippets="mainContent.snippets" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erudit",
3
- "version": "4.3.5-dev.1",
3
+ "version": "4.3.5",
4
4
  "type": "module",
5
5
  "description": "🤓 CMS for perfect educational sites.",
6
6
  "license": "MIT",
@@ -24,9 +24,9 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@erudit-js/cli": "4.3.5-dev.1",
28
- "@erudit-js/core": "4.3.5-dev.1",
29
- "@erudit-js/prose": "4.3.5-dev.1",
27
+ "@erudit-js/cli": "4.3.5",
28
+ "@erudit-js/core": "4.3.5",
29
+ "@erudit-js/prose": "4.3.5",
30
30
  "@floating-ui/vue": "^1.1.11",
31
31
  "@resvg/resvg-js": "^2.6.2",
32
32
  "@tailwindcss/vite": "^4.2.1",
@@ -35,7 +35,7 @@
35
35
  "consola": "^3.4.2",
36
36
  "drizzle-kit": "^0.31.10",
37
37
  "drizzle-orm": "^0.45.2",
38
- "esbuild": "^0.27.4",
38
+ "esbuild": "^0.28.0",
39
39
  "flexsearch": "^0.8.212",
40
40
  "glob": "^13.0.6",
41
41
  "image-size": "^2.0.2",
@@ -21,6 +21,9 @@ export async function getContentChildren(
21
21
  'nav',
22
22
  );
23
23
  const description = await getContentDescription(childNode.fullId);
24
+ const decoration = await ERUDIT.repository.content.ownDecoration(
25
+ childNode.fullId,
26
+ );
24
27
  const elementSnippets = await ERUDIT.repository.content.elementSnippets(
25
28
  childNode.fullId,
26
29
  );
@@ -37,6 +40,10 @@ export async function getContentChildren(
37
40
  child.description = description;
38
41
  }
39
42
 
43
+ if (decoration) {
44
+ child.decoration = decoration;
45
+ }
46
+
40
47
  if (keyLinks && keyLinks.length > 0) {
41
48
  child.keyLinks = keyLinks;
42
49
  }
@@ -1,5 +1,10 @@
1
1
  import { eq } from 'drizzle-orm';
2
2
 
3
+ function buildDecorationPath(fullId: string, extension: string) {
4
+ const navNode = ERUDIT.contentNav.getNodeOrThrow(fullId);
5
+ return 'content/' + navNode.contentRelPath + '/decoration.' + extension;
6
+ }
7
+
3
8
  export async function getContentDecoration(fullId: string) {
4
9
  const parts = fullId.split('/');
5
10
  for (let i = parts.length; i > 0; i--) {
@@ -11,13 +16,18 @@ export async function getContentDecoration(fullId: string) {
11
16
  });
12
17
 
13
18
  if (dbContent?.decorationExtension) {
14
- const navNode = ERUDIT.contentNav.getNodeOrThrow(trimmedId);
15
- return (
16
- 'content/' +
17
- navNode.contentRelPath +
18
- '/decoration.' +
19
- dbContent.decorationExtension
20
- );
19
+ return buildDecorationPath(trimmedId, dbContent.decorationExtension);
21
20
  }
22
21
  }
23
22
  }
23
+
24
+ export async function getContentOwnDecoration(fullId: string) {
25
+ const dbContent = await ERUDIT.db.query.content.findFirst({
26
+ columns: { decorationExtension: true },
27
+ where: eq(ERUDIT.db.schema.content.fullId, fullId),
28
+ });
29
+
30
+ if (dbContent?.decorationExtension) {
31
+ return buildDecorationPath(fullId, dbContent.decorationExtension);
32
+ }
33
+ }
@@ -35,6 +35,47 @@ export async function getContentDependencies(fullId: string) {
35
35
  }
36
36
  }
37
37
 
38
+ // Fetch auto deps early so we can merge their unique names into overlapping
39
+ // hard deps. On books/groups the hard dep row itself has no uniqueNames
40
+ // (declared at the container level) while children's auto deps carry the
41
+ // exact element references from prose <Dep> tags.
42
+ const dbAutoDependencies = await ERUDIT.db.query.contentDeps.findMany({
43
+ columns: { toFullId: true, hard: true, uniqueNames: true },
44
+ where: and(
45
+ or(
46
+ eq(ERUDIT.db.schema.contentDeps.fromFullId, fullId),
47
+ like(ERUDIT.db.schema.contentDeps.fromFullId, `${fullId}/%`),
48
+ ),
49
+ eq(ERUDIT.db.schema.contentDeps.hard, false),
50
+ ),
51
+ });
52
+
53
+ // Merge unique names across rows that share the same toFullId
54
+ // (can happen when a topic and its children both dep on the same target).
55
+ const autoUniqueMap = new Map<string, Set<string>>();
56
+ for (const row of dbAutoDependencies) {
57
+ if (!autoUniqueMap.has(row.toFullId)) {
58
+ autoUniqueMap.set(row.toFullId, new Set());
59
+ }
60
+ if (row.uniqueNames) {
61
+ for (const name of row.uniqueNames.split(',')) {
62
+ autoUniqueMap.get(row.toFullId)!.add(name);
63
+ }
64
+ }
65
+ }
66
+
67
+ // When an auto dep targets the same content item as a hard dep, the auto
68
+ // dep will be filtered out later. Merge its unique names into the hard dep
69
+ // so the exact element sublisting is not lost.
70
+ for (const [toFullId, autoUniques] of autoUniqueMap) {
71
+ if (hardUniqueMap.has(toFullId)) {
72
+ const hardSet = hardUniqueMap.get(toFullId)!;
73
+ for (const name of autoUniques) {
74
+ hardSet.add(name);
75
+ }
76
+ }
77
+ }
78
+
38
79
  const hardToFullIds = ERUDIT.contentNav.orderIds(
39
80
  externalToFullIds(dbHardDependencies),
40
81
  );
@@ -61,31 +102,6 @@ export async function getContentDependencies(fullId: string) {
61
102
 
62
103
  const autoDependencies: ContentAutoDep[] = [];
63
104
 
64
- const dbAutoDependencies = await ERUDIT.db.query.contentDeps.findMany({
65
- columns: { toFullId: true, hard: true, uniqueNames: true },
66
- where: and(
67
- or(
68
- eq(ERUDIT.db.schema.contentDeps.fromFullId, fullId),
69
- like(ERUDIT.db.schema.contentDeps.fromFullId, `${fullId}/%`),
70
- ),
71
- eq(ERUDIT.db.schema.contentDeps.hard, false),
72
- ),
73
- });
74
-
75
- // Merge unique names across rows that share the same toFullId
76
- // (can happen when a topic and its children both dep on the same target).
77
- const autoUniqueMap = new Map<string, Set<string>>();
78
- for (const row of dbAutoDependencies) {
79
- if (!autoUniqueMap.has(row.toFullId)) {
80
- autoUniqueMap.set(row.toFullId, new Set());
81
- }
82
- if (row.uniqueNames) {
83
- for (const name of row.uniqueNames.split(',')) {
84
- autoUniqueMap.get(row.toFullId)!.add(name);
85
- }
86
- }
87
- }
88
-
89
105
  // Skip auto-dependency if a hard dependency from the same source exists
90
106
  const autoToFullIds = ERUDIT.contentNav
91
107
  .orderIds(externalToFullIds(dbAutoDependencies))
@@ -16,7 +16,10 @@ import {
16
16
  getContentHeadingUnique,
17
17
  getContentUnique,
18
18
  } from './content/repository/unique';
19
- import { getContentDecoration } from './content/repository/decoration';
19
+ import {
20
+ getContentDecoration,
21
+ getContentOwnDecoration,
22
+ } from './content/repository/decoration';
20
23
  import { getContentFlags } from './content/repository/flags';
21
24
  import { getContentConnections } from './content/repository/connections';
22
25
  import { getQuoteIds } from './quote/repository/ids';
@@ -70,6 +73,7 @@ export const repository = {
70
73
  title: getContentTitle,
71
74
  description: getContentDescription,
72
75
  decoration: getContentDecoration,
76
+ ownDecoration: getContentOwnDecoration,
73
77
  elementSnippets: getContentElementSnippets,
74
78
  flags: getContentFlags,
75
79
  connections: getContentConnections,
@@ -35,6 +35,7 @@ export interface MainContentChildrenItem {
35
35
  link: string;
36
36
  title: string;
37
37
  description?: string;
38
+ decoration?: string;
38
39
  keyLinks?: ElementSnippet[];
39
40
  stats?: ContentStats;
40
41
  }