@jjlmoya/utils-nature 1.22.0 → 1.24.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 (68) hide show
  1. package/package.json +2 -2
  2. package/src/category/i18n/es.ts +1 -1
  3. package/src/category/i18n/fr.ts +110 -110
  4. package/src/category/i18n/it.ts +1 -1
  5. package/src/category/i18n/pl.ts +1 -1
  6. package/src/category/i18n/pt.ts +1 -1
  7. package/src/category/i18n/ru.ts +110 -110
  8. package/src/category/seo.astro +1 -1
  9. package/src/components/PreviewNavSidebar.astro +1 -1
  10. package/src/components/PreviewToolbar.astro +1 -1
  11. package/src/data.ts +1 -1
  12. package/src/env.d.ts +1 -1
  13. package/src/layouts/PreviewLayout.astro +117 -117
  14. package/src/pages/[locale]/[slug].astro +164 -164
  15. package/src/pages/index.astro +1 -1
  16. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  17. package/src/tests/category_seo_quality.test.ts +74 -0
  18. package/src/tests/faq_count.test.ts +19 -19
  19. package/src/tests/mocks/astro_mock.js +1 -1
  20. package/src/tests/no_en_dash.test.ts +71 -71
  21. package/src/tests/no_h1_in_components.test.ts +1 -1
  22. package/src/tests/seo_length.test.ts +23 -23
  23. package/src/tests/seo_parity.test.ts +2 -2
  24. package/src/tests/seo_wellformed_export.test.ts +65 -0
  25. package/src/tests/spanish_leakage.test.ts +175 -175
  26. package/src/tool/compostBinVolumeRatioCalculator/bibliography.astro +2 -14
  27. package/src/tool/compostBinVolumeRatioCalculator/seo.astro +6 -14
  28. package/src/tool/cricketThermometer/i18n/de.ts +169 -169
  29. package/src/tool/cricketThermometer/i18n/en.ts +169 -169
  30. package/src/tool/cricketThermometer/i18n/es.ts +169 -169
  31. package/src/tool/cricketThermometer/i18n/fr.ts +169 -169
  32. package/src/tool/cricketThermometer/i18n/id.ts +169 -169
  33. package/src/tool/cricketThermometer/i18n/it.ts +169 -169
  34. package/src/tool/cricketThermometer/i18n/ja.ts +169 -169
  35. package/src/tool/cricketThermometer/i18n/ko.ts +169 -169
  36. package/src/tool/cricketThermometer/i18n/nl.ts +169 -169
  37. package/src/tool/cricketThermometer/i18n/pl.ts +169 -169
  38. package/src/tool/cricketThermometer/i18n/pt.ts +169 -169
  39. package/src/tool/cricketThermometer/i18n/ru.ts +169 -169
  40. package/src/tool/cricketThermometer/i18n/sv.ts +169 -169
  41. package/src/tool/cricketThermometer/i18n/tr.ts +169 -169
  42. package/src/tool/cricketThermometer/i18n/zh.ts +169 -169
  43. package/src/tool/cricketThermometer/seo.astro +15 -15
  44. package/src/tool/digitalCarbon/i18n/de.ts +229 -229
  45. package/src/tool/digitalCarbon/i18n/fr.ts +229 -229
  46. package/src/tool/digitalCarbon/i18n/ru.ts +229 -229
  47. package/src/tool/digitalCarbon/seo.astro +15 -15
  48. package/src/tool/rainHarvester/i18n/de.ts +167 -167
  49. package/src/tool/rainHarvester/i18n/en.ts +167 -167
  50. package/src/tool/rainHarvester/i18n/es.ts +167 -167
  51. package/src/tool/rainHarvester/i18n/fr.ts +167 -167
  52. package/src/tool/rainHarvester/i18n/id.ts +167 -167
  53. package/src/tool/rainHarvester/i18n/it.ts +167 -167
  54. package/src/tool/rainHarvester/i18n/ja.ts +167 -167
  55. package/src/tool/rainHarvester/i18n/ko.ts +167 -167
  56. package/src/tool/rainHarvester/i18n/nl.ts +167 -167
  57. package/src/tool/rainHarvester/i18n/pl.ts +167 -167
  58. package/src/tool/rainHarvester/i18n/pt.ts +167 -167
  59. package/src/tool/rainHarvester/i18n/ru.ts +167 -167
  60. package/src/tool/rainHarvester/i18n/sv.ts +167 -167
  61. package/src/tool/rainHarvester/i18n/tr.ts +167 -167
  62. package/src/tool/rainHarvester/i18n/zh.ts +167 -167
  63. package/src/tool/rainHarvester/seo.astro +15 -15
  64. package/src/tool/seedCalculator/i18n/fr.ts +197 -197
  65. package/src/tool/seedCalculator/i18n/pl.ts +197 -197
  66. package/src/tool/seedCalculator/i18n/ru.ts +197 -197
  67. package/src/tool/seedCalculator/seo.astro +15 -15
  68. package/src/types.ts +1 -1
@@ -1,117 +1,117 @@
1
- ---
2
- import "@jjlmoya/utils-shared/theme.css";
3
- import PreviewToolbar from "../components/PreviewToolbar.astro";
4
- import type { KnownLocale } from "../types";
5
-
6
- interface Props {
7
- title: string;
8
- currentLocale?: KnownLocale;
9
- localeUrls?: Partial<Record<KnownLocale, string>>;
10
- hasSidebar?: boolean;
11
- }
12
-
13
- const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Astro.props;
14
- ---
15
-
16
- <!doctype html>
17
- <html lang={currentLocale}>
18
- <head>
19
- <meta charset="UTF-8" />
20
- <meta name="viewport" content="width=device-width" />
21
- <title>{title} · preview</title>
22
- <link rel="preconnect" href="https://fonts.googleapis.com" />
23
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
24
- <link
25
- href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
26
- rel="stylesheet"
27
- />
28
-
29
- <script is:inline>
30
- (function () {
31
- const saved = localStorage.getItem("theme") || "theme-dark";
32
- document.documentElement.classList.add(saved);
33
- })();
34
- </script>
35
- <slot name="head" />
36
- </head>
37
- <body>
38
- <PreviewToolbar currentLocale={currentLocale} localeUrls={localeUrls} />
39
- <div class:list={["page-wrapper", { "with-sidebar": hasSidebar }]}>
40
- {
41
- hasSidebar && (
42
- <aside class="sidebar-area">
43
- <slot name="sidebar" />
44
- </aside>
45
- )
46
- }
47
- <main>
48
- <slot />
49
- </main>
50
- </div>
51
- </body>
52
- </html>
53
-
54
- <style is:global>
55
- :root {
56
- --accent: #f43f5e;
57
- --primary-base: #9f1239;
58
- --cyan: #06b6d4;
59
- }
60
-
61
- .theme-dark,
62
- .theme-light {
63
- --text-main: var(--text-base);
64
- --border-color: var(--border-base);
65
- }
66
-
67
- *,
68
- *::before,
69
- *::after {
70
- box-sizing: border-box;
71
- }
72
-
73
- body {
74
- background-color: var(--bg-page);
75
- color: var(--text-base);
76
- margin: 0;
77
- min-height: 100vh;
78
- transition:
79
- background-color 0.3s ease,
80
- color 0.3s ease;
81
- font-family: Inter, sans-serif;}
82
-
83
- main {
84
- padding: 0 2rem;
85
- }
86
-
87
- .page-wrapper {
88
- display: flex;
89
- flex-direction: column;
90
- }
91
-
92
- .page-wrapper.with-sidebar {
93
- display: grid;
94
- grid-template-columns: 240px 1fr;
95
- min-height: 100vh;
96
- }
97
-
98
- .sidebar-area {
99
- position: sticky;
100
- top: 0;
101
- height: 100vh;
102
- overflow-y: auto;
103
- border-right: 1px solid var(--border-color);
104
- background: var(--bg-page);
105
- }
106
-
107
- @media (max-width: 768px) {
108
- .page-wrapper.with-sidebar {
109
- grid-template-columns: 1fr;
110
- }
111
-
112
- .sidebar-area {
113
- display: none;
114
- }
115
- }
116
- </style>
117
-
1
+ ---
2
+ import "@jjlmoya/utils-shared/theme.css";
3
+ import PreviewToolbar from "../components/PreviewToolbar.astro";
4
+ import type { KnownLocale } from "../types";
5
+
6
+ interface Props {
7
+ title: string;
8
+ currentLocale?: KnownLocale;
9
+ localeUrls?: Partial<Record<KnownLocale, string>>;
10
+ hasSidebar?: boolean;
11
+ }
12
+
13
+ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Astro.props;
14
+ ---
15
+
16
+ <!doctype html>
17
+ <html lang={currentLocale}>
18
+ <head>
19
+ <meta charset="UTF-8" />
20
+ <meta name="viewport" content="width=device-width" />
21
+ <title>{title} · preview</title>
22
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
23
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
24
+ <link
25
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
26
+ rel="stylesheet"
27
+ />
28
+
29
+ <script is:inline>
30
+ (function () {
31
+ const saved = localStorage.getItem("theme") || "theme-dark";
32
+ document.documentElement.classList.add(saved);
33
+ })();
34
+ </script>
35
+ <slot name="head" />
36
+ </head>
37
+ <body>
38
+ <PreviewToolbar currentLocale={currentLocale} localeUrls={localeUrls} />
39
+ <div class:list={["page-wrapper", { "with-sidebar": hasSidebar }]}>
40
+ {
41
+ hasSidebar && (
42
+ <aside class="sidebar-area">
43
+ <slot name="sidebar" />
44
+ </aside>
45
+ )
46
+ }
47
+ <main>
48
+ <slot />
49
+ </main>
50
+ </div>
51
+ </body>
52
+ </html>
53
+
54
+ <style is:global>
55
+ :root {
56
+ --accent: #f43f5e;
57
+ --primary-base: #9f1239;
58
+ --cyan: #06b6d4;
59
+ }
60
+
61
+ .theme-dark,
62
+ .theme-light {
63
+ --text-main: var(--text-base);
64
+ --border-color: var(--border-base);
65
+ }
66
+
67
+ *,
68
+ *::before,
69
+ *::after {
70
+ box-sizing: border-box;
71
+ }
72
+
73
+ body {
74
+ background-color: var(--bg-page);
75
+ color: var(--text-base);
76
+ margin: 0;
77
+ min-height: 100vh;
78
+ transition:
79
+ background-color 0.3s ease,
80
+ color 0.3s ease;
81
+ font-family: Inter, sans-serif;}
82
+
83
+ main {
84
+ padding: 0 2rem;
85
+ }
86
+
87
+ .page-wrapper {
88
+ display: flex;
89
+ flex-direction: column;
90
+ }
91
+
92
+ .page-wrapper.with-sidebar {
93
+ display: grid;
94
+ grid-template-columns: 240px 1fr;
95
+ min-height: 100vh;
96
+ }
97
+
98
+ .sidebar-area {
99
+ position: sticky;
100
+ top: 0;
101
+ height: 100vh;
102
+ overflow-y: auto;
103
+ border-right: 1px solid var(--border-color);
104
+ background: var(--bg-page);
105
+ }
106
+
107
+ @media (max-width: 768px) {
108
+ .page-wrapper.with-sidebar {
109
+ grid-template-columns: 1fr;
110
+ }
111
+
112
+ .sidebar-area {
113
+ display: none;
114
+ }
115
+ }
116
+ </style>
117
+
@@ -1,164 +1,164 @@
1
- ---
2
- import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
- import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
- import { ALL_TOOLS } from "../../index";
5
- import {
6
- UtilityHeader,
7
- FAQSection,
8
- Bibliography,
9
- SEORenderer,
10
- } from "@jjlmoya/utils-shared";
11
- import type { KnownLocale, ToolLocaleContent } from "../../types";
12
- import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
13
-
14
- export async function getStaticPaths() {
15
- const paths = [];
16
-
17
- for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
- const Component = (await (lazyComp as () => Promise<{ default: unknown }>)()).default;
19
- const localeEntries = Object.entries(entry.i18n) as [
20
- KnownLocale,
21
- () => Promise<ToolLocaleContent>,
22
- ][];
23
- const localeContents = await Promise.all(
24
- localeEntries.map(async ([locale, loader]) => ({
25
- locale,
26
- content: await loader(),
27
- })),
28
- );
29
-
30
- const localeUrls = Object.fromEntries(
31
- localeContents.map(({ locale, content }) => [
32
- locale,
33
- `/${locale}/${content.slug}`,
34
- ]),
35
- ) as Partial<Record<KnownLocale, string>>;
36
-
37
- const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
- const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
-
40
- for (const { locale, content } of localeContents) {
41
- const allToolsNav = (
42
- await Promise.all(
43
- ALL_TOOLS.map(async ({ entry: navEntry }) => {
44
- const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
45
- if (!loader) return null;
46
- const navContent = await loader();
47
- return {
48
- id: navEntry.id,
49
- title: navContent.title,
50
- href: `/${locale}/${navContent.slug}`,
51
- isActive: navEntry.id === entry.id,
52
- };
53
- }),
54
- )
55
- ).filter(Boolean) as NavItem[];
56
- paths.push({
57
- params: { locale, slug: content.slug },
58
- props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
59
- });
60
- }
61
- }
62
-
63
- return paths;
64
- }
65
-
66
- interface NavItem {
67
- id: string;
68
- title: string;
69
- href: string;
70
- isActive?: boolean;
71
- }
72
-
73
- export interface Props {
74
- Component: unknown;
75
- locale: KnownLocale;
76
- content: ToolLocaleContent;
77
- localeUrls: Partial<Record<KnownLocale, string>>;
78
- allToolsNav: NavItem[];
79
- englishSlug: string;
80
- }
81
-
82
- const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
-
84
- const cssFiles = import.meta.glob("../../tool/**/*.css", { query: "?raw", import: "default" });
85
- const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
- const cssLoader = cssKey ? cssFiles[cssKey] : undefined;
87
- const toolCss = cssLoader ? await cssLoader() as string : "";
88
-
89
- const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
90
-
91
- const words = content.title.split(" ");
92
- const titleHighlight = words[0] || "";
93
- const titleBase = words.slice(1).join(" ") || "";
94
- ---
95
-
96
- <PreviewLayout
97
- title={content.title}
98
- currentLocale={locale}
99
- localeUrls={localeUrls}
100
- hasSidebar={true}
101
- >
102
- <PreviewNavSidebar
103
- slot="sidebar"
104
- categoryTitle="Tools"
105
- tools={allToolsNav}
106
- />
107
- <Fragment slot="head">
108
- {toolCss && <style set:html={toolCss} />}
109
- {
110
- ( content.schemas ?? []).map((schema) => (
111
- <script
112
- is:inline
113
- type="application/ld+json"
114
- set:html={JSON.stringify(schema)}
115
- />
116
- ))
117
- }
118
- </Fragment>
119
-
120
- <div class="tool-page">
121
- <UtilityHeader
122
- titleHighlight={titleHighlight}
123
- titleBase={titleBase}
124
- description={content.description}
125
- />
126
-
127
- <section class="section-tool">
128
- <Component ui={content.ui} />
129
- </section>
130
-
131
- <section class="section-seo">
132
- <SEORenderer content={seoContent} />
133
- </section>
134
-
135
- <section class="section-faq">
136
- <FAQSection items={content.faq} />
137
- </section>
138
-
139
- <section class="section-bibliography">
140
- <Bibliography links={content.bibliography} />
141
- </section>
142
- </div>
143
- </PreviewLayout>
144
-
145
- <style>
146
- .tool-page {
147
- display: flex;
148
- flex-direction: column;
149
- gap: 2rem;
150
- }
151
-
152
- .section-tool {
153
- max-width: 1200px;
154
- margin: 0 auto;
155
- width: 100%;
156
- }
157
-
158
- .section-seo,
159
- .section-faq,
160
- .section-bibliography {
161
- padding-top: 2rem;
162
- border-top: 1px solid var(--border-color);
163
- }
164
- </style>
1
+ ---
2
+ import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
+ import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
+ import { ALL_TOOLS } from "../../index";
5
+ import {
6
+ UtilityHeader,
7
+ FAQSection,
8
+ Bibliography,
9
+ SEORenderer,
10
+ } from "@jjlmoya/utils-shared";
11
+ import type { KnownLocale, ToolLocaleContent } from "../../types";
12
+ import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
13
+
14
+ export async function getStaticPaths() {
15
+ const paths = [];
16
+
17
+ for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
+ const Component = (await (lazyComp as () => Promise<{ default: unknown }>)()).default;
19
+ const localeEntries = Object.entries(entry.i18n) as [
20
+ KnownLocale,
21
+ () => Promise<ToolLocaleContent>,
22
+ ][];
23
+ const localeContents = await Promise.all(
24
+ localeEntries.map(async ([locale, loader]) => ({
25
+ locale,
26
+ content: await loader(),
27
+ })),
28
+ );
29
+
30
+ const localeUrls = Object.fromEntries(
31
+ localeContents.map(({ locale, content }) => [
32
+ locale,
33
+ `/${locale}/${content.slug}`,
34
+ ]),
35
+ ) as Partial<Record<KnownLocale, string>>;
36
+
37
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
+
40
+ for (const { locale, content } of localeContents) {
41
+ const allToolsNav = (
42
+ await Promise.all(
43
+ ALL_TOOLS.map(async ({ entry: navEntry }) => {
44
+ const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
45
+ if (!loader) return null;
46
+ const navContent = await loader();
47
+ return {
48
+ id: navEntry.id,
49
+ title: navContent.title,
50
+ href: `/${locale}/${navContent.slug}`,
51
+ isActive: navEntry.id === entry.id,
52
+ };
53
+ }),
54
+ )
55
+ ).filter(Boolean) as NavItem[];
56
+ paths.push({
57
+ params: { locale, slug: content.slug },
58
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
59
+ });
60
+ }
61
+ }
62
+
63
+ return paths;
64
+ }
65
+
66
+ interface NavItem {
67
+ id: string;
68
+ title: string;
69
+ href: string;
70
+ isActive?: boolean;
71
+ }
72
+
73
+ export interface Props {
74
+ Component: unknown;
75
+ locale: KnownLocale;
76
+ content: ToolLocaleContent;
77
+ localeUrls: Partial<Record<KnownLocale, string>>;
78
+ allToolsNav: NavItem[];
79
+ englishSlug: string;
80
+ }
81
+
82
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
+
84
+ const cssFiles = import.meta.glob("../../tool/**/*.css", { query: "?raw", import: "default" });
85
+ const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
+ const cssLoader = cssKey ? cssFiles[cssKey] : undefined;
87
+ const toolCss = cssLoader ? await cssLoader() as string : "";
88
+
89
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
90
+
91
+ const words = content.title.split(" ");
92
+ const titleHighlight = words[0] || "";
93
+ const titleBase = words.slice(1).join(" ") || "";
94
+ ---
95
+
96
+ <PreviewLayout
97
+ title={content.title}
98
+ currentLocale={locale}
99
+ localeUrls={localeUrls}
100
+ hasSidebar={true}
101
+ >
102
+ <PreviewNavSidebar
103
+ slot="sidebar"
104
+ categoryTitle="Tools"
105
+ tools={allToolsNav}
106
+ />
107
+ <Fragment slot="head">
108
+ {toolCss && <style set:html={toolCss} />}
109
+ {
110
+ ( content.schemas ?? []).map((schema) => (
111
+ <script
112
+ is:inline
113
+ type="application/ld+json"
114
+ set:html={JSON.stringify(schema)}
115
+ />
116
+ ))
117
+ }
118
+ </Fragment>
119
+
120
+ <div class="tool-page">
121
+ <UtilityHeader
122
+ titleHighlight={titleHighlight}
123
+ titleBase={titleBase}
124
+ description={content.description}
125
+ />
126
+
127
+ <section class="section-tool">
128
+ <Component ui={content.ui} />
129
+ </section>
130
+
131
+ <section class="section-seo">
132
+ <SEORenderer content={seoContent} />
133
+ </section>
134
+
135
+ <section class="section-faq">
136
+ <FAQSection items={content.faq} />
137
+ </section>
138
+
139
+ <section class="section-bibliography">
140
+ <Bibliography links={content.bibliography} />
141
+ </section>
142
+ </div>
143
+ </PreviewLayout>
144
+
145
+ <style>
146
+ .tool-page {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 2rem;
150
+ }
151
+
152
+ .section-tool {
153
+ max-width: 1200px;
154
+ margin: 0 auto;
155
+ width: 100%;
156
+ }
157
+
158
+ .section-seo,
159
+ .section-faq,
160
+ .section-bibliography {
161
+ padding-top: 2rem;
162
+ border-top: 1px solid var(--border-color);
163
+ }
164
+ </style>
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  ---
3
3
  <meta http-equiv="refresh" content="0;url=/es" />
4
-
4
+
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readdirSync, readFileSync, existsSync } from 'fs';
3
+ import { join, relative } from 'path';
4
+
5
+ function findBibliographyAstroFiles(dir: string): string[] {
6
+ const files: string[] = [];
7
+ if (!existsSync(dir)) return files;
8
+ const entries = readdirSync(dir, { withFileTypes: true });
9
+
10
+ for (const entry of entries) {
11
+ const fullPath = join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ files.push(...findBibliographyAstroFiles(fullPath));
14
+ } else if (entry.isFile() && entry.name === 'bibliography.astro') {
15
+ files.push(fullPath);
16
+ }
17
+ }
18
+
19
+ return files;
20
+ }
21
+
22
+ const toolDir = join(process.cwd(), 'src', 'tool');
23
+ const bibliographyFiles = findBibliographyAstroFiles(toolDir);
24
+
25
+ describe('Bibliography Component Wellformed Export', () => {
26
+ it('found tool bibliography.astro components', () => {
27
+ expect(bibliographyFiles.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ bibliographyFiles.forEach((file) => {
31
+ const relativePath = relative(process.cwd(), file);
32
+
33
+ it(`${relativePath} should use SharedBibliography from @jjlmoya/utils-shared`, () => {
34
+ const content = readFileSync(file, 'utf-8');
35
+
36
+ const usesSharedComponent =
37
+ content.includes('@jjlmoya/utils-shared') &&
38
+ (content.includes('Bibliography') || content.includes('SharedBibliography'));
39
+
40
+ expect(
41
+ usesSharedComponent,
42
+ `File "${relativePath}" does not use the standard Bibliography component from @jjlmoya/utils-shared, resulting in unstyled or raw bibliography output.`,
43
+ ).toBe(true);
44
+ });
45
+ });
46
+ });