@jjlmoya/utils-books 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.github/workflows/npm-publish.yml +40 -0
  2. package/.gitignore +6 -0
  3. package/.stylelintrc.json +98 -0
  4. package/astro.config.mjs +19 -0
  5. package/eslint.config.js +201 -0
  6. package/package.json +79 -0
  7. package/prompts/create_tool.md +98 -0
  8. package/prompts/i18n/de.md +16 -0
  9. package/prompts/i18n/en.md +16 -0
  10. package/prompts/i18n/es.md +16 -0
  11. package/prompts/i18n/fr.md +16 -0
  12. package/prompts/i18n/id.md +16 -0
  13. package/prompts/i18n/it.md +16 -0
  14. package/prompts/i18n/ja.md +16 -0
  15. package/prompts/i18n/ko.md +16 -0
  16. package/prompts/i18n/nl.md +16 -0
  17. package/prompts/i18n/pl.md +16 -0
  18. package/prompts/i18n/pt.md +16 -0
  19. package/prompts/i18n/ru.md +16 -0
  20. package/prompts/i18n/sv.md +16 -0
  21. package/prompts/i18n/tr.md +16 -0
  22. package/prompts/i18n/zh.md +16 -0
  23. package/prompts/seo.md +58 -0
  24. package/prompts/translations/french.md +33 -0
  25. package/scripts/postinstall.mjs +27 -0
  26. package/src/category/BooksCategorySEO.astro +9 -0
  27. package/src/category/i18n/de.ts +21 -0
  28. package/src/category/i18n/en.ts +21 -0
  29. package/src/category/i18n/es.ts +21 -0
  30. package/src/category/i18n/fr.ts +21 -0
  31. package/src/category/i18n/id.ts +21 -0
  32. package/src/category/i18n/it.ts +21 -0
  33. package/src/category/i18n/ja.ts +21 -0
  34. package/src/category/i18n/ko.ts +21 -0
  35. package/src/category/i18n/nl.ts +21 -0
  36. package/src/category/i18n/pl.ts +21 -0
  37. package/src/category/i18n/pt.ts +21 -0
  38. package/src/category/i18n/ru.ts +21 -0
  39. package/src/category/i18n/sv.ts +21 -0
  40. package/src/category/i18n/tr.ts +21 -0
  41. package/src/category/i18n/zh.ts +21 -0
  42. package/src/category/index.ts +24 -0
  43. package/src/components/PreviewNavSidebar.astro +116 -0
  44. package/src/components/PreviewToolbar.astro +143 -0
  45. package/src/data.ts +10 -0
  46. package/src/entries.ts +6 -0
  47. package/src/env.d.ts +5 -0
  48. package/src/index.ts +20 -0
  49. package/src/layouts/PreviewLayout.astro +118 -0
  50. package/src/pages/[locale]/[slug].astro +164 -0
  51. package/src/pages/[locale].astro +251 -0
  52. package/src/pages/index.astro +4 -0
  53. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  54. package/src/tests/diacritics_density.test.ts +118 -0
  55. package/src/tests/faq_count.test.ts +18 -0
  56. package/src/tests/i18n_coverage.test.ts +34 -0
  57. package/src/tests/inverted_punctuation.test.ts +84 -0
  58. package/src/tests/locale_completeness.test.ts +23 -0
  59. package/src/tests/mocks/astro_mock.js +2 -0
  60. package/src/tests/no_em_dash.test.ts +47 -0
  61. package/src/tests/no_en_dash.test.ts +70 -0
  62. package/src/tests/no_h1_in_components.test.ts +48 -0
  63. package/src/tests/pagespeed_best_practices.test.ts +198 -0
  64. package/src/tests/qa-test-helpers.ts +32 -0
  65. package/src/tests/qa_bibliography_links.test.ts +54 -0
  66. package/src/tests/qa_claim_evidence.test.ts +69 -0
  67. package/src/tests/qa_logic_reference_coverage.test.ts +46 -0
  68. package/src/tests/qa_runtime_i18n.test.ts +100 -0
  69. package/src/tests/schemas_fulfillment.test.ts +23 -0
  70. package/src/tests/script_density.test.ts +94 -0
  71. package/src/tests/seo_length.test.ts +23 -0
  72. package/src/tests/seo_parity.test.ts +60 -0
  73. package/src/tests/seo_translation_completeness.test.ts +69 -0
  74. package/src/tests/seo_wellformed_export.test.ts +65 -0
  75. package/src/tests/shared-test-helpers.ts +56 -0
  76. package/src/tests/slug_language_code_format.test.ts +23 -0
  77. package/src/tests/slug_uniqueness.test.ts +81 -0
  78. package/src/tests/spanish_leakage.test.ts +175 -0
  79. package/src/tests/title_quality.test.ts +55 -0
  80. package/src/tests/tool_exports.test.ts +34 -0
  81. package/src/tests/tool_validation.test.ts +16 -0
  82. package/src/tests/translation_copy.test.ts +127 -0
  83. package/src/tool/book-pagination-and-spine-calculator/bibliography.astro +16 -0
  84. package/src/tool/book-pagination-and-spine-calculator/bibliography.ts +7 -0
  85. package/src/tool/book-pagination-and-spine-calculator/book-pagination-and-spine-calculator.css +298 -0
  86. package/src/tool/book-pagination-and-spine-calculator/component.astro +40 -0
  87. package/src/tool/book-pagination-and-spine-calculator/controller.ts +87 -0
  88. package/src/tool/book-pagination-and-spine-calculator/dom-views.ts +21 -0
  89. package/src/tool/book-pagination-and-spine-calculator/entry.ts +27 -0
  90. package/src/tool/book-pagination-and-spine-calculator/evaluator.ts +12 -0
  91. package/src/tool/book-pagination-and-spine-calculator/i18n/de.ts +78 -0
  92. package/src/tool/book-pagination-and-spine-calculator/i18n/en.ts +78 -0
  93. package/src/tool/book-pagination-and-spine-calculator/i18n/es.ts +78 -0
  94. package/src/tool/book-pagination-and-spine-calculator/i18n/fr.ts +78 -0
  95. package/src/tool/book-pagination-and-spine-calculator/i18n/id.ts +78 -0
  96. package/src/tool/book-pagination-and-spine-calculator/i18n/it.ts +78 -0
  97. package/src/tool/book-pagination-and-spine-calculator/i18n/ja.ts +78 -0
  98. package/src/tool/book-pagination-and-spine-calculator/i18n/ko.ts +78 -0
  99. package/src/tool/book-pagination-and-spine-calculator/i18n/nl.ts +78 -0
  100. package/src/tool/book-pagination-and-spine-calculator/i18n/pl.ts +78 -0
  101. package/src/tool/book-pagination-and-spine-calculator/i18n/pt.ts +78 -0
  102. package/src/tool/book-pagination-and-spine-calculator/i18n/ru.ts +78 -0
  103. package/src/tool/book-pagination-and-spine-calculator/i18n/sv.ts +78 -0
  104. package/src/tool/book-pagination-and-spine-calculator/i18n/tr.ts +78 -0
  105. package/src/tool/book-pagination-and-spine-calculator/i18n/zh.ts +78 -0
  106. package/src/tool/book-pagination-and-spine-calculator/index.ts +11 -0
  107. package/src/tool/book-pagination-and-spine-calculator/logic.test.ts +21 -0
  108. package/src/tool/book-pagination-and-spine-calculator/logic.ts +52 -0
  109. package/src/tool/book-pagination-and-spine-calculator/seo.astro +16 -0
  110. package/src/tool/book-pagination-and-spine-calculator/storage.ts +22 -0
  111. package/src/tool/book-pagination-and-spine-calculator/ui.ts +35 -0
  112. package/src/tools.ts +5 -0
  113. package/src/types.ts +69 -0
  114. package/tsconfig.json +15 -0
  115. package/vitest.config.ts +20 -0
@@ -0,0 +1,21 @@
1
+ import type { CategoryLocaleContent } from '../../types';
2
+
3
+ export const content: CategoryLocaleContent = {
4
+ slug: 'shu',
5
+ title: '图书出版工具',
6
+ description: '为作者、编辑和独立出版商提供实用的浏览器工具,先从页数和书脊规划开始。',
7
+ seo: [
8
+ { type: 'title', text: '图书出版与书脊规划工具', level: 2 },
9
+ { type: 'paragraph', html: '当手稿、页面设计、纸张和装订方式能够一起查看时,图书制作就更容易规划。本工具库为作者、编辑、设计师和出版商提供小巧而专注的工具,让你在开始完整制作流程前快速得到估算。' },
10
+ { type: 'title', text: '在送印前规划图书', level: 2 },
11
+ { type: 'paragraph', html: '第一个工具根据手稿字数和几项物理参数,估算页数、每页字数、书脊宽度以及展开书封尺寸。你可以选择小说、大字版或练习册预设,再调整开本、页边距、字体、纸张厚度和装订方式,了解图书会如何变化。' },
12
+ { type: 'title', text: '边界清晰的实用估算', level: 2 },
13
+ { type: 'paragraph', html: '这些工具适合前期规划、方案比较、教学和私人草稿。它们不能替代印刷打样、专业拼版流程,也不能替代制作伙伴提供的最终纸张和装订规格。' },
14
+ { type: 'stats', items: [
15
+ { label: '工具', value: '1+', icon: 'mdi:book-open-variant' },
16
+ { label: '格式', value: '3', icon: 'mdi:book-cog-outline' },
17
+ { label: '单位', value: '2', icon: 'mdi:ruler-square' },
18
+ { label: '隐私', value: '本地处理', icon: 'mdi:shield-check-outline' },
19
+ ] },
20
+ ],
21
+ };
@@ -0,0 +1,24 @@
1
+ import { bookPaginationAndSpineCalculator } from '../tool/book-pagination-and-spine-calculator/entry';
2
+ import type { CategoryLocaleContent, KnownLocale } from '../types';
3
+
4
+ export const booksCategory = {
5
+ icon: 'mdi:book-open-page-variant-outline',
6
+ tools: [bookPaginationAndSpineCalculator],
7
+ i18n: {
8
+ de: () => import('./i18n/de').then((module) => module.content),
9
+ en: () => import('./i18n/en').then((module) => module.content),
10
+ es: () => import('./i18n/es').then((module) => module.content),
11
+ fr: () => import('./i18n/fr').then((module) => module.content),
12
+ id: () => import('./i18n/id').then((module) => module.content),
13
+ it: () => import('./i18n/it').then((module) => module.content),
14
+ ja: () => import('./i18n/ja').then((module) => module.content),
15
+ ko: () => import('./i18n/ko').then((module) => module.content),
16
+ nl: () => import('./i18n/nl').then((module) => module.content),
17
+ pl: () => import('./i18n/pl').then((module) => module.content),
18
+ pt: () => import('./i18n/pt').then((module) => module.content),
19
+ ru: () => import('./i18n/ru').then((module) => module.content),
20
+ sv: () => import('./i18n/sv').then((module) => module.content),
21
+ tr: () => import('./i18n/tr').then((module) => module.content),
22
+ zh: () => import('./i18n/zh').then((module) => module.content),
23
+ } as Partial<Record<KnownLocale, () => Promise<CategoryLocaleContent>>>,
24
+ };
@@ -0,0 +1,116 @@
1
+ ---
2
+ interface NavItem {
3
+ id: string;
4
+ title: string;
5
+ href: string;
6
+ isActive?: boolean;
7
+ }
8
+
9
+ interface Props {
10
+ categoryTitle: string;
11
+ tools?: NavItem[];
12
+ }
13
+
14
+ const { categoryTitle, tools = [] } = Astro.props;
15
+ ---
16
+
17
+ <nav class="preview-nav-sidebar">
18
+ <div class="sidebar-header">
19
+ <h3>{categoryTitle}</h3>
20
+ </div>
21
+
22
+ <ul class="tools-list">
23
+ {tools.map((tool) => (
24
+ <li>
25
+ <a
26
+ href={tool.href}
27
+ class:list={['tool-link', { active: tool.isActive }]}
28
+ >
29
+ <span class="tool-title">{tool.title}</span>
30
+ </a>
31
+ </li>
32
+ ))}
33
+ </ul>
34
+ </nav>
35
+
36
+ <style>
37
+ .preview-nav-sidebar {
38
+ display: flex;
39
+ flex-direction: column;
40
+ height: 100%;
41
+ background: var(--bg-surface, #0f172a);
42
+ }
43
+
44
+ .sidebar-header {
45
+ padding: 2rem 1.5rem 1.5rem;
46
+ border-bottom: 1px solid var(--border-color, #1e293b);
47
+ }
48
+
49
+ .sidebar-header h3 {
50
+ margin: 0;
51
+ font-size: 0.75rem;
52
+ font-weight: 900;
53
+ text-transform: uppercase;
54
+ letter-spacing: 0.1em;
55
+ color: var(--text-muted, #94a3b8);
56
+ }
57
+
58
+ .tools-list {
59
+ list-style: none;
60
+ margin: 0;
61
+ padding: 0.5rem 0;
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 0.25rem;
65
+ }
66
+
67
+ .tools-list li {
68
+ margin: 0;
69
+ }
70
+
71
+ .tool-link {
72
+ display: flex;
73
+ align-items: center;
74
+ padding: 0.75rem 1.5rem;
75
+ color: var(--text-muted, #94a3b8);
76
+ text-decoration: none;
77
+ font-size: 0.9375rem;
78
+ font-weight: 500;
79
+ transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
80
+ border-left: 3px solid transparent;
81
+ position: relative;
82
+ }
83
+
84
+ .tool-link:hover {
85
+ color: var(--text-base, #f1f5f9);
86
+ background: rgba(255, 255, 255, 0.08);
87
+ padding-left: 1.75rem;
88
+ }
89
+
90
+ .tool-link.active {
91
+ color: var(--accent, #f43f5e);
92
+ background: rgba(244, 63, 94, 0.15);
93
+ border-left-color: var(--accent, #f43f5e);
94
+ font-weight: 600;
95
+ }
96
+
97
+ .tool-link.active::before {
98
+ content: '';
99
+ position: absolute;
100
+ left: 0;
101
+ top: 50%;
102
+ transform: translateY(-50%);
103
+ width: 3px;
104
+ height: 24px;
105
+ background: var(--accent, #f43f5e);
106
+ border-radius: 0 2px 2px 0;
107
+ }
108
+
109
+ .tool-title {
110
+ display: block;
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ white-space: nowrap;
114
+ }
115
+ </style>
116
+
@@ -0,0 +1,143 @@
1
+ ---
2
+ import type { KnownLocale } from '../types';
3
+
4
+ interface Props {
5
+ currentLocale: KnownLocale;
6
+ localeUrls?: Partial<Record<KnownLocale, string>>;
7
+ }
8
+
9
+ const { currentLocale, localeUrls = {} } = Astro.props;
10
+
11
+ const otherLocales = Object.entries(localeUrls).filter(([l]) => l !== currentLocale) as [
12
+ KnownLocale,
13
+ string,
14
+ ][];
15
+ ---
16
+
17
+ <nav class="preview-toolbar">
18
+ {
19
+ otherLocales.length > 0 && (
20
+ <div class="locale-group">
21
+ <span class="locale-current">{currentLocale.toUpperCase()}</span>
22
+ {otherLocales.map(([locale, url]) => (
23
+ <a href={url} class="btn-locale">
24
+ {locale.toUpperCase()}
25
+ </a>
26
+ ))}
27
+ </div>
28
+ )
29
+ }
30
+
31
+ <button id="theme-toggle" class="btn-theme" aria-label="Toggle theme" title="Toggle theme">
32
+ <span class="theme-icon"></span>
33
+ </button>
34
+ </nav>
35
+
36
+ <script>
37
+ function initToolbar() {
38
+ const btn = document.getElementById('theme-toggle');
39
+ if (!btn) return;
40
+
41
+ const applyTheme = (theme: string) => {
42
+ document.documentElement.classList.remove('theme-light', 'theme-dark');
43
+ document.documentElement.classList.add(theme);
44
+ localStorage.setItem('theme', theme);
45
+ };
46
+
47
+ btn.addEventListener('click', () => {
48
+ const next = document.documentElement.classList.contains('theme-dark')
49
+ ? 'theme-light'
50
+ : 'theme-dark';
51
+ applyTheme(next);
52
+ });
53
+ }
54
+
55
+ initToolbar();
56
+ document.addEventListener('astro:after-swap', initToolbar);
57
+ </script>
58
+
59
+ <style>
60
+ .preview-toolbar {
61
+ position: fixed;
62
+ top: 1rem;
63
+ right: 1.5rem;
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 0.5rem;
67
+ z-index: 1000;
68
+ background: var(--bg-surface);
69
+ padding: 0.4rem;
70
+ border-radius: 1rem;
71
+ border: 1px solid var(--border-color);
72
+ backdrop-filter: blur(12px);
73
+ }
74
+
75
+ .locale-group {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 0.25rem;
79
+ padding-right: 0.5rem;
80
+ border-right: 1px solid var(--border-color);
81
+ }
82
+
83
+ .locale-current {
84
+ font-size: 0.7rem;
85
+ font-weight: 900;
86
+ color: var(--text-muted);
87
+ padding: 0.4rem 0.6rem;
88
+ letter-spacing: 0.08em;
89
+ }
90
+
91
+ .btn-locale {
92
+ background: transparent;
93
+ border: 1px solid var(--border-color);
94
+ color: var(--text-main);
95
+ cursor: pointer;
96
+ padding: 0.4rem 0.7rem;
97
+ border-radius: 0.5rem;
98
+ font-weight: 700;
99
+ font-size: 0.7rem;
100
+ text-decoration: none;
101
+ letter-spacing: 0.08em;
102
+ transition: all 0.2s ease;
103
+ }
104
+
105
+ .btn-locale:hover {
106
+ border-color: var(--accent);
107
+ color: var(--accent);
108
+ }
109
+
110
+ .btn-theme {
111
+ background: transparent;
112
+ border: none;
113
+ cursor: pointer;
114
+ padding: 0.5rem;
115
+ border-radius: 0.6rem;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ transition: background 0.2s ease;
120
+ }
121
+
122
+ .btn-theme:hover {
123
+ background: rgba(255, 255, 255, 0.08);
124
+ }
125
+
126
+ .theme-icon {
127
+ display: block;
128
+ width: 16px;
129
+ height: 16px;
130
+ border-radius: 50%;
131
+ }
132
+
133
+ :global(.theme-light) .theme-icon {
134
+ background: #f59e0b;
135
+ box-shadow: 0 0 8px #fbbf24;
136
+ }
137
+
138
+ :global(.theme-dark) .theme-icon {
139
+ background: #94a3b8;
140
+ box-shadow: inset -4px -2px 0 #1e293b;
141
+ }
142
+ </style>
143
+
package/src/data.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { booksCategory } from './category';
2
+
3
+ export type {
4
+ KnownLocale,
5
+ ToolLocaleContent,
6
+ CategoryLocaleContent,
7
+ LocaleMap,
8
+ BooksToolEntry,
9
+ BooksCategoryEntry,
10
+ } from './types';
package/src/entries.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spine-calculator/entry';
2
+ export type { BookPaginationUI, BookPaginationLocaleContent } from './tool/book-pagination-and-spine-calculator/entry';
3
+
4
+ import { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spine-calculator/entry';
5
+
6
+ export const ALL_ENTRIES = [bookPaginationAndSpineCalculator];
package/src/env.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module '*.astro' {
2
+ const Component: (_props: Record<string, unknown>) => unknown;
3
+ export default Component;
4
+ }
5
+
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export { booksCategory } from './category';
2
+ export const booksCategorySEO = () => import('./category/BooksCategorySEO.astro').then((module) => module.default);
3
+
4
+ export { bookPaginationAndSpineCalculator, BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL } from './tool/book-pagination-and-spine-calculator';
5
+
6
+ export type {
7
+ KnownLocale,
8
+ FAQItem,
9
+ BibliographyEntry,
10
+ HowToStep,
11
+ ToolLocaleContent,
12
+ CategoryLocaleContent,
13
+ LocaleLoader,
14
+ LocaleMap,
15
+ BooksToolEntry,
16
+ BooksCategoryEntry,
17
+ ToolDefinition,
18
+ } from './types';
19
+
20
+ export { ALL_ENTRIES, ALL_TOOLS } from './tools';
@@ -0,0 +1,118 @@
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
+ font-family: Inter, sans-serif;
77
+ margin: 0;
78
+ min-height: 100vh;
79
+ transition:
80
+ background-color 0.3s ease,
81
+ color 0.3s ease;
82
+ }
83
+
84
+ main {
85
+ padding: 0 2rem;
86
+ }
87
+
88
+ .page-wrapper {
89
+ display: flex;
90
+ flex-direction: column;
91
+ }
92
+
93
+ .page-wrapper.with-sidebar {
94
+ display: grid;
95
+ grid-template-columns: 240px 1fr;
96
+ min-height: 100vh;
97
+ }
98
+
99
+ .sidebar-area {
100
+ position: sticky;
101
+ top: 0;
102
+ height: 100vh;
103
+ overflow-y: auto;
104
+ border-right: 1px solid var(--border-color);
105
+ background: var(--bg-page);
106
+ }
107
+
108
+ @media (max-width: 768px) {
109
+ .page-wrapper.with-sidebar {
110
+ grid-template-columns: 1fr;
111
+ }
112
+
113
+ .sidebar-area {
114
+ display: none;
115
+ }
116
+ }
117
+ </style>
118
+
@@ -0,0 +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
+ import type BookPaginationComponent from "../../tool/book-pagination-and-spine-calculator/component.astro";
14
+ import type { BookPaginationUI } from "../../tool/book-pagination-and-spine-calculator/ui";
15
+
16
+ export async function getStaticPaths() {
17
+ const paths = [];
18
+
19
+ for (const { entry, Component: lazyComp } of ALL_TOOLS) {
20
+ const { default: Component } = await (lazyComp as () => Promise<{ default: unknown }>)();
21
+ const localeEntries = Object.entries(entry.i18n) as [
22
+ KnownLocale,
23
+ () => Promise<ToolLocaleContent>,
24
+ ][];
25
+ const localeContents = await Promise.all(
26
+ localeEntries.map(async ([locale, loader]) => ({
27
+ locale,
28
+ content: await loader(),
29
+ })),
30
+ );
31
+
32
+ const localeUrls = Object.fromEntries(
33
+ localeContents.map(({ locale, content }) => [
34
+ locale,
35
+ `/${locale}/${content.slug}`,
36
+ ]),
37
+ ) as Partial<Record<KnownLocale, string>>;
38
+
39
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
40
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
41
+
42
+ for (const { locale, content } of localeContents) {
43
+ const allToolsNav = (
44
+ await Promise.all(
45
+ ALL_TOOLS.map(async ({ entry: navEntry }) => {
46
+ const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
47
+ if (!loader) return null;
48
+ const navContent = await loader();
49
+ return {
50
+ id: navEntry.id,
51
+ title: navContent.title,
52
+ href: `/${locale}/${navContent.slug}`,
53
+ isActive: navEntry.id === entry.id,
54
+ };
55
+ }),
56
+ )
57
+ ).filter(Boolean) as NavItem[];
58
+ paths.push({
59
+ params: { locale, slug: content.slug },
60
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
61
+ });
62
+ }
63
+ }
64
+
65
+ return paths;
66
+ }
67
+
68
+ interface NavItem {
69
+ id: string;
70
+ title: string;
71
+ href: string;
72
+ isActive?: boolean;
73
+ }
74
+
75
+ type PageProps = {
76
+ Component: typeof BookPaginationComponent;
77
+ locale: KnownLocale;
78
+ content: ToolLocaleContent<BookPaginationUI>;
79
+ localeUrls: Partial<Record<KnownLocale, string>>;
80
+ allToolsNav: NavItem[];
81
+ englishSlug: string;
82
+ };
83
+
84
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props as PageProps;
85
+
86
+ const cssFiles = import.meta.glob<string>("../../tool/*/*.css", { query: "?raw", import: "default" });
87
+ const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
88
+ const cssLoader = cssKey ? cssFiles[cssKey] : undefined;
89
+ const toolCss = cssLoader ? await cssLoader() : "";
90
+
91
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
92
+
93
+ const words = content.title.split(" ");
94
+ const titleHighlight = words[0] || "";
95
+ const titleBase = words.slice(1).join(" ") || "";
96
+ ---
97
+
98
+ <PreviewLayout
99
+ title={content.title}
100
+ currentLocale={locale}
101
+ localeUrls={localeUrls}
102
+ hasSidebar={true}
103
+ >
104
+ <PreviewNavSidebar
105
+ slot="sidebar"
106
+ categoryTitle="Tools"
107
+ tools={allToolsNav}
108
+ />
109
+ <Fragment slot="head">
110
+ {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
111
+ {
112
+ ( content.schemas ?? []).map((schema: unknown) => (
113
+ <script
114
+ is:inline
115
+ type="application/ld+json"
116
+ set:html={JSON.stringify(schema)}
117
+ />
118
+ ))
119
+ }
120
+ </Fragment>
121
+
122
+ <div class="tool-page">
123
+ <UtilityHeader
124
+ titleHighlight={titleHighlight}
125
+ titleBase={titleBase}
126
+ description={content.description}
127
+ />
128
+
129
+ <section class="section-tool">
130
+ <Component ui={content.ui} />
131
+ </section>
132
+
133
+ <section class="section-seo">
134
+ <SEORenderer content={seoContent} />
135
+ </section>
136
+
137
+ <section class="section-faq">
138
+ <FAQSection items={content.faq} />
139
+ </section>
140
+
141
+ <section class="section-bibliography">
142
+ <Bibliography links={content.bibliography} />
143
+ </section>
144
+ </div>
145
+ </PreviewLayout>
146
+
147
+ <style>
148
+ .tool-page {
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 2rem;
152
+ }
153
+ .section-tool {
154
+ max-width: 1200px;
155
+ margin: 0 auto;
156
+ width: 100%;
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>