@jjlmoya/utils-audiovisual 1.16.0 → 1.18.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 (234) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +1 -0
  5. package/src/pages/[locale]/[slug].astro +28 -12
  6. package/src/tests/locale_completeness.test.ts +2 -36
  7. package/src/tests/shared-test-helpers.ts +56 -0
  8. package/src/tests/tool_exports.test.ts +34 -0
  9. package/src/tests/tool_validation.test.ts +2 -2
  10. package/src/tool/chromaticLens/bibliography.ts +12 -0
  11. package/src/tool/chromaticLens/chromatic-lens-color-palette-extraction-online.css +39 -39
  12. package/src/tool/chromaticLens/entry.ts +2 -0
  13. package/src/tool/chromaticLens/i18n/de.ts +1 -15
  14. package/src/tool/chromaticLens/i18n/en.ts +1 -15
  15. package/src/tool/chromaticLens/i18n/es.ts +1 -13
  16. package/src/tool/chromaticLens/i18n/fr.ts +1 -13
  17. package/src/tool/chromaticLens/i18n/id.ts +1 -15
  18. package/src/tool/chromaticLens/i18n/it.ts +1 -15
  19. package/src/tool/chromaticLens/i18n/ja.ts +1 -15
  20. package/src/tool/chromaticLens/i18n/ko.ts +1 -15
  21. package/src/tool/chromaticLens/i18n/nl.ts +1 -15
  22. package/src/tool/chromaticLens/i18n/pl.ts +1 -15
  23. package/src/tool/chromaticLens/i18n/pt.ts +1 -15
  24. package/src/tool/chromaticLens/i18n/ru.ts +1 -15
  25. package/src/tool/chromaticLens/i18n/sv.ts +1 -15
  26. package/src/tool/chromaticLens/i18n/tr.ts +1 -15
  27. package/src/tool/chromaticLens/i18n/zh.ts +1 -15
  28. package/src/tool/chromaticLens/seo.astro +1 -1
  29. package/src/tool/collageMaker/bibliography.ts +8 -0
  30. package/src/tool/collageMaker/entry.ts +2 -0
  31. package/src/tool/collageMaker/free-online-photo-collage-maker-professional-compositions.css +47 -47
  32. package/src/tool/collageMaker/i18n/de.ts +1 -11
  33. package/src/tool/collageMaker/i18n/en.ts +1 -11
  34. package/src/tool/collageMaker/i18n/es.ts +1 -9
  35. package/src/tool/collageMaker/i18n/fr.ts +1 -9
  36. package/src/tool/collageMaker/i18n/id.ts +1 -11
  37. package/src/tool/collageMaker/i18n/it.ts +1 -11
  38. package/src/tool/collageMaker/i18n/ja.ts +1 -11
  39. package/src/tool/collageMaker/i18n/ko.ts +1 -11
  40. package/src/tool/collageMaker/i18n/nl.ts +1 -11
  41. package/src/tool/collageMaker/i18n/pl.ts +1 -11
  42. package/src/tool/collageMaker/i18n/pt.ts +1 -11
  43. package/src/tool/collageMaker/i18n/ru.ts +1 -11
  44. package/src/tool/collageMaker/i18n/sv.ts +1 -11
  45. package/src/tool/collageMaker/i18n/tr.ts +1 -11
  46. package/src/tool/collageMaker/i18n/zh.ts +1 -11
  47. package/src/tool/collageMaker/seo.astro +1 -1
  48. package/src/tool/depthOfFieldCalculator/bibliography.astro +15 -0
  49. package/src/tool/depthOfFieldCalculator/bibliography.ts +20 -0
  50. package/src/tool/depthOfFieldCalculator/component.astro +341 -0
  51. package/src/tool/depthOfFieldCalculator/depth-of-field-calculator.css +417 -0
  52. package/src/tool/depthOfFieldCalculator/entry.ts +52 -0
  53. package/src/tool/depthOfFieldCalculator/i18n/de.ts +141 -0
  54. package/src/tool/depthOfFieldCalculator/i18n/en.ts +141 -0
  55. package/src/tool/depthOfFieldCalculator/i18n/es.ts +141 -0
  56. package/src/tool/depthOfFieldCalculator/i18n/fr.ts +141 -0
  57. package/src/tool/depthOfFieldCalculator/i18n/id.ts +141 -0
  58. package/src/tool/depthOfFieldCalculator/i18n/it.ts +141 -0
  59. package/src/tool/depthOfFieldCalculator/i18n/ja.ts +141 -0
  60. package/src/tool/depthOfFieldCalculator/i18n/ko.ts +141 -0
  61. package/src/tool/depthOfFieldCalculator/i18n/nl.ts +141 -0
  62. package/src/tool/depthOfFieldCalculator/i18n/pl.ts +141 -0
  63. package/src/tool/depthOfFieldCalculator/i18n/pt.ts +141 -0
  64. package/src/tool/depthOfFieldCalculator/i18n/ru.ts +141 -0
  65. package/src/tool/depthOfFieldCalculator/i18n/sv.ts +141 -0
  66. package/src/tool/depthOfFieldCalculator/i18n/tr.ts +141 -0
  67. package/src/tool/depthOfFieldCalculator/i18n/zh.ts +141 -0
  68. package/src/tool/depthOfFieldCalculator/index.ts +10 -0
  69. package/src/tool/depthOfFieldCalculator/logic.ts +91 -0
  70. package/src/tool/depthOfFieldCalculator/seo.astro +15 -0
  71. package/src/tool/exifCleaner/bibliography.ts +12 -0
  72. package/src/tool/exifCleaner/entry.ts +2 -0
  73. package/src/tool/exifCleaner/exif-metadata-cleaner-remove-gps-photo-privacy.css +36 -36
  74. package/src/tool/exifCleaner/i18n/de.ts +1 -15
  75. package/src/tool/exifCleaner/i18n/en.ts +1 -15
  76. package/src/tool/exifCleaner/i18n/es.ts +1 -15
  77. package/src/tool/exifCleaner/i18n/fr.ts +1 -15
  78. package/src/tool/exifCleaner/i18n/id.ts +1 -15
  79. package/src/tool/exifCleaner/i18n/it.ts +1 -15
  80. package/src/tool/exifCleaner/i18n/ja.ts +1 -15
  81. package/src/tool/exifCleaner/i18n/ko.ts +1 -15
  82. package/src/tool/exifCleaner/i18n/nl.ts +1 -15
  83. package/src/tool/exifCleaner/i18n/pl.ts +1 -15
  84. package/src/tool/exifCleaner/i18n/pt.ts +1 -15
  85. package/src/tool/exifCleaner/i18n/ru.ts +1 -15
  86. package/src/tool/exifCleaner/i18n/sv.ts +1 -15
  87. package/src/tool/exifCleaner/i18n/tr.ts +1 -15
  88. package/src/tool/exifCleaner/i18n/zh.ts +1 -15
  89. package/src/tool/exifCleaner/seo.astro +2 -5
  90. package/src/tool/imageCompressor/bibliography.ts +12 -0
  91. package/src/tool/imageCompressor/entry.ts +2 -0
  92. package/src/tool/imageCompressor/i18n/de.ts +1 -15
  93. package/src/tool/imageCompressor/i18n/en.ts +1 -15
  94. package/src/tool/imageCompressor/i18n/es.ts +1 -13
  95. package/src/tool/imageCompressor/i18n/fr.ts +1 -13
  96. package/src/tool/imageCompressor/i18n/id.ts +1 -15
  97. package/src/tool/imageCompressor/i18n/it.ts +1 -15
  98. package/src/tool/imageCompressor/i18n/ja.ts +1 -15
  99. package/src/tool/imageCompressor/i18n/ko.ts +1 -15
  100. package/src/tool/imageCompressor/i18n/nl.ts +1 -15
  101. package/src/tool/imageCompressor/i18n/pl.ts +1 -15
  102. package/src/tool/imageCompressor/i18n/pt.ts +1 -15
  103. package/src/tool/imageCompressor/i18n/ru.ts +1 -15
  104. package/src/tool/imageCompressor/i18n/sv.ts +1 -15
  105. package/src/tool/imageCompressor/i18n/tr.ts +1 -15
  106. package/src/tool/imageCompressor/i18n/zh.ts +1 -15
  107. package/src/tool/imageCompressor/online-image-compressor-reduce-file-size-no-quality-loss.css +71 -64
  108. package/src/tool/imageCompressor/seo.astro +1 -1
  109. package/src/tool/printQualityCalculator/bibliography.ts +12 -0
  110. package/src/tool/printQualityCalculator/entry.ts +2 -0
  111. package/src/tool/printQualityCalculator/i18n/de.ts +1 -15
  112. package/src/tool/printQualityCalculator/i18n/en.ts +1 -15
  113. package/src/tool/printQualityCalculator/i18n/es.ts +1 -13
  114. package/src/tool/printQualityCalculator/i18n/fr.ts +1 -13
  115. package/src/tool/printQualityCalculator/i18n/id.ts +1 -15
  116. package/src/tool/printQualityCalculator/i18n/it.ts +1 -15
  117. package/src/tool/printQualityCalculator/i18n/ja.ts +1 -15
  118. package/src/tool/printQualityCalculator/i18n/ko.ts +1 -15
  119. package/src/tool/printQualityCalculator/i18n/nl.ts +1 -15
  120. package/src/tool/printQualityCalculator/i18n/pl.ts +1 -15
  121. package/src/tool/printQualityCalculator/i18n/pt.ts +1 -15
  122. package/src/tool/printQualityCalculator/i18n/ru.ts +1 -15
  123. package/src/tool/printQualityCalculator/i18n/sv.ts +1 -15
  124. package/src/tool/printQualityCalculator/i18n/tr.ts +1 -15
  125. package/src/tool/printQualityCalculator/i18n/zh.ts +1 -15
  126. package/src/tool/printQualityCalculator/print-quality-calculator-pixels-to-cm-dpi.css +89 -81
  127. package/src/tool/printQualityCalculator/seo.astro +2 -5
  128. package/src/tool/privacyBlur/bibliography.ts +8 -0
  129. package/src/tool/privacyBlur/entry.ts +2 -0
  130. package/src/tool/privacyBlur/i18n/de.ts +1 -11
  131. package/src/tool/privacyBlur/i18n/en.ts +1 -11
  132. package/src/tool/privacyBlur/i18n/es.ts +1 -9
  133. package/src/tool/privacyBlur/i18n/fr.ts +1 -9
  134. package/src/tool/privacyBlur/i18n/id.ts +1 -11
  135. package/src/tool/privacyBlur/i18n/it.ts +1 -11
  136. package/src/tool/privacyBlur/i18n/ja.ts +1 -11
  137. package/src/tool/privacyBlur/i18n/ko.ts +1 -11
  138. package/src/tool/privacyBlur/i18n/nl.ts +1 -11
  139. package/src/tool/privacyBlur/i18n/pl.ts +1 -11
  140. package/src/tool/privacyBlur/i18n/pt.ts +1 -11
  141. package/src/tool/privacyBlur/i18n/ru.ts +1 -11
  142. package/src/tool/privacyBlur/i18n/sv.ts +1 -11
  143. package/src/tool/privacyBlur/i18n/tr.ts +1 -11
  144. package/src/tool/privacyBlur/i18n/zh.ts +1 -11
  145. package/src/tool/privacyBlur/online-privacy-editor-pixelate-blur-faces-photos.css +36 -36
  146. package/src/tool/privacyBlur/seo.astro +1 -1
  147. package/src/tool/subtitleSync/bibliography.ts +12 -0
  148. package/src/tool/subtitleSync/entry.ts +2 -0
  149. package/src/tool/subtitleSync/i18n/de.ts +1 -13
  150. package/src/tool/subtitleSync/i18n/en.ts +1 -13
  151. package/src/tool/subtitleSync/i18n/es.ts +1 -13
  152. package/src/tool/subtitleSync/i18n/fr.ts +1 -13
  153. package/src/tool/subtitleSync/i18n/id.ts +1 -13
  154. package/src/tool/subtitleSync/i18n/it.ts +1 -13
  155. package/src/tool/subtitleSync/i18n/ja.ts +1 -13
  156. package/src/tool/subtitleSync/i18n/ko.ts +1 -13
  157. package/src/tool/subtitleSync/i18n/nl.ts +1 -13
  158. package/src/tool/subtitleSync/i18n/pl.ts +1 -13
  159. package/src/tool/subtitleSync/i18n/pt.ts +1 -13
  160. package/src/tool/subtitleSync/i18n/ru.ts +1 -13
  161. package/src/tool/subtitleSync/i18n/sv.ts +1 -13
  162. package/src/tool/subtitleSync/i18n/tr.ts +1 -13
  163. package/src/tool/subtitleSync/i18n/zh.ts +1 -13
  164. package/src/tool/subtitleSync/seo.astro +1 -1
  165. package/src/tool/subtitleSync/synchronize-srt-subtitles-online-adjust-timing-free.css +43 -43
  166. package/src/tool/timelapseCalculator/bibliography.ts +20 -0
  167. package/src/tool/timelapseCalculator/entry.ts +2 -0
  168. package/src/tool/timelapseCalculator/i18n/de.ts +1 -21
  169. package/src/tool/timelapseCalculator/i18n/en.ts +1 -21
  170. package/src/tool/timelapseCalculator/i18n/es.ts +1 -21
  171. package/src/tool/timelapseCalculator/i18n/fr.ts +1 -21
  172. package/src/tool/timelapseCalculator/i18n/id.ts +1 -21
  173. package/src/tool/timelapseCalculator/i18n/it.ts +1 -21
  174. package/src/tool/timelapseCalculator/i18n/ja.ts +1 -21
  175. package/src/tool/timelapseCalculator/i18n/ko.ts +1 -21
  176. package/src/tool/timelapseCalculator/i18n/nl.ts +1 -21
  177. package/src/tool/timelapseCalculator/i18n/pl.ts +1 -21
  178. package/src/tool/timelapseCalculator/i18n/pt.ts +1 -21
  179. package/src/tool/timelapseCalculator/i18n/ru.ts +1 -21
  180. package/src/tool/timelapseCalculator/i18n/sv.ts +1 -21
  181. package/src/tool/timelapseCalculator/i18n/tr.ts +1 -21
  182. package/src/tool/timelapseCalculator/i18n/zh.ts +1 -21
  183. package/src/tool/timelapseCalculator/seo.astro +2 -5
  184. package/src/tool/timelapseCalculator/timelapse-hyperlapse-calculator-perfect-intervals.css +44 -42
  185. package/src/tool/tvDistance/bibliography.ts +12 -0
  186. package/src/tool/tvDistance/entry.ts +2 -0
  187. package/src/tool/tvDistance/i18n/de.ts +1 -13
  188. package/src/tool/tvDistance/i18n/en.ts +1 -13
  189. package/src/tool/tvDistance/i18n/es.ts +1 -13
  190. package/src/tool/tvDistance/i18n/fr.ts +1 -13
  191. package/src/tool/tvDistance/i18n/id.ts +1 -13
  192. package/src/tool/tvDistance/i18n/it.ts +1 -13
  193. package/src/tool/tvDistance/i18n/ja.ts +1 -13
  194. package/src/tool/tvDistance/i18n/ko.ts +1 -13
  195. package/src/tool/tvDistance/i18n/nl.ts +1 -13
  196. package/src/tool/tvDistance/i18n/pl.ts +1 -13
  197. package/src/tool/tvDistance/i18n/pt.ts +1 -13
  198. package/src/tool/tvDistance/i18n/ru.ts +1 -13
  199. package/src/tool/tvDistance/i18n/sv.ts +1 -13
  200. package/src/tool/tvDistance/i18n/tr.ts +1 -13
  201. package/src/tool/tvDistance/i18n/zh.ts +1 -13
  202. package/src/tool/tvDistance/seo.astro +1 -1
  203. package/src/tool/tvDistance/tv-viewing-distance-calculator-thx-4k-optimal-screen.css +56 -56
  204. package/src/tool/videoFrameExtractor/bibliography.ts +8 -0
  205. package/src/tool/videoFrameExtractor/entry.ts +2 -0
  206. package/src/tool/videoFrameExtractor/i18n/de.ts +1 -9
  207. package/src/tool/videoFrameExtractor/i18n/en.ts +1 -9
  208. package/src/tool/videoFrameExtractor/i18n/es.ts +1 -9
  209. package/src/tool/videoFrameExtractor/i18n/fr.ts +1 -8
  210. package/src/tool/videoFrameExtractor/i18n/id.ts +1 -9
  211. package/src/tool/videoFrameExtractor/i18n/it.ts +1 -8
  212. package/src/tool/videoFrameExtractor/i18n/ja.ts +1 -9
  213. package/src/tool/videoFrameExtractor/i18n/ko.ts +1 -9
  214. package/src/tool/videoFrameExtractor/i18n/nl.ts +1 -9
  215. package/src/tool/videoFrameExtractor/i18n/pl.ts +1 -9
  216. package/src/tool/videoFrameExtractor/i18n/pt.ts +1 -9
  217. package/src/tool/videoFrameExtractor/i18n/ru.ts +1 -9
  218. package/src/tool/videoFrameExtractor/i18n/sv.ts +1 -9
  219. package/src/tool/videoFrameExtractor/i18n/tr.ts +1 -9
  220. package/src/tool/videoFrameExtractor/i18n/zh.ts +1 -9
  221. package/src/tool/videoFrameExtractor/online-video-frame-extractor-capture-hd-stills.css +55 -55
  222. package/src/tool/videoFrameExtractor/seo.astro +1 -1
  223. package/src/tools.ts +2 -0
  224. package/src/types.ts +0 -2
  225. package/src/tool/chromaticLens/style.css +0 -308
  226. package/src/tool/collageMaker/style.css +0 -386
  227. package/src/tool/exifCleaner/style.css +0 -289
  228. package/src/tool/imageCompressor/style.css +0 -503
  229. package/src/tool/printQualityCalculator/style.css +0 -491
  230. package/src/tool/privacyBlur/style.css +0 -332
  231. package/src/tool/subtitleSync/style.css +0 -325
  232. package/src/tool/timelapseCalculator/style.css +0 -285
  233. package/src/tool/tvDistance/style.css +0 -435
  234. package/src/tool/videoFrameExtractor/style.css +0 -426
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-audiovisual",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -9,6 +9,7 @@ import { tvDistance } from '../tool/tvDistance/index';
9
9
  import { imageCompressor } from '../tool/imageCompressor/index';
10
10
  import { collageMaker } from '../tool/collageMaker/index';
11
11
  import { videoFrameExtractor } from '../tool/videoFrameExtractor/index';
12
+ import { depthOfFieldCalculator } from '../tool/depthOfFieldCalculator/index';
12
13
 
13
14
  export const audiovisualCategory: AudiovisualCategoryEntry = {
14
15
  icon: 'mdi:camera-iris',
@@ -23,6 +24,7 @@ export const audiovisualCategory: AudiovisualCategoryEntry = {
23
24
  imageCompressor as AudiovisualToolEntry,
24
25
  collageMaker as AudiovisualToolEntry,
25
26
  videoFrameExtractor as AudiovisualToolEntry,
27
+ depthOfFieldCalculator as AudiovisualToolEntry,
26
28
  ],
27
29
  i18n: {
28
30
  es: async () => (await import('./i18n/es')).content,
package/src/entries.ts CHANGED
@@ -18,6 +18,8 @@ export { tvDistance } from './tool/tvDistance/entry';
18
18
  export type { TvDistanceUI, TvDistanceLocaleContent } from './tool/tvDistance/entry';
19
19
  export { videoFrameExtractor } from './tool/videoFrameExtractor/entry';
20
20
  export type { VideoFrameExtractorUI, VideoFrameExtractorLocaleContent } from './tool/videoFrameExtractor/entry';
21
+ export { depthOfFieldCalculator } from './tool/depthOfFieldCalculator/entry';
22
+ export type { DepthOfFieldUI, DepthOfFieldLocaleContent } from './tool/depthOfFieldCalculator/entry';
21
23
  export { audiovisualCategory, toolsCategory } from './category';
22
24
  import { chromaticLens } from './tool/chromaticLens/entry';
23
25
  import { collageMaker } from './tool/collageMaker/entry';
@@ -29,4 +31,5 @@ import { subtitleSync } from './tool/subtitleSync/entry';
29
31
  import { timelapseCalculator } from './tool/timelapseCalculator/entry';
30
32
  import { tvDistance } from './tool/tvDistance/entry';
31
33
  import { videoFrameExtractor } from './tool/videoFrameExtractor/entry';
32
- export const ALL_ENTRIES = [chromaticLens, collageMaker, exifCleaner, imageCompressor, printQualityCalculator, privacyBlur, subtitleSync, timelapseCalculator, tvDistance, videoFrameExtractor];
34
+ import { depthOfFieldCalculator } from './tool/depthOfFieldCalculator/entry';
35
+ export const ALL_ENTRIES = [chromaticLens, collageMaker, exifCleaner, imageCompressor, printQualityCalculator, privacyBlur, subtitleSync, timelapseCalculator, tvDistance, videoFrameExtractor, depthOfFieldCalculator];
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export * from './tool/imageCompressor';
13
13
  export * from './tool/collageMaker';
14
14
  export * from './tool/videoFrameExtractor';
15
15
  export * from './tool/privacyBlur';
16
+ export * from './tool/depthOfFieldCalculator';
16
17
 
17
18
  export type {
18
19
  KnownLocale,
@@ -34,18 +34,28 @@ export async function getStaticPaths() {
34
34
  ]),
35
35
  ) as Partial<Record<KnownLocale, string>>;
36
36
 
37
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
+
37
40
  for (const { locale, content } of localeContents) {
38
- const allToolsNav = await Promise.all(
39
- ALL_TOOLS.map(async ({ entry: navEntry }) => ({
40
- id: navEntry.id,
41
- title: (await navEntry.i18n[locale]!()).title,
42
- href: `/${locale}/${(await navEntry.i18n[locale]!()).slug}`,
43
- isActive: navEntry.id === entry.id,
44
- })),
45
- );
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[];
46
56
  paths.push({
47
57
  params: { locale, slug: content.slug },
48
- props: { Component, locale, content, localeUrls, allToolsNav },
58
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
49
59
  });
50
60
  }
51
61
  }
@@ -66,11 +76,16 @@ interface Props {
66
76
  content: ToolLocaleContent;
67
77
  localeUrls: Partial<Record<KnownLocale, string>>;
68
78
  allToolsNav: NavItem[];
79
+ englishSlug: string;
69
80
  }
70
81
 
71
- const { Component, locale, content, localeUrls, allToolsNav } = Astro.props;
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 toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
72
87
 
73
- const seoContent: UtilitySEOContent = { locale, sections: content.seo };
88
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
74
89
 
75
90
  const words = content.title.split(" ");
76
91
  const titleHighlight = words[0] || "";
@@ -89,8 +104,9 @@ const titleBase = words.slice(1).join(" ") || "";
89
104
  tools={allToolsNav}
90
105
  />
91
106
  <Fragment slot="head">
107
+ <Fragment set:html={toolCss ? `<style>${toolCss}</style>` : ''} />
92
108
  {
93
- content.schemas.map((schema: unknown) => (
109
+ ( content.schemas ?? []).map((schema: unknown) => (
94
110
  <script
95
111
  is:inline
96
112
  type="application/ld+json"
@@ -1,42 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { ALL_TOOLS } from '../tools';
3
- import type { ToolLocaleContent } from '../types';
4
3
 
5
4
  describe('Locale Completeness Validation', () => {
6
- ALL_TOOLS.forEach((tool) => {
7
- describe(`Tool: ${tool.entry.id}`, () => {
8
- Object.keys(tool.entry.i18n).forEach((locale) => {
9
- describe(`Locale: ${locale}`, () => {
10
- it('faqTitle should be defined when faq items exist', async () => {
11
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
12
- const content = (await loader?.()) as ToolLocaleContent;
13
-
14
- if (content.faq.length > 0) {
15
- expect(
16
- content.faqTitle,
17
- `Tool "${tool.entry.id}" locale "${locale}" has ${content.faq.length} FAQ items but is missing faqTitle`,
18
- ).toBeTruthy();
19
- }
20
- });
21
-
22
- it('bibliographyTitle should be defined when bibliography items exist', async () => {
23
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
24
- const content = (await loader?.()) as ToolLocaleContent;
25
-
26
- if (content.bibliography.length > 0) {
27
- expect(
28
- content.bibliographyTitle,
29
- `Tool "${tool.entry.id}" locale "${locale}" has ${content.bibliography.length} bibliography items but is missing bibliographyTitle`,
30
- ).toBeTruthy();
31
- }
32
- });
33
- });
34
- });
35
- });
36
- });
37
-
38
- it('all 10 tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(10);
5
+ it('all 11 tools registered', () => {
6
+ expect(ALL_TOOLS.length).toBe(11);
40
7
  });
41
8
  });
42
-
@@ -0,0 +1,56 @@
1
+ import type { ToolDefinition } from '../types';
2
+
3
+ export interface ToolExportValidationResult {
4
+ passed: boolean;
5
+ failures: string[];
6
+ }
7
+
8
+ function validateComponentType(
9
+ toolId: string,
10
+ componentName: string,
11
+ component: unknown,
12
+ failures: string[],
13
+ ): void {
14
+ if (typeof component !== 'function') {
15
+ failures.push(`${toolId}: ${componentName} is not a function (${typeof component})`);
16
+ }
17
+ }
18
+
19
+ async function validateComponentExecution(
20
+ toolId: string,
21
+ componentName: string,
22
+ fn: () => Promise<unknown>,
23
+ failures: string[],
24
+ ): Promise<void> {
25
+ try {
26
+ const result = await fn();
27
+ if (!result || typeof result !== 'object') {
28
+ failures.push(`${toolId}: ${componentName} import returned invalid result`);
29
+ }
30
+ } catch (error) {
31
+ failures.push(`${toolId}: ${componentName} execution error - ${error instanceof Error ? error.message : 'unknown'}`);
32
+ }
33
+ }
34
+
35
+ export async function validateToolExports(tools: ToolDefinition[]): Promise<ToolExportValidationResult> {
36
+ const failures: string[] = [];
37
+
38
+ for (const tool of tools) {
39
+ validateComponentType(tool.entry.id, 'Component', tool.Component, failures);
40
+ validateComponentType(tool.entry.id, 'SEOComponent', tool.SEOComponent, failures);
41
+ validateComponentType(tool.entry.id, 'BibliographyComponent', tool.BibliographyComponent, failures);
42
+
43
+ const componentFn = tool.Component as () => Promise<unknown>;
44
+ const seoFn = tool.SEOComponent as () => Promise<unknown>;
45
+ const bibFn = tool.BibliographyComponent as () => Promise<unknown>;
46
+
47
+ await validateComponentExecution(tool.entry.id, 'Component', componentFn, failures);
48
+ await validateComponentExecution(tool.entry.id, 'SEOComponent', seoFn, failures);
49
+ await validateComponentExecution(tool.entry.id, 'BibliographyComponent', bibFn, failures);
50
+ }
51
+
52
+ return {
53
+ passed: failures.length === 0,
54
+ failures,
55
+ };
56
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import { validateToolExports } from './shared-test-helpers';
4
+
5
+ describe('Tool Exports Pattern Validation', () => {
6
+ describe('Component Exports Format', () => {
7
+ ALL_TOOLS.forEach((tool) => {
8
+ it(`${tool.entry.id}: Component should be a lazy-loaded function`, () => {
9
+ expect(typeof tool.Component).toBe('function');
10
+ expect(tool.Component).toBeInstanceOf(Function);
11
+ });
12
+
13
+ it(`${tool.entry.id}: SEOComponent should be a lazy-loaded function`, () => {
14
+ expect(typeof tool.SEOComponent).toBe('function');
15
+ expect(tool.SEOComponent).toBeInstanceOf(Function);
16
+ });
17
+
18
+ it(`${tool.entry.id}: BibliographyComponent should be a lazy-loaded function`, () => {
19
+ expect(typeof tool.BibliographyComponent).toBe('function');
20
+ expect(tool.BibliographyComponent).toBeInstanceOf(Function);
21
+ });
22
+ });
23
+ });
24
+
25
+ describe('Dynamic Import Validation', () => {
26
+ it('all tools must have functional dynamic imports', async () => {
27
+ const result = await validateToolExports(ALL_TOOLS);
28
+ if (!result.passed) {
29
+ throw new Error(`Tool export validation failed:\n${result.failures.join('\n')}`);
30
+ }
31
+ expect(result.passed).toBe(true);
32
+ });
33
+ });
34
+ });
@@ -4,8 +4,8 @@ import { audiovisualCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 10 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(10);
7
+ it('should have 11 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(11);
9
9
  });
10
10
 
11
11
  it('audiovisualCategory should be defined', () => {
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: "Median Cut Algorithm - Wikipedia",
6
+ url: "https://en.wikipedia.org/wiki/Median_cut",
7
+ },
8
+ {
9
+ name: "Farblehre für Designer",
10
+ url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
11
+ },
12
+ ];
@@ -1,4 +1,4 @@
1
- :global(.cl-root) {
1
+ .cl-root {
2
2
  --cl-bg: #fff;
3
3
  --cl-bg-elevated: #f8fafc;
4
4
  --cl-border: #e2e8f0;
@@ -16,7 +16,7 @@
16
16
  margin: 0 auto;
17
17
  }
18
18
 
19
- :global(.theme-dark .cl-root) {
19
+ .theme-dark .cl-root {
20
20
  --cl-bg: #18181b;
21
21
  --cl-bg-elevated: #27272a;
22
22
  --cl-border: #3f3f46;
@@ -30,7 +30,7 @@
30
30
  --cl-shadow: rgba(0, 0, 0, 0.5);
31
31
  }
32
32
 
33
- :global(.cl-card) {
33
+ .cl-card {
34
34
  background: var(--cl-bg);
35
35
  border: 1px solid var(--cl-border);
36
36
  border-radius: 3rem;
@@ -40,7 +40,7 @@
40
40
  overflow: hidden;
41
41
  }
42
42
 
43
- :global(.cl-drop) {
43
+ .cl-drop {
44
44
  padding: 5rem 2rem;
45
45
  display: flex;
46
46
  flex-direction: column;
@@ -53,13 +53,13 @@
53
53
  gap: 0.5rem;
54
54
  }
55
55
 
56
- :global(.cl-drop:hover),
57
- :global(.cl-drop-active) {
56
+ .cl-drop:hover,
57
+ .cl-drop-active {
58
58
  background: var(--cl-accent-alpha-hover);
59
59
  border-color: var(--cl-accent);
60
60
  }
61
61
 
62
- :global(.cl-drop-icon) {
62
+ .cl-drop-icon {
63
63
  width: 5rem;
64
64
  height: 5rem;
65
65
  background: var(--cl-accent-alpha);
@@ -71,30 +71,30 @@
71
71
  color: var(--cl-accent);
72
72
  }
73
73
 
74
- :global(.cl-drop-icon svg) {
74
+ .cl-drop-icon svg {
75
75
  width: 2.5rem;
76
76
  height: 2.5rem;
77
77
  }
78
78
 
79
- :global(.cl-drop-title) {
79
+ .cl-drop-title {
80
80
  font-size: 2rem;
81
81
  font-weight: 950;
82
82
  color: var(--cl-text);
83
83
  margin: 0;
84
84
  }
85
85
 
86
- :global(.cl-drop-sub) {
86
+ .cl-drop-sub {
87
87
  font-size: 1.1rem;
88
88
  color: var(--cl-text-muted);
89
89
  margin: 0;
90
90
  font-weight: 600;
91
91
  }
92
92
 
93
- :global(.cl-workspace) {
93
+ .cl-workspace {
94
94
  padding: 1.5rem;
95
95
  }
96
96
 
97
- :global(.cl-mini-drop) {
97
+ .cl-mini-drop {
98
98
  display: inline-flex;
99
99
  align-items: center;
100
100
  gap: 0.75rem;
@@ -110,17 +110,17 @@
110
110
  transition: border-color 0.2s, color 0.2s;
111
111
  }
112
112
 
113
- :global(.cl-mini-drop:hover) {
113
+ .cl-mini-drop:hover {
114
114
  border-color: var(--cl-accent);
115
115
  color: var(--cl-accent);
116
116
  }
117
117
 
118
- :global(.cl-mini-drop svg) {
118
+ .cl-mini-drop svg {
119
119
  width: 1.1rem;
120
120
  height: 1.1rem;
121
121
  }
122
122
 
123
- :global(.cl-config-bar) {
123
+ .cl-config-bar {
124
124
  padding: 1rem 0;
125
125
  margin-bottom: 2rem;
126
126
  border-bottom: 1px solid var(--cl-border);
@@ -128,13 +128,13 @@
128
128
  justify-content: flex-end;
129
129
  }
130
130
 
131
- :global(.cl-config-item) {
131
+ .cl-config-item {
132
132
  display: flex;
133
133
  align-items: center;
134
134
  gap: 1rem;
135
135
  }
136
136
 
137
- :global(.cl-config-label) {
137
+ .cl-config-label {
138
138
  font-size: 0.75rem;
139
139
  font-weight: 900;
140
140
  text-transform: uppercase;
@@ -142,7 +142,7 @@
142
142
  letter-spacing: 0.1em;
143
143
  }
144
144
 
145
- :global(.cl-count-select) {
145
+ .cl-count-select {
146
146
  padding: 0.5rem 1rem;
147
147
  border-radius: 0.75rem;
148
148
  background: var(--cl-bg-elevated);
@@ -152,58 +152,58 @@
152
152
  cursor: pointer;
153
153
  }
154
154
 
155
- :global(.cl-result-layout) {
155
+ .cl-result-layout {
156
156
  display: grid;
157
157
  grid-template-columns: 1fr 1.25fr;
158
158
  gap: 3rem;
159
159
  }
160
160
 
161
161
  @media (max-width: 800px) {
162
- :global(.cl-result-layout) {
162
+ .cl-result-layout {
163
163
  grid-template-columns: 1fr;
164
164
  }
165
165
  }
166
166
 
167
- :global(.cl-preview-col) {
167
+ .cl-preview-col {
168
168
  display: flex;
169
169
  flex-direction: column;
170
170
  gap: 1rem;
171
171
  }
172
172
 
173
- :global(.cl-preview-img) {
173
+ .cl-preview-img {
174
174
  width: 100%;
175
175
  border-radius: 1.5rem;
176
176
  box-shadow: 0 20px 40px var(--cl-shadow);
177
177
  display: block;
178
178
  }
179
179
 
180
- :global(.cl-palette-col) {
180
+ .cl-palette-col {
181
181
  display: flex;
182
182
  flex-direction: column;
183
183
  gap: 1.5rem;
184
184
  }
185
185
 
186
- :global(.cl-palette-header) {
186
+ .cl-palette-header {
187
187
  display: flex;
188
188
  align-items: center;
189
189
  gap: 1rem;
190
190
  color: var(--cl-accent);
191
191
  }
192
192
 
193
- :global(.cl-palette-header svg) {
193
+ .cl-palette-header svg {
194
194
  width: 1.25rem;
195
195
  height: 1.25rem;
196
196
  flex-shrink: 0;
197
197
  }
198
198
 
199
- :global(.cl-palette-header h4) {
199
+ .cl-palette-header h4 {
200
200
  font-size: 1.25rem;
201
201
  font-weight: 950;
202
202
  color: var(--cl-text);
203
203
  margin: 0;
204
204
  }
205
205
 
206
- :global(.cl-loader) {
206
+ .cl-loader {
207
207
  display: flex;
208
208
  flex-direction: column;
209
209
  align-items: center;
@@ -211,7 +211,7 @@
211
211
  padding: 2rem 0;
212
212
  }
213
213
 
214
- :global(.cl-spinner) {
214
+ .cl-spinner {
215
215
  width: 3rem;
216
216
  height: 3rem;
217
217
  border: 3px solid var(--cl-accent-alpha);
@@ -220,7 +220,7 @@
220
220
  animation: cl-spin 0.8s linear infinite;
221
221
  }
222
222
 
223
- :global(.cl-loader-text) {
223
+ .cl-loader-text {
224
224
  font-size: 0.75rem;
225
225
  font-weight: 900;
226
226
  text-transform: uppercase;
@@ -229,14 +229,14 @@
229
229
  margin: 0;
230
230
  }
231
231
 
232
- :global(.cl-swatches) {
232
+ .cl-swatches {
233
233
  display: flex;
234
234
  flex-direction: column;
235
235
  gap: 0.75rem;
236
236
  animation: cl-fade-up 0.5s ease;
237
237
  }
238
238
 
239
- :global(.cl-swatch) {
239
+ .cl-swatch {
240
240
  display: flex;
241
241
  align-items: center;
242
242
  gap: 1.25rem;
@@ -248,17 +248,17 @@
248
248
  transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), border-color 0.2s;
249
249
  }
250
250
 
251
- :global(.cl-swatch:hover) {
251
+ .cl-swatch:hover {
252
252
  transform: scale(1.02);
253
253
  border-color: var(--cl-accent);
254
254
  }
255
255
 
256
- :global(.cl-swatch-copied) {
256
+ .cl-swatch-copied {
257
257
  background: var(--cl-emerald-alpha);
258
258
  border-color: var(--cl-emerald);
259
259
  }
260
260
 
261
- :global(.cl-swatch-color) {
261
+ .cl-swatch-color {
262
262
  width: 3.5rem;
263
263
  height: 3.5rem;
264
264
  border-radius: 0.75rem;
@@ -266,19 +266,19 @@
266
266
  flex-shrink: 0;
267
267
  }
268
268
 
269
- :global(.cl-swatch-info) {
269
+ .cl-swatch-info {
270
270
  display: flex;
271
271
  flex-direction: column;
272
272
  gap: 0.2rem;
273
273
  }
274
274
 
275
- :global(.cl-swatch-hex) {
275
+ .cl-swatch-hex {
276
276
  font-weight: 950;
277
277
  color: var(--cl-text);
278
278
  font-size: 1.25rem;
279
279
  }
280
280
 
281
- :global(.cl-swatch-action) {
281
+ .cl-swatch-action {
282
282
  font-size: 0.7rem;
283
283
  font-weight: 900;
284
284
  color: var(--cl-text-muted);
@@ -286,7 +286,7 @@
286
286
  letter-spacing: 0.05em;
287
287
  }
288
288
 
289
- :global(.cl-hidden) {
289
+ .cl-hidden {
290
290
  display: none;
291
291
  }
292
292
 
@@ -305,4 +305,4 @@
305
305
  opacity: 1;
306
306
  transform: translateY(0);
307
307
  }
308
- }
308
+ }
@@ -37,3 +37,5 @@ export const chromaticLens: AudiovisualToolEntry<ChromaticLensUI> = {
37
37
  zh: async () => (await import('./i18n/zh')).content as unknown as ChromaticLensLocaleContent,
38
38
  },
39
39
  };
40
+
41
+ export { bibliography } from './bibliography';
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -14,8 +15,6 @@ const ui: ChromaticLensUI = {
14
15
  copiedLabel: "Kopiert!",
15
16
  colorCountLabel: "Anzahl der Farben",
16
17
  changeImage: "Bild ändern",
17
- faqTitle: "Häufig gestellte Fragen zur Farbetraktion",
18
- bibliographyTitle: "Ressourcen und technische Dokumentation"
19
18
  };
20
19
 
21
20
  const faq: ChromaticLensLocaleContent['faq'] = [
@@ -52,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
52
51
  },
53
52
  ];
54
53
 
55
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
56
- {
57
- name: "Median Cut Algorithm - Wikipedia",
58
- url: "https://en.wikipedia.org/wiki/Median_cut",
59
- },
60
- {
61
- name: "Farblehre für Designer",
62
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
63
- },
64
- ];
65
-
66
54
  const seo: ChromaticLensLocaleContent['seo'] = [
67
55
  {
68
56
  type: 'summary',
@@ -237,9 +225,7 @@ export const content: ChromaticLensLocaleContent = {
237
225
  description,
238
226
  ui,
239
227
  seo,
240
- faqTitle: "Häufig gestellte Fragen",
241
228
  faq,
242
- bibliographyTitle: "Referenzen",
243
229
  bibliography,
244
230
  howTo,
245
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
4
 
@@ -14,8 +15,6 @@ const ui: ChromaticLensUI = {
14
15
  copiedLabel: "Copied!",
15
16
  colorCountLabel: "Number of colors",
16
17
  changeImage: "Change image",
17
- faqTitle: "Frequently asked questions about color extraction",
18
- bibliographyTitle: "Resources and technical documentation"
19
18
  };
20
19
 
21
20
  const faq: ChromaticLensLocaleContent['faq'] = [
@@ -52,17 +51,6 @@ const howTo: ChromaticLensLocaleContent['howTo'] = [
52
51
  },
53
52
  ];
54
53
 
55
- const bibliography: ChromaticLensLocaleContent['bibliography'] = [
56
- {
57
- name: "Median Cut Algorithm - Wikipedia",
58
- url: "https://en.wikipedia.org/wiki/Median_cut",
59
- },
60
- {
61
- name: "Color Theory for Designers",
62
- url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
63
- },
64
- ];
65
-
66
54
  const seo: ChromaticLensLocaleContent['seo'] = [
67
55
  {
68
56
  type: 'summary',
@@ -237,9 +225,7 @@ export const content: ChromaticLensLocaleContent = {
237
225
  description,
238
226
  ui,
239
227
  seo,
240
- faqTitle: "Frequently Asked Questions",
241
228
  faq,
242
- bibliographyTitle: "References",
243
229
  bibliography,
244
230
  howTo,
245
231
  schemas: [faqSchema as any, howToSchema as any, appSchema],