@jjlmoya/utils-forensic-science 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 (316) hide show
  1. package/package.json +69 -0
  2. package/scripts/crystal-lattice-catalog-translations.mjs +459 -0
  3. package/scripts/postinstall.mjs +27 -0
  4. package/scripts/sync-crystal-lattice-i18n.mjs +50 -0
  5. package/src/category/i18n/de.ts +55 -0
  6. package/src/category/i18n/en.ts +55 -0
  7. package/src/category/i18n/es.ts +55 -0
  8. package/src/category/i18n/fr.ts +55 -0
  9. package/src/category/i18n/id.ts +55 -0
  10. package/src/category/i18n/it.ts +55 -0
  11. package/src/category/i18n/ja.ts +56 -0
  12. package/src/category/i18n/ko.ts +56 -0
  13. package/src/category/i18n/nl.ts +55 -0
  14. package/src/category/i18n/pl.ts +55 -0
  15. package/src/category/i18n/pt.ts +55 -0
  16. package/src/category/i18n/ru.ts +55 -0
  17. package/src/category/i18n/sv.ts +55 -0
  18. package/src/category/i18n/tr.ts +55 -0
  19. package/src/category/i18n/zh.ts +56 -0
  20. package/src/category/index.ts +48 -0
  21. package/src/category/seo.astro +15 -0
  22. package/src/components/PreviewNavSidebar.astro +116 -0
  23. package/src/components/PreviewToolbar.astro +143 -0
  24. package/src/data.ts +10 -0
  25. package/src/entries.ts +48 -0
  26. package/src/env.d.ts +5 -0
  27. package/src/index.ts +29 -0
  28. package/src/layouts/PreviewLayout.astro +117 -0
  29. package/src/pages/[locale]/[slug].astro +162 -0
  30. package/src/pages/[locale].astro +261 -0
  31. package/src/pages/index.astro +3 -0
  32. package/src/tests/diacritics_density.test.ts +140 -0
  33. package/src/tests/faq_count.test.ts +19 -0
  34. package/src/tests/i18n_coverage.test.ts +36 -0
  35. package/src/tests/inverted_punctuation.test.ts +84 -0
  36. package/src/tests/locale_completeness.test.ts +24 -0
  37. package/src/tests/mocks/astro_mock.js +2 -0
  38. package/src/tests/no_en_dash.test.ts +70 -0
  39. package/src/tests/no_h1_in_components.test.ts +48 -0
  40. package/src/tests/schemas_fulfillment.test.ts +23 -0
  41. package/src/tests/script_density.test.ts +94 -0
  42. package/src/tests/seo_length.test.ts +46 -0
  43. package/src/tests/shared-test-helpers.ts +56 -0
  44. package/src/tests/slug_language_code_format.test.ts +23 -0
  45. package/src/tests/slug_uniqueness.test.ts +81 -0
  46. package/src/tests/title_quality.test.ts +55 -0
  47. package/src/tests/tool_exports.test.ts +34 -0
  48. package/src/tests/tool_validation.test.ts +16 -0
  49. package/src/tests/widmark.test.ts +28 -0
  50. package/src/tool/forensic-age-estimator/bibliography.astro +14 -0
  51. package/src/tool/forensic-age-estimator/bibliography.ts +16 -0
  52. package/src/tool/forensic-age-estimator/component.astro +321 -0
  53. package/src/tool/forensic-age-estimator/dental-skeletal-third-molar-age-estimator.css +634 -0
  54. package/src/tool/forensic-age-estimator/entry.ts +32 -0
  55. package/src/tool/forensic-age-estimator/i18n/de.ts +245 -0
  56. package/src/tool/forensic-age-estimator/i18n/en.ts +245 -0
  57. package/src/tool/forensic-age-estimator/i18n/es.ts +245 -0
  58. package/src/tool/forensic-age-estimator/i18n/fr.ts +245 -0
  59. package/src/tool/forensic-age-estimator/i18n/id.ts +245 -0
  60. package/src/tool/forensic-age-estimator/i18n/it.ts +245 -0
  61. package/src/tool/forensic-age-estimator/i18n/ja.ts +245 -0
  62. package/src/tool/forensic-age-estimator/i18n/ko.ts +245 -0
  63. package/src/tool/forensic-age-estimator/i18n/nl.ts +245 -0
  64. package/src/tool/forensic-age-estimator/i18n/pl.ts +245 -0
  65. package/src/tool/forensic-age-estimator/i18n/pt.ts +245 -0
  66. package/src/tool/forensic-age-estimator/i18n/ru.ts +245 -0
  67. package/src/tool/forensic-age-estimator/i18n/sv.ts +245 -0
  68. package/src/tool/forensic-age-estimator/i18n/tr.ts +245 -0
  69. package/src/tool/forensic-age-estimator/i18n/zh.ts +246 -0
  70. package/src/tool/forensic-age-estimator/index.ts +11 -0
  71. package/src/tool/forensic-age-estimator/logic.ts +81 -0
  72. package/src/tool/forensic-age-estimator/seo.astro +15 -0
  73. package/src/tool/forensic-blood-test-simulator/bibliography.astro +6 -0
  74. package/src/tool/forensic-blood-test-simulator/bibliography.ts +12 -0
  75. package/src/tool/forensic-blood-test-simulator/component.astro +168 -0
  76. package/src/tool/forensic-blood-test-simulator/dom-utils.ts +40 -0
  77. package/src/tool/forensic-blood-test-simulator/entry.ts +32 -0
  78. package/src/tool/forensic-blood-test-simulator/forensic-presumptive-blood-testing-luminol-kastle-meyer-simulator.css +532 -0
  79. package/src/tool/forensic-blood-test-simulator/helpers.ts +16 -0
  80. package/src/tool/forensic-blood-test-simulator/i18n/de.ts +185 -0
  81. package/src/tool/forensic-blood-test-simulator/i18n/en.ts +185 -0
  82. package/src/tool/forensic-blood-test-simulator/i18n/es.ts +185 -0
  83. package/src/tool/forensic-blood-test-simulator/i18n/fr.ts +220 -0
  84. package/src/tool/forensic-blood-test-simulator/i18n/id.ts +220 -0
  85. package/src/tool/forensic-blood-test-simulator/i18n/it.ts +220 -0
  86. package/src/tool/forensic-blood-test-simulator/i18n/ja.ts +220 -0
  87. package/src/tool/forensic-blood-test-simulator/i18n/ko.ts +220 -0
  88. package/src/tool/forensic-blood-test-simulator/i18n/nl.ts +220 -0
  89. package/src/tool/forensic-blood-test-simulator/i18n/pl.ts +220 -0
  90. package/src/tool/forensic-blood-test-simulator/i18n/pt.ts +220 -0
  91. package/src/tool/forensic-blood-test-simulator/i18n/ru.ts +220 -0
  92. package/src/tool/forensic-blood-test-simulator/i18n/sv.ts +220 -0
  93. package/src/tool/forensic-blood-test-simulator/i18n/tr.ts +220 -0
  94. package/src/tool/forensic-blood-test-simulator/i18n/zh.ts +220 -0
  95. package/src/tool/forensic-blood-test-simulator/index.ts +11 -0
  96. package/src/tool/forensic-blood-test-simulator/logic.ts +174 -0
  97. package/src/tool/forensic-blood-test-simulator/script.ts +192 -0
  98. package/src/tool/forensic-blood-test-simulator/seo.astro +15 -0
  99. package/src/tool/forensic-fiber-comparison-microscope/bibliography.astro +6 -0
  100. package/src/tool/forensic-fiber-comparison-microscope/bibliography.ts +16 -0
  101. package/src/tool/forensic-fiber-comparison-microscope/component.astro +146 -0
  102. package/src/tool/forensic-fiber-comparison-microscope/entry.ts +32 -0
  103. package/src/tool/forensic-fiber-comparison-microscope/forensic-fiber-comparison-microscope.css +629 -0
  104. package/src/tool/forensic-fiber-comparison-microscope/i18n/de.ts +250 -0
  105. package/src/tool/forensic-fiber-comparison-microscope/i18n/en.ts +246 -0
  106. package/src/tool/forensic-fiber-comparison-microscope/i18n/es.ts +246 -0
  107. package/src/tool/forensic-fiber-comparison-microscope/i18n/fr.ts +246 -0
  108. package/src/tool/forensic-fiber-comparison-microscope/i18n/id.ts +250 -0
  109. package/src/tool/forensic-fiber-comparison-microscope/i18n/it.ts +246 -0
  110. package/src/tool/forensic-fiber-comparison-microscope/i18n/ja.ts +250 -0
  111. package/src/tool/forensic-fiber-comparison-microscope/i18n/ko.ts +246 -0
  112. package/src/tool/forensic-fiber-comparison-microscope/i18n/nl.ts +246 -0
  113. package/src/tool/forensic-fiber-comparison-microscope/i18n/pl.ts +250 -0
  114. package/src/tool/forensic-fiber-comparison-microscope/i18n/pt.ts +250 -0
  115. package/src/tool/forensic-fiber-comparison-microscope/i18n/ru.ts +250 -0
  116. package/src/tool/forensic-fiber-comparison-microscope/i18n/sv.ts +250 -0
  117. package/src/tool/forensic-fiber-comparison-microscope/i18n/tr.ts +246 -0
  118. package/src/tool/forensic-fiber-comparison-microscope/i18n/zh.ts +250 -0
  119. package/src/tool/forensic-fiber-comparison-microscope/index.ts +11 -0
  120. package/src/tool/forensic-fiber-comparison-microscope/logic.ts +244 -0
  121. package/src/tool/forensic-fiber-comparison-microscope/render.ts +265 -0
  122. package/src/tool/forensic-fiber-comparison-microscope/seo.astro +15 -0
  123. package/src/tool/forensic-fiber-comparison-microscope/view.ts +267 -0
  124. package/src/tool/forensic-glass-becke-line-simulator/bibliography.astro +6 -0
  125. package/src/tool/forensic-glass-becke-line-simulator/bibliography.ts +16 -0
  126. package/src/tool/forensic-glass-becke-line-simulator/component.astro +81 -0
  127. package/src/tool/forensic-glass-becke-line-simulator/entry.ts +32 -0
  128. package/src/tool/forensic-glass-becke-line-simulator/forensic-glass-becke-line-simulator.css +392 -0
  129. package/src/tool/forensic-glass-becke-line-simulator/i18n/de.ts +231 -0
  130. package/src/tool/forensic-glass-becke-line-simulator/i18n/en.ts +231 -0
  131. package/src/tool/forensic-glass-becke-line-simulator/i18n/es.ts +231 -0
  132. package/src/tool/forensic-glass-becke-line-simulator/i18n/fr.ts +231 -0
  133. package/src/tool/forensic-glass-becke-line-simulator/i18n/id.ts +231 -0
  134. package/src/tool/forensic-glass-becke-line-simulator/i18n/it.ts +231 -0
  135. package/src/tool/forensic-glass-becke-line-simulator/i18n/ja.ts +231 -0
  136. package/src/tool/forensic-glass-becke-line-simulator/i18n/ko.ts +231 -0
  137. package/src/tool/forensic-glass-becke-line-simulator/i18n/nl.ts +231 -0
  138. package/src/tool/forensic-glass-becke-line-simulator/i18n/pl.ts +231 -0
  139. package/src/tool/forensic-glass-becke-line-simulator/i18n/pt.ts +231 -0
  140. package/src/tool/forensic-glass-becke-line-simulator/i18n/ru.ts +231 -0
  141. package/src/tool/forensic-glass-becke-line-simulator/i18n/sv.ts +231 -0
  142. package/src/tool/forensic-glass-becke-line-simulator/i18n/tr.ts +231 -0
  143. package/src/tool/forensic-glass-becke-line-simulator/i18n/zh.ts +231 -0
  144. package/src/tool/forensic-glass-becke-line-simulator/index.ts +11 -0
  145. package/src/tool/forensic-glass-becke-line-simulator/logic.ts +100 -0
  146. package/src/tool/forensic-glass-becke-line-simulator/seo.astro +15 -0
  147. package/src/tool/forensic-glass-becke-line-simulator/view.ts +281 -0
  148. package/src/tool/forensic-image-authenticity-analyzer/bibliography.astro +9 -0
  149. package/src/tool/forensic-image-authenticity-analyzer/bibliography.ts +7 -0
  150. package/src/tool/forensic-image-authenticity-analyzer/component.astro +250 -0
  151. package/src/tool/forensic-image-authenticity-analyzer/entry.ts +29 -0
  152. package/src/tool/forensic-image-authenticity-analyzer/forensic-image-metadata-authenticity-analyzer.css +679 -0
  153. package/src/tool/forensic-image-authenticity-analyzer/i18n/de.ts +105 -0
  154. package/src/tool/forensic-image-authenticity-analyzer/i18n/en.ts +105 -0
  155. package/src/tool/forensic-image-authenticity-analyzer/i18n/es.ts +105 -0
  156. package/src/tool/forensic-image-authenticity-analyzer/i18n/fr.ts +105 -0
  157. package/src/tool/forensic-image-authenticity-analyzer/i18n/id.ts +76 -0
  158. package/src/tool/forensic-image-authenticity-analyzer/i18n/it.ts +105 -0
  159. package/src/tool/forensic-image-authenticity-analyzer/i18n/ja.ts +72 -0
  160. package/src/tool/forensic-image-authenticity-analyzer/i18n/ko.ts +72 -0
  161. package/src/tool/forensic-image-authenticity-analyzer/i18n/nl.ts +75 -0
  162. package/src/tool/forensic-image-authenticity-analyzer/i18n/pl.ts +75 -0
  163. package/src/tool/forensic-image-authenticity-analyzer/i18n/pt.ts +105 -0
  164. package/src/tool/forensic-image-authenticity-analyzer/i18n/ru.ts +75 -0
  165. package/src/tool/forensic-image-authenticity-analyzer/i18n/sv.ts +76 -0
  166. package/src/tool/forensic-image-authenticity-analyzer/i18n/tr.ts +76 -0
  167. package/src/tool/forensic-image-authenticity-analyzer/i18n/zh.ts +71 -0
  168. package/src/tool/forensic-image-authenticity-analyzer/index.ts +11 -0
  169. package/src/tool/forensic-image-authenticity-analyzer/logic.ts +283 -0
  170. package/src/tool/forensic-image-authenticity-analyzer/seo.astro +10 -0
  171. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.astro +6 -0
  172. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.ts +12 -0
  173. package/src/tool/forensic-microcrystal-drug-simulator/component.astro +240 -0
  174. package/src/tool/forensic-microcrystal-drug-simulator/entry.ts +32 -0
  175. package/src/tool/forensic-microcrystal-drug-simulator/forensic-microcrystal-drug-simulator.css +430 -0
  176. package/src/tool/forensic-microcrystal-drug-simulator/i18n/de.ts +244 -0
  177. package/src/tool/forensic-microcrystal-drug-simulator/i18n/en.ts +244 -0
  178. package/src/tool/forensic-microcrystal-drug-simulator/i18n/es.ts +244 -0
  179. package/src/tool/forensic-microcrystal-drug-simulator/i18n/fr.ts +244 -0
  180. package/src/tool/forensic-microcrystal-drug-simulator/i18n/id.ts +244 -0
  181. package/src/tool/forensic-microcrystal-drug-simulator/i18n/it.ts +244 -0
  182. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ja.ts +244 -0
  183. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ko.ts +244 -0
  184. package/src/tool/forensic-microcrystal-drug-simulator/i18n/nl.ts +244 -0
  185. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pl.ts +244 -0
  186. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pt.ts +244 -0
  187. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ru.ts +244 -0
  188. package/src/tool/forensic-microcrystal-drug-simulator/i18n/sv.ts +244 -0
  189. package/src/tool/forensic-microcrystal-drug-simulator/i18n/tr.ts +244 -0
  190. package/src/tool/forensic-microcrystal-drug-simulator/i18n/zh.ts +244 -0
  191. package/src/tool/forensic-microcrystal-drug-simulator/index.ts +11 -0
  192. package/src/tool/forensic-microcrystal-drug-simulator/logic.ts +189 -0
  193. package/src/tool/forensic-microcrystal-drug-simulator/seo.astro +15 -0
  194. package/src/tool/forensic-sex-determinator/bibliography.astro +14 -0
  195. package/src/tool/forensic-sex-determinator/bibliography.ts +12 -0
  196. package/src/tool/forensic-sex-determinator/component.astro +463 -0
  197. package/src/tool/forensic-sex-determinator/entry.ts +32 -0
  198. package/src/tool/forensic-sex-determinator/forensic-sex-determinator.css +413 -0
  199. package/src/tool/forensic-sex-determinator/i18n/de.ts +211 -0
  200. package/src/tool/forensic-sex-determinator/i18n/en.ts +211 -0
  201. package/src/tool/forensic-sex-determinator/i18n/es.ts +211 -0
  202. package/src/tool/forensic-sex-determinator/i18n/fr.ts +211 -0
  203. package/src/tool/forensic-sex-determinator/i18n/id.ts +211 -0
  204. package/src/tool/forensic-sex-determinator/i18n/it.ts +211 -0
  205. package/src/tool/forensic-sex-determinator/i18n/ja.ts +211 -0
  206. package/src/tool/forensic-sex-determinator/i18n/ko.ts +211 -0
  207. package/src/tool/forensic-sex-determinator/i18n/nl.ts +211 -0
  208. package/src/tool/forensic-sex-determinator/i18n/pl.ts +211 -0
  209. package/src/tool/forensic-sex-determinator/i18n/pt.ts +211 -0
  210. package/src/tool/forensic-sex-determinator/i18n/ru.ts +211 -0
  211. package/src/tool/forensic-sex-determinator/i18n/sv.ts +211 -0
  212. package/src/tool/forensic-sex-determinator/i18n/tr.ts +211 -0
  213. package/src/tool/forensic-sex-determinator/i18n/zh.ts +211 -0
  214. package/src/tool/forensic-sex-determinator/index.ts +11 -0
  215. package/src/tool/forensic-sex-determinator/logic.ts +89 -0
  216. package/src/tool/forensic-sex-determinator/seo.astro +15 -0
  217. package/src/tool/forensic-stature-estimator/bibliography.astro +14 -0
  218. package/src/tool/forensic-stature-estimator/bibliography.ts +12 -0
  219. package/src/tool/forensic-stature-estimator/component.astro +49 -0
  220. package/src/tool/forensic-stature-estimator/components/EstimationPanel.astro +65 -0
  221. package/src/tool/forensic-stature-estimator/components/OsteometricBoard.astro +39 -0
  222. package/src/tool/forensic-stature-estimator/components/OsteometricSelector.astro +109 -0
  223. package/src/tool/forensic-stature-estimator/dom-utils.ts +71 -0
  224. package/src/tool/forensic-stature-estimator/entry.ts +32 -0
  225. package/src/tool/forensic-stature-estimator/forensic-stature-estimator.css +689 -0
  226. package/src/tool/forensic-stature-estimator/helpers.ts +51 -0
  227. package/src/tool/forensic-stature-estimator/i18n/de.ts +196 -0
  228. package/src/tool/forensic-stature-estimator/i18n/en.ts +196 -0
  229. package/src/tool/forensic-stature-estimator/i18n/es.ts +196 -0
  230. package/src/tool/forensic-stature-estimator/i18n/fr.ts +196 -0
  231. package/src/tool/forensic-stature-estimator/i18n/id.ts +196 -0
  232. package/src/tool/forensic-stature-estimator/i18n/it.ts +196 -0
  233. package/src/tool/forensic-stature-estimator/i18n/ja.ts +196 -0
  234. package/src/tool/forensic-stature-estimator/i18n/ko.ts +196 -0
  235. package/src/tool/forensic-stature-estimator/i18n/nl.ts +196 -0
  236. package/src/tool/forensic-stature-estimator/i18n/pl.ts +196 -0
  237. package/src/tool/forensic-stature-estimator/i18n/pt.ts +196 -0
  238. package/src/tool/forensic-stature-estimator/i18n/ru.ts +196 -0
  239. package/src/tool/forensic-stature-estimator/i18n/sv.ts +196 -0
  240. package/src/tool/forensic-stature-estimator/i18n/tr.ts +196 -0
  241. package/src/tool/forensic-stature-estimator/i18n/zh.ts +196 -0
  242. package/src/tool/forensic-stature-estimator/index.ts +11 -0
  243. package/src/tool/forensic-stature-estimator/logic.ts +119 -0
  244. package/src/tool/forensic-stature-estimator/script.ts +288 -0
  245. package/src/tool/forensic-stature-estimator/seo.astro +15 -0
  246. package/src/tool/forensic-tlc-ink-simulator/bibliography.astro +6 -0
  247. package/src/tool/forensic-tlc-ink-simulator/bibliography.ts +16 -0
  248. package/src/tool/forensic-tlc-ink-simulator/component.astro +245 -0
  249. package/src/tool/forensic-tlc-ink-simulator/entry.ts +32 -0
  250. package/src/tool/forensic-tlc-ink-simulator/forensic-tlc-ink-simulator.css +462 -0
  251. package/src/tool/forensic-tlc-ink-simulator/i18n/de.ts +243 -0
  252. package/src/tool/forensic-tlc-ink-simulator/i18n/en.ts +243 -0
  253. package/src/tool/forensic-tlc-ink-simulator/i18n/es.ts +243 -0
  254. package/src/tool/forensic-tlc-ink-simulator/i18n/fr.ts +243 -0
  255. package/src/tool/forensic-tlc-ink-simulator/i18n/id.ts +243 -0
  256. package/src/tool/forensic-tlc-ink-simulator/i18n/it.ts +243 -0
  257. package/src/tool/forensic-tlc-ink-simulator/i18n/ja.ts +235 -0
  258. package/src/tool/forensic-tlc-ink-simulator/i18n/ko.ts +235 -0
  259. package/src/tool/forensic-tlc-ink-simulator/i18n/nl.ts +243 -0
  260. package/src/tool/forensic-tlc-ink-simulator/i18n/pl.ts +243 -0
  261. package/src/tool/forensic-tlc-ink-simulator/i18n/pt.ts +243 -0
  262. package/src/tool/forensic-tlc-ink-simulator/i18n/ru.ts +243 -0
  263. package/src/tool/forensic-tlc-ink-simulator/i18n/sv.ts +243 -0
  264. package/src/tool/forensic-tlc-ink-simulator/i18n/tr.ts +243 -0
  265. package/src/tool/forensic-tlc-ink-simulator/i18n/zh.ts +235 -0
  266. package/src/tool/forensic-tlc-ink-simulator/index.ts +11 -0
  267. package/src/tool/forensic-tlc-ink-simulator/logic.ts +152 -0
  268. package/src/tool/forensic-tlc-ink-simulator/seo.astro +15 -0
  269. package/src/tool/gsr-dispersion-calculator/bibliography.astro +6 -0
  270. package/src/tool/gsr-dispersion-calculator/bibliography.ts +16 -0
  271. package/src/tool/gsr-dispersion-calculator/component.astro +294 -0
  272. package/src/tool/gsr-dispersion-calculator/entry.ts +32 -0
  273. package/src/tool/gsr-dispersion-calculator/gsr-dispersion-calculator.css +305 -0
  274. package/src/tool/gsr-dispersion-calculator/i18n/de.ts +272 -0
  275. package/src/tool/gsr-dispersion-calculator/i18n/en.ts +272 -0
  276. package/src/tool/gsr-dispersion-calculator/i18n/es.ts +272 -0
  277. package/src/tool/gsr-dispersion-calculator/i18n/fr.ts +272 -0
  278. package/src/tool/gsr-dispersion-calculator/i18n/id.ts +272 -0
  279. package/src/tool/gsr-dispersion-calculator/i18n/it.ts +272 -0
  280. package/src/tool/gsr-dispersion-calculator/i18n/ja.ts +272 -0
  281. package/src/tool/gsr-dispersion-calculator/i18n/ko.ts +272 -0
  282. package/src/tool/gsr-dispersion-calculator/i18n/nl.ts +272 -0
  283. package/src/tool/gsr-dispersion-calculator/i18n/pl.ts +272 -0
  284. package/src/tool/gsr-dispersion-calculator/i18n/pt.ts +272 -0
  285. package/src/tool/gsr-dispersion-calculator/i18n/ru.ts +272 -0
  286. package/src/tool/gsr-dispersion-calculator/i18n/sv.ts +272 -0
  287. package/src/tool/gsr-dispersion-calculator/i18n/tr.ts +272 -0
  288. package/src/tool/gsr-dispersion-calculator/i18n/zh.ts +272 -0
  289. package/src/tool/gsr-dispersion-calculator/index.ts +11 -0
  290. package/src/tool/gsr-dispersion-calculator/logic.ts +148 -0
  291. package/src/tool/gsr-dispersion-calculator/seo.astro +15 -0
  292. package/src/tool/widmark-alcohol-simulator/bibliography.astro +14 -0
  293. package/src/tool/widmark-alcohol-simulator/bibliography.ts +12 -0
  294. package/src/tool/widmark-alcohol-simulator/component.astro +453 -0
  295. package/src/tool/widmark-alcohol-simulator/entry.ts +32 -0
  296. package/src/tool/widmark-alcohol-simulator/i18n/de.ts +193 -0
  297. package/src/tool/widmark-alcohol-simulator/i18n/en.ts +206 -0
  298. package/src/tool/widmark-alcohol-simulator/i18n/es.ts +193 -0
  299. package/src/tool/widmark-alcohol-simulator/i18n/fr.ts +193 -0
  300. package/src/tool/widmark-alcohol-simulator/i18n/id.ts +193 -0
  301. package/src/tool/widmark-alcohol-simulator/i18n/it.ts +193 -0
  302. package/src/tool/widmark-alcohol-simulator/i18n/ja.ts +193 -0
  303. package/src/tool/widmark-alcohol-simulator/i18n/ko.ts +193 -0
  304. package/src/tool/widmark-alcohol-simulator/i18n/nl.ts +193 -0
  305. package/src/tool/widmark-alcohol-simulator/i18n/pl.ts +193 -0
  306. package/src/tool/widmark-alcohol-simulator/i18n/pt.ts +193 -0
  307. package/src/tool/widmark-alcohol-simulator/i18n/ru.ts +193 -0
  308. package/src/tool/widmark-alcohol-simulator/i18n/sv.ts +193 -0
  309. package/src/tool/widmark-alcohol-simulator/i18n/tr.ts +193 -0
  310. package/src/tool/widmark-alcohol-simulator/i18n/zh.ts +193 -0
  311. package/src/tool/widmark-alcohol-simulator/index.ts +11 -0
  312. package/src/tool/widmark-alcohol-simulator/logic.ts +97 -0
  313. package/src/tool/widmark-alcohol-simulator/seo.astro +15 -0
  314. package/src/tool/widmark-alcohol-simulator/widmark-alcohol-simulator.css +386 -0
  315. package/src/tools.ts +27 -0
  316. package/src/types.ts +70 -0
@@ -0,0 +1,119 @@
1
+ export interface StatureInput {
2
+ bone: 'femur' | 'tibia' | 'humerus' | 'fibula' | 'radius' | 'ulna';
3
+ length: number;
4
+ sex: 'male' | 'female';
5
+ ancestry: 'white' | 'black' | 'asian';
6
+ age: number;
7
+ }
8
+
9
+ export interface StatureResult {
10
+ stature: number;
11
+ see: number;
12
+ minStature: number;
13
+ maxStature: number;
14
+ formulaText: string;
15
+ coef: {
16
+ multiplier: number;
17
+ constant: number;
18
+ ageLoss: number;
19
+ see: number;
20
+ };
21
+ fallbackWarning: boolean;
22
+ }
23
+
24
+ interface RegressionCoef {
25
+ multiplier: number;
26
+ constant: number;
27
+ see: number;
28
+ }
29
+
30
+ const equations: Record<string, Record<string, RegressionCoef>> = {
31
+ male: {
32
+ white_femur: { multiplier: 2.32, constant: 65.53, see: 3.94 },
33
+ white_tibia: { multiplier: 2.42, constant: 81.93, see: 4.00 },
34
+ white_humerus: { multiplier: 2.89, constant: 78.10, see: 4.57 },
35
+ white_fibula: { multiplier: 2.60, constant: 75.50, see: 3.86 },
36
+ white_radius: { multiplier: 3.79, constant: 79.42, see: 4.66 },
37
+ white_ulna: { multiplier: 3.76, constant: 75.55, see: 4.72 },
38
+ black_femur: { multiplier: 2.10, constant: 72.22, see: 3.91 },
39
+ black_tibia: { multiplier: 2.19, constant: 85.36, see: 3.96 },
40
+ black_humerus: { multiplier: 2.88, constant: 75.48, see: 4.23 },
41
+ black_fibula: { multiplier: 2.34, constant: 80.07, see: 4.02 },
42
+ black_radius: { multiplier: 3.32, constant: 85.43, see: 4.57 },
43
+ black_ulna: { multiplier: 3.20, constant: 82.77, see: 4.74 },
44
+ asian_femur: { multiplier: 2.15, constant: 72.57, see: 3.80 },
45
+ asian_tibia: { multiplier: 2.39, constant: 81.45, see: 3.27 },
46
+ asian_humerus: { multiplier: 2.68, constant: 83.19, see: 4.25 },
47
+ asian_fibula: { multiplier: 2.40, constant: 80.56, see: 3.24 },
48
+ asian_radius: { multiplier: 3.54, constant: 82.00, see: 4.60 },
49
+ asian_ulna: { multiplier: 3.48, constant: 77.45, see: 4.66 }
50
+ },
51
+ female: {
52
+ white_femur: { multiplier: 2.47, constant: 54.10, see: 3.72 },
53
+ white_tibia: { multiplier: 2.90, constant: 61.53, see: 3.66 },
54
+ white_humerus: { multiplier: 3.36, constant: 57.97, see: 4.45 },
55
+ white_fibula: { multiplier: 2.93, constant: 59.61, see: 3.57 },
56
+ white_radius: { multiplier: 4.74, constant: 54.93, see: 4.45 },
57
+ white_ulna: { multiplier: 4.27, constant: 57.76, see: 4.30 },
58
+ black_femur: { multiplier: 2.28, constant: 59.76, see: 3.41 },
59
+ black_tibia: { multiplier: 2.45, constant: 72.65, see: 3.70 },
60
+ black_humerus: { multiplier: 3.08, constant: 64.67, see: 4.25 },
61
+ black_fibula: { multiplier: 2.49, constant: 70.90, see: 3.80 },
62
+ black_radius: { multiplier: 3.67, constant: 71.79, see: 4.59 },
63
+ black_ulna: { multiplier: 3.31, constant: 75.38, see: 4.83 }
64
+ }
65
+ };
66
+
67
+ function getCoef(sex: string, ancestry: string, bone: string) {
68
+ const key = `${ancestry}_${bone}`;
69
+ const coefMap = equations[sex];
70
+ return coefMap ? coefMap[key] : null;
71
+ }
72
+
73
+ function computeStature(multiplier: number, constant: number, length: number, age: number) {
74
+ const raw = multiplier * length + constant;
75
+ const ageLoss = age > 30 ? 0.06 * (age - 30) : 0;
76
+ return {
77
+ val: raw - ageLoss,
78
+ ageLoss
79
+ };
80
+ }
81
+
82
+ function defaultResult(): StatureResult {
83
+ return {
84
+ stature: 0,
85
+ see: 0,
86
+ minStature: 0,
87
+ maxStature: 0,
88
+ formulaText: '',
89
+ coef: { multiplier: 0, constant: 0, ageLoss: 0, see: 0 },
90
+ fallbackWarning: false
91
+ };
92
+ }
93
+
94
+ export function estimateStature(input: StatureInput): StatureResult {
95
+ const isFallback = input.sex === 'female' && input.ancestry === 'asian';
96
+ const ancestry = isFallback ? 'white' : input.ancestry;
97
+ const coef = getCoef(input.sex, ancestry, input.bone);
98
+
99
+ if (!coef) return defaultResult();
100
+
101
+ const { val, ageLoss } = computeStature(coef.multiplier, coef.constant, input.length, input.age);
102
+ const ageText = input.age > 30 ? ` - 0.06 * (${input.age} - 30)` : '';
103
+ const formulaText = `Stature = ${coef.multiplier} * ${input.length} + ${coef.constant}${ageText} ± ${coef.see}`;
104
+
105
+ return {
106
+ stature: Math.round(val * 100) / 100,
107
+ see: Math.round(coef.see * 100) / 100,
108
+ minStature: Math.round((val - coef.see) * 100) / 100,
109
+ maxStature: Math.round((val + coef.see) * 100) / 100,
110
+ formulaText,
111
+ coef: {
112
+ multiplier: coef.multiplier,
113
+ constant: coef.constant,
114
+ ageLoss: Math.round(ageLoss * 100) / 100,
115
+ see: coef.see
116
+ },
117
+ fallbackWarning: isFallback
118
+ };
119
+ }
@@ -0,0 +1,288 @@
1
+ import { estimateStature } from './logic';
2
+ import type { StatureInput, StatureResult } from './logic';
3
+ import { cmToFtIn, convertValueForUnit, applyLengthLimits, getLimitsForUnit } from './helpers';
4
+ import { setupCustomDropdown, closeAllDropdowns, restoreValue, restoreSelectable } from './dom-utils';
5
+
6
+ const uiDataEl = document.getElementById('stature-ui-data');
7
+ const ui = uiDataEl ? JSON.parse(uiDataEl.textContent || '{}') : {};
8
+
9
+ const boneInput = document.getElementById('input-bone') as HTMLInputElement | null,
10
+ ancestryInput = document.getElementById('input-ancestry') as HTMLInputElement | null,
11
+ lengthInputVal = document.getElementById('input-length-val') as HTMLInputElement | null,
12
+ lengthInputSlider = document.getElementById('input-length') as HTMLInputElement | null,
13
+ ageInput = document.getElementById('input-age') as HTMLInputElement | null,
14
+ caliper = document.getElementById('caliper-slider'),
15
+ fallbackBox = document.getElementById('fallback-alert'),
16
+ lengthLabelText = document.getElementById('length-label-text'),
17
+ boardTitle = document.querySelector('.osteometric-board-title'),
18
+ rulerTicks = document.querySelector('.ruler-ticks');
19
+
20
+ const partBone = document.getElementById('part-bone'),
21
+ partConst = document.getElementById('part-const'),
22
+ operatorAgeSign = document.getElementById('operator-age-sign'),
23
+ containerPartAge = document.getElementById('container-part-age'),
24
+ partAge = document.getElementById('part-age'),
25
+ partError = document.getElementById('part-error');
26
+
27
+ const statureCmValue = document.getElementById('stature-cm-value'),
28
+ statureInchValue = document.getElementById('stature-inch-value'),
29
+ seeValue = document.getElementById('see-value'),
30
+ rangeValue = document.getElementById('range-value');
31
+
32
+ const STORAGE_KEY = 'forensic-stature-estimator-input';
33
+ let currentUnit: 'metric' | 'imperial' = 'metric';
34
+
35
+ document.addEventListener('click', closeAllDropdowns);
36
+
37
+ const getSex = () => (document.querySelector('input[name="sex"]:checked') as HTMLInputElement | null)?.value as 'male' | 'female' || 'male';
38
+ const getSelectedUnit = () => (document.querySelector('input[name="system-unit"]:checked') as HTMLInputElement | null)?.value as 'metric' | 'imperial' || 'metric';
39
+
40
+ function getBoneConfig(): { min: number; max: number; default: number } {
41
+ if (!boneInput) return { min: 20, max: 60, default: 40 };
42
+ const option = document.querySelector(`#dropdown-bone .custom-select-option[data-value="${boneInput.value}"]`);
43
+ if (!option) return { min: 20, max: 60, default: 40 };
44
+ return {
45
+ min: parseFloat(option.getAttribute('data-min') || '20'),
46
+ max: parseFloat(option.getAttribute('data-max') || '60'),
47
+ default: parseFloat(option.getAttribute('data-default') || '40')
48
+ };
49
+ }
50
+
51
+ function updateBoneVisuals(selectedBone: string, lengthCm: number) {
52
+ const shapes = document.querySelectorAll('.bone-shape');
53
+ shapes.forEach(shape => shape.classList.add('hidden'));
54
+
55
+ const activeShape = document.getElementById(`bone-shape-${selectedBone}`);
56
+ if (activeShape) activeShape.classList.remove('hidden');
57
+
58
+ const boneStage = document.querySelector('.bone-stage') as HTMLElement | null;
59
+ if (caliper && boneStage) {
60
+ const ratio = Math.max(0, Math.min(1, lengthCm / 60));
61
+ const positionPct = ratio * 80;
62
+ caliper.style.left = `calc(${10 + positionPct}%)`;
63
+ boneStage.style.width = `calc(${positionPct}% - 12px)`;
64
+ }
65
+ }
66
+
67
+ function updateRulerLabels(unit: 'metric' | 'imperial') {
68
+ if (lengthLabelText) {
69
+ lengthLabelText.textContent = `${ui.lengthLabel} (${unit === 'imperial' ? ui.unitsInches : ui.unitsCm})`;
70
+ }
71
+ if (boardTitle) {
72
+ boardTitle.textContent = unit === 'imperial' ? ui.rulerLabelImperial : ui.rulerLabelMetric;
73
+ }
74
+ if (rulerTicks) {
75
+ rulerTicks.classList.toggle('unit-imperial', unit === 'imperial');
76
+ }
77
+ }
78
+
79
+ function updateLengthMinMax(convertValue: boolean = false) {
80
+ if (!lengthInputVal || !lengthInputSlider) return;
81
+ const config = getBoneConfig();
82
+ const unit = getSelectedUnit();
83
+ const limits = getLimitsForUnit(unit, config);
84
+
85
+ lengthInputSlider.min = limits.formattedMin;
86
+ lengthInputSlider.max = limits.formattedMax;
87
+ lengthInputSlider.step = limits.step.toString();
88
+ lengthInputVal.step = limits.step.toString();
89
+
90
+ updateRulerLabels(unit);
91
+
92
+ let val = parseFloat(lengthInputVal.value);
93
+ if (convertValue) {
94
+ val = convertValueForUnit(val, unit);
95
+ }
96
+
97
+ val = applyLengthLimits(val, config, unit);
98
+ lengthInputVal.value = val.toFixed(unit === 'imperial' ? 2 : 1);
99
+ lengthInputSlider.value = val.toString();
100
+ }
101
+
102
+ function updateMetricResults(result: StatureResult) {
103
+ if (statureCmValue) {
104
+ statureCmValue.textContent = result.stature.toFixed(2);
105
+ const cmLabel = document.querySelector('.stature-unit');
106
+ if (cmLabel) cmLabel.textContent = ui.unitsCm;
107
+ }
108
+ if (statureInchValue) statureInchValue.textContent = cmToFtIn(result.stature);
109
+ if (seeValue) seeValue.textContent = `± ${result.see.toFixed(2)} ${ui.unitsCm}`;
110
+ if (rangeValue) {
111
+ rangeValue.textContent = `${result.minStature.toFixed(2)} - ${result.maxStature.toFixed(2)} ${ui.unitsCm}`;
112
+ }
113
+ }
114
+
115
+ function updateImperialResults(result: StatureResult) {
116
+ const seeInches = result.see / 2.54;
117
+ const minInches = result.minStature / 2.54;
118
+ const maxInches = result.maxStature / 2.54;
119
+
120
+ if (statureCmValue) {
121
+ statureCmValue.textContent = cmToFtIn(result.stature);
122
+ const cmLabel = document.querySelector('.stature-unit');
123
+ if (cmLabel) cmLabel.textContent = '';
124
+ }
125
+ if (statureInchValue) statureInchValue.textContent = `${result.stature.toFixed(2)} ${ui.unitsCm}`;
126
+ if (seeValue) seeValue.textContent = `± ${seeInches.toFixed(2)} ${ui.unitsInches}`;
127
+ if (rangeValue) {
128
+ rangeValue.textContent = `${minInches.toFixed(2)} - ${maxInches.toFixed(2)} ${ui.unitsInches}`;
129
+ }
130
+ }
131
+
132
+ function updateAgeVisuals(ageLoss: number, age: number) {
133
+ if (age > 30) {
134
+ if (operatorAgeSign) operatorAgeSign.style.display = 'inline-flex';
135
+ if (containerPartAge) containerPartAge.style.display = 'inline-flex';
136
+ if (partAge) {
137
+ if (getSelectedUnit() === 'imperial') {
138
+ partAge.textContent = `${ageLoss.toFixed(2)} ${ui.unitsCm} = ${(ageLoss / 2.54).toFixed(2)} ${ui.unitsInches}`;
139
+ } else {
140
+ partAge.textContent = `0.06 × (${age} - 30) = ${ageLoss.toFixed(2)} ${ui.unitsCm}`;
141
+ }
142
+ }
143
+ } else {
144
+ if (operatorAgeSign) operatorAgeSign.style.display = 'none';
145
+ if (containerPartAge) containerPartAge.style.display = 'none';
146
+ }
147
+ }
148
+
149
+ function updateFormulaMetric(result: StatureResult, lengthCm: number, age: number) {
150
+ if (partBone) partBone.textContent = `${result.coef.multiplier} × ${lengthCm.toFixed(1)} ${ui.unitsCm}`;
151
+ if (partConst) partConst.textContent = `${result.coef.constant} ${ui.unitsCm}`;
152
+ if (partError) partError.textContent = `${result.coef.see.toFixed(2)} ${ui.unitsCm}`;
153
+ updateAgeVisuals(result.coef.ageLoss, age);
154
+ }
155
+
156
+ function updateFormulaImperial(result: StatureResult, lengthCm: number, age: number) {
157
+ if (partBone) {
158
+ const valInches = (result.coef.multiplier * lengthCm) / 2.54;
159
+ partBone.textContent = `${result.coef.multiplier} × ${lengthCm.toFixed(1)} ${ui.unitsCm} = ${valInches.toFixed(2)} ${ui.unitsInches}`;
160
+ }
161
+ if (partConst) {
162
+ partConst.textContent = `${result.coef.constant} ${ui.unitsCm} = ${(result.coef.constant / 2.54).toFixed(2)} ${ui.unitsInches}`;
163
+ }
164
+ if (partError) {
165
+ partError.textContent = `${(result.coef.see / 2.54).toFixed(2)} ${ui.unitsInches}`;
166
+ }
167
+ updateAgeVisuals(result.coef.ageLoss, age);
168
+ }
169
+
170
+ function displayResults(result: StatureResult, lengthCm: number, age: number, unit: 'metric' | 'imperial') {
171
+ if (unit === 'metric') {
172
+ updateMetricResults(result);
173
+ updateFormulaMetric(result, lengthCm, age);
174
+ } else {
175
+ updateImperialResults(result);
176
+ updateFormulaImperial(result, lengthCm, age);
177
+ }
178
+ if (fallbackBox) fallbackBox.classList.toggle('hidden', !result.fallbackWarning);
179
+ }
180
+
181
+ function getInputs() {
182
+ if (!boneInput || !lengthInputVal || !ancestryInput || !ageInput) return null;
183
+ return {
184
+ bone: boneInput.value as StatureInput['bone'],
185
+ length: parseFloat(lengthInputVal.value) || 0,
186
+ sex: getSex(),
187
+ ancestry: ancestryInput.value as StatureInput['ancestry'],
188
+ age: parseInt(ageInput.value, 10) || 25,
189
+ unit: getSelectedUnit()
190
+ };
191
+ }
192
+
193
+ function update() {
194
+ const inputs = getInputs();
195
+ if (!inputs) return;
196
+
197
+ const lengthCm = inputs.unit === 'imperial' ? inputs.length * 2.54 : inputs.length;
198
+ try {
199
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(inputs));
200
+ } catch {}
201
+ updateBoneVisuals(inputs.bone, lengthCm);
202
+
203
+ const result = estimateStature({
204
+ bone: inputs.bone,
205
+ length: lengthCm,
206
+ sex: inputs.sex,
207
+ ancestry: inputs.ancestry,
208
+ age: inputs.age
209
+ });
210
+ displayResults(result, lengthCm, inputs.age, inputs.unit);
211
+ }
212
+
213
+ function handleBoneChange() {
214
+ updateLengthMinMax(false);
215
+ update();
216
+ }
217
+
218
+ function handleUnitChange() {
219
+ const prevUnit = currentUnit;
220
+ currentUnit = getSelectedUnit();
221
+ if (prevUnit !== currentUnit) {
222
+ updateLengthMinMax(true);
223
+ update();
224
+ }
225
+ }
226
+
227
+ function handleSliderInput() {
228
+ if (lengthInputVal && lengthInputSlider) {
229
+ lengthInputVal.value = lengthInputSlider.value;
230
+ update();
231
+ }
232
+ }
233
+
234
+ function handleNumberInput() {
235
+ if (lengthInputVal && lengthInputSlider) {
236
+ const val = parseFloat(lengthInputVal.value);
237
+ const config = getBoneConfig();
238
+ const unit = getSelectedUnit();
239
+ const min = unit === 'imperial' ? config.min / 2.54 : config.min;
240
+ const max = unit === 'imperial' ? config.max / 2.54 : config.max;
241
+ if (!isNaN(val) && val >= min && val <= max) {
242
+ lengthInputSlider.value = val.toString();
243
+ update();
244
+ }
245
+ }
246
+ }
247
+
248
+ setupCustomDropdown('dropdown-bone', handleBoneChange);
249
+ setupCustomDropdown('dropdown-ancestry', update);
250
+
251
+ if (lengthInputSlider) lengthInputSlider.addEventListener('input', handleSliderInput);
252
+ if (lengthInputVal) lengthInputVal.addEventListener('change', handleNumberInput);
253
+ if (ageInput) ageInput.addEventListener('input', update);
254
+
255
+ document.querySelectorAll('input[name="sex"]').forEach(el => el.addEventListener('change', update));
256
+ document.querySelectorAll('input[name="system-unit"]').forEach(el => el.addEventListener('change', handleUnitChange));
257
+
258
+ function restoreSavedState() {
259
+ const saved = localStorage.getItem(STORAGE_KEY);
260
+ if (!saved) {
261
+ updateLengthMinMax(false);
262
+ return;
263
+ }
264
+ const parsed = JSON.parse(saved);
265
+ if (parsed.unit) {
266
+ const el = document.querySelector(`input[name="system-unit"][value="${parsed.unit}"]`) as HTMLInputElement | null;
267
+ if (el) el.checked = true;
268
+ currentUnit = parsed.unit;
269
+ }
270
+ restoreSelectable(boneInput, parsed.bone, 'dropdown-bone');
271
+ updateLengthMinMax(false);
272
+ if (parsed.sex) {
273
+ const el = document.querySelector(`input[name="sex"][value="${parsed.sex}"]`) as HTMLInputElement | null;
274
+ if (el) el.checked = true;
275
+ }
276
+ restoreSelectable(ancestryInput, parsed.ancestry, 'dropdown-ancestry');
277
+ restoreValue(ageInput, parsed.age);
278
+ restoreValue(lengthInputVal, parsed.length);
279
+ restoreValue(lengthInputSlider, parsed.length);
280
+ }
281
+
282
+ try {
283
+ restoreSavedState();
284
+ } catch {
285
+ updateLengthMinMax(false);
286
+ }
287
+
288
+ update();
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { forensicStatureEstimator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await forensicStatureEstimator.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography as BibliographyRenderer } from '@jjlmoya/utils-shared';
3
+ import { bibliography } from './bibliography';
4
+ ---
5
+
6
+ <BibliographyRenderer links={bibliography} />
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'ASTM E1422 - Standard Guide for Test Methods for Forensic Writing Ink Comparison.',
6
+ url: 'https://store.astm.org/e1422-98.html',
7
+ },
8
+ {
9
+ name: 'Scientific Working Group for Forensic Document Examination. Standard for Test Methods for Forensic Writing Ink Comparison.',
10
+ url: 'https://www.swgdoc.org/documents/SWGDOC%20Standard%20for%20Test%20Methods%20for%20Forensic%20Writing%20Ink%20Comparison.pdf',
11
+ },
12
+ {
13
+ name: 'Chemical Erasure in Document Analysis',
14
+ url: 'https://www.scribd.com/document/939245308/CHAPTER-5-CHEMICAL-ASPECTS-OF-DOCUMENT-EXAMINATION-INK-NAD-PAINTS',
15
+ },
16
+ ];
@@ -0,0 +1,245 @@
1
+ ---
2
+ import './forensic-tlc-ink-simulator.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ const rulerMarks = (ui.rulerMarks ?? '').split(',');
10
+ ---
11
+
12
+ <section class="tlc-shell" data-tlc-simulator>
13
+ <div class="tlc-lab">
14
+ <div class="tlc-stage">
15
+ <div class="tlc-chamber" aria-label={ui.chamberAria}>
16
+ <div class="tlc-glass-shine"></div>
17
+ <div class="tlc-solvent" id="tlc-solvent"></div>
18
+ <div class="tlc-plate">
19
+ <div class="tlc-plate-clip"></div>
20
+ <div class="tlc-solvent-front" id="tlc-front"><span>{ui.solventFront}</span></div>
21
+ <div class="tlc-origin"><span>{ui.originLine}</span></div>
22
+ <div class="tlc-spot"></div>
23
+ <div class="tlc-lane" id="tlc-lane"></div>
24
+ </div>
25
+ <div class="tlc-ruler" aria-label={ui.rulerAria}>
26
+ {rulerMarks.map((mark) => <span>{mark}</span>)}
27
+ </div>
28
+ </div>
29
+
30
+ <div class="tlc-table-wrap">
31
+ <table class="tlc-table">
32
+ <thead>
33
+ <tr>
34
+ <th>{ui.pigment}</th>
35
+ <th>{ui.distance}</th>
36
+ <th>{ui.rfValue}</th>
37
+ </tr>
38
+ </thead>
39
+ <tbody id="tlc-table-body"></tbody>
40
+ </table>
41
+ </div>
42
+ </div>
43
+
44
+ <aside class="tlc-panel">
45
+ <div class="tlc-unit-toggle" role="group" aria-label={ui.units}>
46
+ <button type="button" data-tlc-unit="metric" data-active="true">{ui.metric}</button>
47
+ <button type="button" data-tlc-unit="imperial" data-active="false">{ui.imperial}</button>
48
+ </div>
49
+
50
+ <label class="tlc-field">
51
+ <span>{ui.inkSample}</span>
52
+ <select id="tlc-ink">
53
+ <option value="ransomNote">{ui.ransomNote}</option>
54
+ <option value="bluePen">{ui.bluePen}</option>
55
+ <option value="blackPen">{ui.blackPen}</option>
56
+ <option value="gelPen">{ui.gelPen}</option>
57
+ </select>
58
+ </label>
59
+
60
+ <label class="tlc-field">
61
+ <span>{ui.solventSystem}</span>
62
+ <select id="tlc-solvent-select">
63
+ <option value="ethanolWater">{ui.ethanolWater}</option>
64
+ <option value="butanolAcetic">{ui.butanolAcetic}</option>
65
+ <option value="isopropanolAmmonia">{ui.isopropanolAmmonia}</option>
66
+ </select>
67
+ </label>
68
+
69
+ <label class="tlc-slider">
70
+ <span>{ui.developmentTime}</span>
71
+ <strong id="tlc-time-label">-</strong>
72
+ <input id="tlc-time" type="range" min="1" max="12" value="6" step="0.5" />
73
+ </label>
74
+
75
+ <div class="tlc-readout">
76
+ <div>
77
+ <span>{ui.frontDistance}</span>
78
+ <strong id="tlc-front-distance">-</strong>
79
+ </div>
80
+ <div>
81
+ <span>{ui.matchScore}</span>
82
+ <strong id="tlc-match">-</strong>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="tlc-mini-map" aria-hidden="true">
87
+ <span></span><span></span><span></span><span></span>
88
+ </div>
89
+ </aside>
90
+
91
+ <p class="tlc-disclaimer">{ui.disclaimer}</p>
92
+ </div>
93
+ </section>
94
+
95
+ <script id="tlc-ui" type="application/json" set:html={JSON.stringify(ui)}></script>
96
+ <script>
97
+ import { ChromatographyPhysics } from './logic';
98
+ import type { InkSample, SolventSystem, TlcBand } from './logic';
99
+
100
+ const ui = JSON.parse(document.getElementById('tlc-ui')?.textContent || '{}');
101
+ const model = new ChromatographyPhysics();
102
+ const STORAGE_KEY = 'forensic-tlc-ink-simulator-state';
103
+ const inkSelect = document.getElementById('tlc-ink') as HTMLSelectElement | null;
104
+ const solventSelect = document.getElementById('tlc-solvent-select') as HTMLSelectElement | null;
105
+ const timeInput = document.getElementById('tlc-time') as HTMLInputElement | null;
106
+ const lane = document.getElementById('tlc-lane');
107
+ const front = document.getElementById('tlc-front') as HTMLElement | null;
108
+ const solvent = document.getElementById('tlc-solvent') as HTMLElement | null;
109
+ const tableBody = document.getElementById('tlc-table-body');
110
+ let unitSystem: 'metric' | 'imperial' = 'metric';
111
+
112
+ function setText(id: string, value: string): void {
113
+ const element = document.getElementById(id);
114
+ if (element) element.textContent = value;
115
+ }
116
+
117
+ function formatDistance(mm: number): string {
118
+ if (unitSystem === 'metric') return `${mm.toFixed(1)} ${ui.unitMillimeter}`;
119
+ return `${(mm / 25.4).toFixed(2)} ${ui.unitInch}`;
120
+ }
121
+
122
+ function updateUnitButtons(): void {
123
+ document.querySelectorAll<HTMLButtonElement>('[data-tlc-unit]').forEach((button) => {
124
+ button.dataset.active = button.dataset.tlcUnit === unitSystem ? 'true' : 'false';
125
+ });
126
+ }
127
+
128
+ function renderBand(band: TlcBand, solventTravelMm: number): HTMLDivElement {
129
+ const mark = document.createElement('div');
130
+ const label = ui[band.labelKey] ?? band.id;
131
+ mark.className = 'tlc-band';
132
+ mark.style.setProperty('--band-color', band.color);
133
+ mark.style.setProperty('--band-bottom', `${10 + (band.distanceMm / solventTravelMm) * 78}%`);
134
+ mark.style.setProperty('--band-height', `${Math.max(5, band.widthMm * 1.8)}px`);
135
+ mark.title = `${label}: ${ui.rfValue} ${band.rf.toFixed(2)}`;
136
+ return mark;
137
+ }
138
+
139
+ function renderTable(bands: TlcBand[]): void {
140
+ if (!tableBody) return;
141
+ tableBody.innerHTML = '';
142
+ bands.forEach((band) => {
143
+ const row = document.createElement('tr');
144
+ const label = ui[band.labelKey] ?? band.id;
145
+ row.innerHTML = `<td><span class="tlc-chip" style="--chip:${band.color}"></span>${label}</td><td>${formatDistance(band.distanceMm)}</td><td>${band.rf.toFixed(2)}</td>`;
146
+ tableBody.append(row);
147
+ });
148
+ }
149
+
150
+ function saveState(): void {
151
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
152
+ ink: inkSelect?.value ?? 'ransomNote',
153
+ solvent: solventSelect?.value ?? 'ethanolWater',
154
+ time: Number(timeInput?.value ?? 6),
155
+ unitSystem,
156
+ }));
157
+ }
158
+
159
+ function applySavedSelectValue(select: HTMLSelectElement | null, value: unknown): void {
160
+ if (typeof value === 'string' && select) {
161
+ select.value = value;
162
+ }
163
+ }
164
+
165
+ function applySavedTime(value: unknown): void {
166
+ if (typeof value === 'number' && timeInput) {
167
+ timeInput.value = String(value);
168
+ }
169
+ }
170
+
171
+ function applySavedUnitSystem(value: unknown): void {
172
+ if (value === 'imperial' || value === 'metric') {
173
+ unitSystem = value;
174
+ }
175
+ }
176
+
177
+ function parseSavedState(): Record<string, unknown> {
178
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record<string, unknown>;
179
+ }
180
+
181
+ function restoreState(): void {
182
+ try {
183
+ const saved = parseSavedState();
184
+ applySavedSelectValue(inkSelect, saved.ink);
185
+ applySavedSelectValue(solventSelect, saved.solvent);
186
+ applySavedTime(saved.time);
187
+ applySavedUnitSystem(saved.unitSystem);
188
+ } catch {
189
+ localStorage.removeItem(STORAGE_KEY);
190
+ }
191
+ }
192
+
193
+ function getSimulationInput(): { timeMinutes: number; ink: InkSample; solventSystem: SolventSystem } {
194
+ return {
195
+ timeMinutes: Number(timeInput?.value ?? 6),
196
+ ink: (inkSelect?.value ?? 'ransomNote') as InkSample,
197
+ solventSystem: (solventSelect?.value ?? 'ethanolWater') as SolventSystem,
198
+ };
199
+ }
200
+
201
+ function renderLane(bands: TlcBand[], solventTravelMm: number): void {
202
+ if (!lane) return;
203
+ lane.innerHTML = '';
204
+ bands.forEach((band) => lane.append(renderBand(band, solventTravelMm)));
205
+ }
206
+
207
+ function renderSolvent(frontPosition: number, timeMinutes: number): void {
208
+ if (front) front.style.bottom = `${frontPosition}%`;
209
+ if (solvent) solvent.style.height = `${Math.min(42, 10 + timeMinutes * 2.2)}%`;
210
+ }
211
+
212
+ function renderReadouts(timeMinutes: number, solventTravelMm: number, matchScore: number): void {
213
+ setText('tlc-time-label', `${timeMinutes.toFixed(1)} ${ui.unitMinute}`);
214
+ setText('tlc-front-distance', formatDistance(solventTravelMm));
215
+ setText('tlc-match', `${matchScore}%`);
216
+ }
217
+
218
+ function render(): void {
219
+ const { timeMinutes, ink, solventSystem } = getSimulationInput();
220
+ const result = model.simulate(timeMinutes, ink, solventSystem);
221
+ const solventTravelMm = result.solventFrontMm - result.originMm;
222
+ const frontPosition = 10 + (solventTravelMm / (result.plateHeightMm - result.originMm)) * 82;
223
+
224
+ renderLane(result.bands, solventTravelMm);
225
+ renderSolvent(frontPosition, timeMinutes);
226
+ renderReadouts(timeMinutes, solventTravelMm, result.matchScore);
227
+ renderTable(result.bands);
228
+ saveState();
229
+ }
230
+
231
+ inkSelect?.addEventListener('change', render);
232
+ solventSelect?.addEventListener('change', render);
233
+ timeInput?.addEventListener('input', render);
234
+ document.querySelectorAll<HTMLButtonElement>('[data-tlc-unit]').forEach((button) => {
235
+ button.addEventListener('click', () => {
236
+ unitSystem = button.dataset.tlcUnit === 'imperial' ? 'imperial' : 'metric';
237
+ updateUnitButtons();
238
+ render();
239
+ });
240
+ });
241
+
242
+ restoreState();
243
+ updateUnitButtons();
244
+ render();
245
+ </script>