@jjlmoya/utils-forensic-science 1.20.0 → 1.21.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 (36) hide show
  1. package/package.json +2 -2
  2. package/scripts/validate-icons.mjs +57 -0
  3. package/src/category/index.ts +10 -10
  4. package/src/entries.ts +26 -25
  5. package/src/index.ts +7 -6
  6. package/src/tests/locale_completeness.test.ts +23 -23
  7. package/src/tests/tool_validation.test.ts +16 -16
  8. package/src/tool/forensic-evidence-scale-calculator/bibliography.astro +9 -0
  9. package/src/tool/forensic-evidence-scale-calculator/bibliography.ts +16 -0
  10. package/src/tool/forensic-evidence-scale-calculator/component.astro +154 -0
  11. package/src/tool/forensic-evidence-scale-calculator/entry.ts +29 -0
  12. package/src/tool/forensic-evidence-scale-calculator/forensic-evidence-scale-calculator.css +516 -0
  13. package/src/tool/forensic-evidence-scale-calculator/i18n/de.ts +3 -0
  14. package/src/tool/forensic-evidence-scale-calculator/i18n/en.ts +299 -0
  15. package/src/tool/forensic-evidence-scale-calculator/i18n/es.ts +3 -0
  16. package/src/tool/forensic-evidence-scale-calculator/i18n/fr.ts +3 -0
  17. package/src/tool/forensic-evidence-scale-calculator/i18n/id.ts +3 -0
  18. package/src/tool/forensic-evidence-scale-calculator/i18n/it.ts +3 -0
  19. package/src/tool/forensic-evidence-scale-calculator/i18n/ja.ts +3 -0
  20. package/src/tool/forensic-evidence-scale-calculator/i18n/ko.ts +3 -0
  21. package/src/tool/forensic-evidence-scale-calculator/i18n/localized-content.ts +1537 -0
  22. package/src/tool/forensic-evidence-scale-calculator/i18n/nl.ts +3 -0
  23. package/src/tool/forensic-evidence-scale-calculator/i18n/pl.ts +3 -0
  24. package/src/tool/forensic-evidence-scale-calculator/i18n/pt.ts +3 -0
  25. package/src/tool/forensic-evidence-scale-calculator/i18n/ru.ts +3 -0
  26. package/src/tool/forensic-evidence-scale-calculator/i18n/sv.ts +3 -0
  27. package/src/tool/forensic-evidence-scale-calculator/i18n/tr.ts +3 -0
  28. package/src/tool/forensic-evidence-scale-calculator/i18n/zh.ts +3 -0
  29. package/src/tool/forensic-evidence-scale-calculator/index.ts +11 -0
  30. package/src/tool/forensic-evidence-scale-calculator/logic.test.ts +55 -0
  31. package/src/tool/forensic-evidence-scale-calculator/logic.ts +107 -0
  32. package/src/tool/forensic-evidence-scale-calculator/seo.astro +10 -0
  33. package/src/tool/forensic-evidence-scale-calculator/ui.ts +3 -0
  34. package/src/tool/forensic-evidence-scale-calculator/unit-selector.ts +120 -0
  35. package/src/tool/forensic-evidence-scale-calculator/view.ts +203 -0
  36. package/src/tools.ts +8 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-forensic-science",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -36,7 +36,7 @@
36
36
  "postinstall": "node scripts/postinstall.mjs",
37
37
  "predev": "node scripts/postinstall.mjs",
38
38
  "prestart": "node scripts/postinstall.mjs",
39
- "prebuild": "node scripts/postinstall.mjs",
39
+ "prebuild": "node scripts/postinstall.mjs && node scripts/validate-icons.mjs",
40
40
  "qa": "npm run lint && npm run test && npm run build",
41
41
  "cf:dry-run": "npm run build && wrangler deploy --dry-run",
42
42
  "cf:preview": "npm run build && wrangler deploy --config wrangler.staging.jsonc",
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'node:module';
4
+ import { readFileSync, readdirSync } from 'node:fs';
5
+ import { dirname, join, relative, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const packageJsonPath = require.resolve('@iconify-json/mdi/package.json');
10
+ const packageRoot = dirname(packageJsonPath);
11
+ const iconSet = JSON.parse(readFileSync(join(packageRoot, 'icons.json'), 'utf8'));
12
+ const availableIcons = new Set([
13
+ ...Object.keys(iconSet.icons ?? {}),
14
+ ...Object.keys(iconSet.aliases ?? {}),
15
+ ]);
16
+
17
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
+ const sourceRoot = resolve(repoRoot, 'src');
19
+ const extensions = new Set(['.astro', '.js', '.mjs', '.ts', '.tsx']);
20
+ const iconPattern = /\bmdi:([a-z0-9-]+)\b/g;
21
+ const failures = [];
22
+ let references = 0;
23
+
24
+ function walk(directory) {
25
+ const entries = readdirSync(directory, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ const path = join(directory, entry.name);
28
+ if (entry.isDirectory()) {
29
+ if (entry.name !== 'node_modules' && entry.name !== 'dist' && entry.name !== 'tests') {
30
+ walk(path);
31
+ }
32
+ continue;
33
+ }
34
+ if (!extensions.has(path.slice(path.lastIndexOf('.')))) continue;
35
+
36
+ const source = readFileSync(path, 'utf8');
37
+ for (const match of source.matchAll(iconPattern)) {
38
+ references += 1;
39
+ const iconName = match[1];
40
+ if (availableIcons.has(iconName)) continue;
41
+
42
+ const line = source.slice(0, match.index).split('\n').length;
43
+ failures.push(`${relative(repoRoot, path)}:${line} — mdi:${iconName}`);
44
+ }
45
+ }
46
+ }
47
+
48
+ walk(sourceRoot);
49
+
50
+ if (failures.length > 0) {
51
+ console.error('Invalid MDI icons found:');
52
+ for (const failure of failures) console.error(`- ${failure}`);
53
+ console.error(`Checked ${references} MDI icon references against @iconify-json/mdi.`);
54
+ process.exitCode = 1;
55
+ } else {
56
+ console.log(`MDI icon validation passed: ${references} references checked.`);
57
+ }
@@ -14,9 +14,10 @@ import { bloodstainPatternOriginAnalyzer } from '../tool/bloodstain-pattern-orig
14
14
  import { forensicFingerprintMinutiaeIdentifier } from '../tool/forensic-fingerprint-minutiae-identifier/entry';
15
15
  import { firePatternOriginAnalyzer } from '../tool/fire-pattern-origin-analyzer/entry';
16
16
  import { forensicToolmarkStriationMatcher } from '../tool/forensic-toolmark-striation-matcher/entry';
17
- import { timeOfDeathAlgorMortisCalculator } from '../tool/time-of-death-algor-mortis-calculator/entry';
18
- import { voiceSpectrogramAnalyzer } from '../tool/voice-spectrogram-analyzer/entry';
19
- import { dnaProfileMatchProbabilityLab } from '../tool/dna-profile-match-probability-lab/entry';
17
+ import { timeOfDeathAlgorMortisCalculator } from '../tool/time-of-death-algor-mortis-calculator/entry';
18
+ import { voiceSpectrogramAnalyzer } from '../tool/voice-spectrogram-analyzer/entry';
19
+ import { dnaProfileMatchProbabilityLab } from '../tool/dna-profile-match-probability-lab/entry';
20
+ import { forensicEvidenceScaleCalculator } from '../tool/forensic-evidence-scale-calculator/entry';
20
21
 
21
22
  export const forensicCategory: ScienceCategoryEntry = {
22
23
  icon: 'mdi:fingerprint',
@@ -36,13 +37,12 @@ export const forensicCategory: ScienceCategoryEntry = {
36
37
  forensicFingerprintMinutiaeIdentifier,
37
38
  firePatternOriginAnalyzer,
38
39
  forensicToolmarkStriationMatcher,
39
- timeOfDeathAlgorMortisCalculator,
40
- voiceSpectrogramAnalyzer,
41
- dnaProfileMatchProbabilityLab
40
+ timeOfDeathAlgorMortisCalculator,
41
+ voiceSpectrogramAnalyzer,
42
+ dnaProfileMatchProbabilityLab,
43
+ forensicEvidenceScaleCalculator
42
44
  ],
43
45
 
44
-
45
-
46
46
  i18n: {
47
47
  de: () => import('./i18n/de').then((m) => m.content),
48
48
  en: () => import('./i18n/en').then((m) => m.content),
@@ -58,8 +58,8 @@ export const forensicCategory: ScienceCategoryEntry = {
58
58
  ru: () => import('./i18n/ru').then((m) => m.content),
59
59
  sv: () => import('./i18n/sv').then((m) => m.content),
60
60
  tr: () => import('./i18n/tr').then((m) => m.content),
61
- zh: () => import('./i18n/zh').then((m) => m.content)
62
- }
61
+ zh: () => import('./i18n/zh').then((m) => m.content)
62
+ }
63
63
  };
64
64
 
65
65
  export const scienceCategory = forensicCategory;
package/src/entries.ts CHANGED
@@ -1,37 +1,39 @@
1
1
  export { forensicAgeEstimator } from './tool/forensic-age-estimator/entry';
2
- export type { ForensicAgeEstimatorUI, ForensicAgeEstimatorLocaleContent } from './tool/forensic-age-estimator/entry';
2
+ export type { ForensicAgeEstimatorUI, ForensicAgeEstimatorLocaleContent } from './tool/forensic-age-estimator/entry';
3
3
  export { widmarkAlcoholSimulator } from './tool/widmark-alcohol-simulator/entry';
4
- export type { WidmarkAlcoholSimulatorUI, WidmarkAlcoholSimulatorLocaleContent } from './tool/widmark-alcohol-simulator/entry';
4
+ export type { WidmarkAlcoholSimulatorUI, WidmarkAlcoholSimulatorLocaleContent } from './tool/widmark-alcohol-simulator/entry';
5
5
  export { forensicSexDeterminator } from './tool/forensic-sex-determinator/entry';
6
- export type { SexDeterminatorUI, SexDeterminatorLocaleContent } from './tool/forensic-sex-determinator/entry';
6
+ export type { SexDeterminatorUI, SexDeterminatorLocaleContent } from './tool/forensic-sex-determinator/entry';
7
7
  export { forensicStatureEstimator } from './tool/forensic-stature-estimator/entry';
8
- export type { StatureEstimatorUI, StatureEstimatorLocaleContent } from './tool/forensic-stature-estimator/entry';
8
+ export type { StatureEstimatorUI, StatureEstimatorLocaleContent } from './tool/forensic-stature-estimator/entry';
9
9
  export { forensicBloodTestSimulator } from './tool/forensic-blood-test-simulator/entry';
10
- export type { BloodTestUI, BloodTestLocaleContent } from './tool/forensic-blood-test-simulator/entry';
10
+ export type { BloodTestUI, BloodTestLocaleContent } from './tool/forensic-blood-test-simulator/entry';
11
11
  export { forensicImageAuthenticityAnalyzer } from './tool/forensic-image-authenticity-analyzer/entry';
12
- export type { ImageAuthenticityUI, ImageAuthenticityLocaleContent } from './tool/forensic-image-authenticity-analyzer/entry';
12
+ export type { ImageAuthenticityUI, ImageAuthenticityLocaleContent } from './tool/forensic-image-authenticity-analyzer/entry';
13
13
  export { gsrDispersionCalculator } from './tool/gsr-dispersion-calculator/entry';
14
- export type { GsrDispersionUI, GsrDispersionLocaleContent } from './tool/gsr-dispersion-calculator/entry';
14
+ export type { GsrDispersionUI, GsrDispersionLocaleContent } from './tool/gsr-dispersion-calculator/entry';
15
15
  export { forensicTlcInkSimulator } from './tool/forensic-tlc-ink-simulator/entry';
16
- export type { TlcInkSimulatorUI, TlcInkSimulatorLocaleContent } from './tool/forensic-tlc-ink-simulator/entry';
16
+ export type { TlcInkSimulatorUI, TlcInkSimulatorLocaleContent } from './tool/forensic-tlc-ink-simulator/entry';
17
17
  export { forensicMicrocrystalDrugSimulator } from './tool/forensic-microcrystal-drug-simulator/entry';
18
- export type { MicrocrystalDrugSimulatorUI, MicrocrystalDrugSimulatorLocaleContent } from './tool/forensic-microcrystal-drug-simulator/entry';
18
+ export type { MicrocrystalDrugSimulatorUI, MicrocrystalDrugSimulatorLocaleContent } from './tool/forensic-microcrystal-drug-simulator/entry';
19
19
  export { forensicGlassBeckeLineSimulator } from './tool/forensic-glass-becke-line-simulator/entry';
20
- export type { GlassBeckeLineSimulatorUI, GlassBeckeLineSimulatorLocaleContent } from './tool/forensic-glass-becke-line-simulator/entry';
20
+ export type { GlassBeckeLineSimulatorUI, GlassBeckeLineSimulatorLocaleContent } from './tool/forensic-glass-becke-line-simulator/entry';
21
21
  export { forensicFiberComparisonMicroscope } from './tool/forensic-fiber-comparison-microscope/entry';
22
- export type { FiberComparisonMicroscopeUI, FiberComparisonMicroscopeLocaleContent } from './tool/forensic-fiber-comparison-microscope/entry';
22
+ export type { FiberComparisonMicroscopeUI, FiberComparisonMicroscopeLocaleContent } from './tool/forensic-fiber-comparison-microscope/entry';
23
23
  export { bloodstainPatternOriginAnalyzer } from './tool/bloodstain-pattern-origin-analyzer/entry';
24
- export type { BloodstainPatternUI, BloodstainPatternLocaleContent } from './tool/bloodstain-pattern-origin-analyzer/entry';
24
+ export type { BloodstainPatternUI, BloodstainPatternLocaleContent } from './tool/bloodstain-pattern-origin-analyzer/entry';
25
25
  export { forensicFingerprintMinutiaeIdentifier } from './tool/forensic-fingerprint-minutiae-identifier/entry';
26
- export type { FingerprintMinutiaeUI, FingerprintMinutiaeLocaleContent } from './tool/forensic-fingerprint-minutiae-identifier/entry';
26
+ export type { FingerprintMinutiaeUI, FingerprintMinutiaeLocaleContent } from './tool/forensic-fingerprint-minutiae-identifier/entry';
27
27
  export { firePatternOriginAnalyzer } from './tool/fire-pattern-origin-analyzer/entry';
28
- export type { FirePatternOriginAnalyzerUI, FirePatternOriginAnalyzerLocaleContent } from './tool/fire-pattern-origin-analyzer/entry';
28
+ export type { FirePatternOriginAnalyzerUI, FirePatternOriginAnalyzerLocaleContent } from './tool/fire-pattern-origin-analyzer/entry';
29
29
  export { forensicToolmarkStriationMatcher } from './tool/forensic-toolmark-striation-matcher/entry';
30
- export type { ToolmarkStriationMatcherUI, ToolmarkStriationMatcherLocaleContent } from './tool/forensic-toolmark-striation-matcher/entry';
30
+ export type { ToolmarkStriationMatcherUI, ToolmarkStriationMatcherLocaleContent } from './tool/forensic-toolmark-striation-matcher/entry';
31
31
  export { timeOfDeathAlgorMortisCalculator } from './tool/time-of-death-algor-mortis-calculator/entry';
32
32
  export type { TimeOfDeathAlgorMortisUI, TimeOfDeathAlgorMortisLocaleContent } from './tool/time-of-death-algor-mortis-calculator/entry';
33
- export { voiceSpectrogramAnalyzer } from './tool/voice-spectrogram-analyzer/entry';
33
+ export { voiceSpectrogramAnalyzer } from './tool/voice-spectrogram-analyzer/entry';
34
34
  export type { VoiceSpectrogramUI, VoiceSpectrogramLocaleContent } from './tool/voice-spectrogram-analyzer/entry';
35
+ export { forensicEvidenceScaleCalculator } from './tool/forensic-evidence-scale-calculator/entry';
36
+ export type { ForensicEvidenceScaleUI, ForensicEvidenceScaleLocaleContent } from './tool/forensic-evidence-scale-calculator/entry';
35
37
 
36
38
  import { forensicAgeEstimator } from './tool/forensic-age-estimator/entry';
37
39
  import { widmarkAlcoholSimulator } from './tool/widmark-alcohol-simulator/entry';
@@ -48,9 +50,10 @@ import { bloodstainPatternOriginAnalyzer } from './tool/bloodstain-pattern-origi
48
50
  import { forensicFingerprintMinutiaeIdentifier } from './tool/forensic-fingerprint-minutiae-identifier/entry';
49
51
  import { firePatternOriginAnalyzer } from './tool/fire-pattern-origin-analyzer/entry';
50
52
  import { forensicToolmarkStriationMatcher } from './tool/forensic-toolmark-striation-matcher/entry';
51
- import { timeOfDeathAlgorMortisCalculator } from './tool/time-of-death-algor-mortis-calculator/entry';
52
- import { voiceSpectrogramAnalyzer } from './tool/voice-spectrogram-analyzer/entry';
53
- import { dnaProfileMatchProbabilityLab } from './tool/dna-profile-match-probability-lab/entry';
53
+ import { timeOfDeathAlgorMortisCalculator } from './tool/time-of-death-algor-mortis-calculator/entry';
54
+ import { voiceSpectrogramAnalyzer } from './tool/voice-spectrogram-analyzer/entry';
55
+ import { dnaProfileMatchProbabilityLab } from './tool/dna-profile-match-probability-lab/entry';
56
+ import { forensicEvidenceScaleCalculator } from './tool/forensic-evidence-scale-calculator/entry';
54
57
 
55
58
  export const ALL_ENTRIES = [
56
59
  forensicAgeEstimator,
@@ -68,10 +71,8 @@ export const ALL_ENTRIES = [
68
71
  forensicFingerprintMinutiaeIdentifier,
69
72
  firePatternOriginAnalyzer,
70
73
  forensicToolmarkStriationMatcher,
71
- timeOfDeathAlgorMortisCalculator,
72
- voiceSpectrogramAnalyzer,
73
- dnaProfileMatchProbabilityLab
74
+ timeOfDeathAlgorMortisCalculator,
75
+ voiceSpectrogramAnalyzer,
76
+ dnaProfileMatchProbabilityLab,
77
+ forensicEvidenceScaleCalculator
74
78
  ];
75
-
76
-
77
-
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { forensicCategory, forensicCategory as templateCategory } from './category';
2
- export const ForensicCategorySEO = () => import('./category/seo.astro').then((m) => m.default);
1
+ export { forensicCategory, forensicCategory as templateCategory } from './category';
2
+ export const ForensicCategorySEO = () => import('./category/seo.astro').then((m) => m.default);
3
3
  export { FORENSIC_AGE_ESTIMATOR_TOOL } from './tool/forensic-age-estimator/index';
4
4
  export { WIDMARK_ALCOHOL_SIMULATOR_TOOL } from './tool/widmark-alcohol-simulator/index';
5
5
  export { FORENSIC_SEX_DETERMINATOR_TOOL } from './tool/forensic-sex-determinator/index';
@@ -13,10 +13,11 @@ export { FORENSIC_GLASS_BECKE_LINE_SIMULATOR_TOOL } from './tool/forensic-glass-
13
13
  export { FORENSIC_FIBER_COMPARISON_MICROSCOPE_TOOL } from './tool/forensic-fiber-comparison-microscope/index';
14
14
  export { BLOODSTAIN_PATTERN_ORIGIN_ANALYZER_TOOL } from './tool/bloodstain-pattern-origin-analyzer/index';
15
15
  export { FORENSIC_FINGERPRINT_MINUTIAE_IDENTIFIER_TOOL } from './tool/forensic-fingerprint-minutiae-identifier/index';
16
- export { FIRE_PATTERN_ORIGIN_ANALYZER_TOOL } from './tool/fire-pattern-origin-analyzer/index';
17
- export { FORENSIC_TOOLMARK_STRIATION_MATCHER_TOOL } from './tool/forensic-toolmark-striation-matcher/index';
18
- export { VOICE_SPECTROGRAM_ANALYZER_TOOL } from './tool/voice-spectrogram-analyzer/index';
19
- export { DNA_PROFILE_MATCH_PROBABILITY_LAB_TOOL } from './tool/dna-profile-match-probability-lab/index';
16
+ export { FIRE_PATTERN_ORIGIN_ANALYZER_TOOL } from './tool/fire-pattern-origin-analyzer/index';
17
+ export { FORENSIC_TOOLMARK_STRIATION_MATCHER_TOOL } from './tool/forensic-toolmark-striation-matcher/index';
18
+ export { VOICE_SPECTROGRAM_ANALYZER_TOOL } from './tool/voice-spectrogram-analyzer/index';
19
+ export { DNA_PROFILE_MATCH_PROBABILITY_LAB_TOOL } from './tool/dna-profile-match-probability-lab/index';
20
+ export { FORENSIC_EVIDENCE_SCALE_CALCULATOR_TOOL } from './tool/forensic-evidence-scale-calculator/index';
20
21
 
21
22
  export type {
22
23
  KnownLocale,
@@ -1,24 +1,24 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
- import type { ToolLocaleContent } from '../types';
4
-
5
- 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('faq and bibliography should be arrays', async () => {
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import type { ToolLocaleContent } from '../types';
4
+
5
+ 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('faq and bibliography should be arrays', async () => {
11
11
  const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
12
- const content = (await loader?.()) as ToolLocaleContent;
13
- expect(Array.isArray(content.faq)).toBe(true);
14
- expect(Array.isArray(content.bibliography)).toBe(true);
15
- });
16
- });
17
- });
18
- });
19
- });
20
-
21
- it('all 18 tools registered', () => {
22
- expect(ALL_TOOLS.length).toBe(18);
23
- });
24
- });
12
+ const content = (await loader?.()) as ToolLocaleContent;
13
+ expect(Array.isArray(content.faq)).toBe(true);
14
+ expect(Array.isArray(content.bibliography)).toBe(true);
15
+ });
16
+ });
17
+ });
18
+ });
19
+ });
20
+
21
+ it('all 19 tools registered', () => {
22
+ expect(ALL_TOOLS.length).toBe(19);
23
+ });
24
+ });
@@ -1,16 +1,16 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
- import { scienceCategory } from '../data';
4
-
5
- describe('Tool Validation Suite', () => {
6
- describe('Library Registration', () => {
7
- it('should have 18 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(18);
9
- });
10
-
11
- it('scienceCategory should be defined', () => {
12
- expect(scienceCategory).toBeDefined();
13
- expect(scienceCategory.i18n).toBeDefined();
14
- });
15
- });
16
- });
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import { scienceCategory } from '../data';
4
+
5
+ describe('Tool Validation Suite', () => {
6
+ describe('Library Registration', () => {
7
+ it('should have 19 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(19);
9
+ });
10
+
11
+ it('scienceCategory should be defined', () => {
12
+ expect(scienceCategory).toBeDefined();
13
+ expect(scienceCategory.i18n).toBeDefined();
14
+ });
15
+ });
16
+ });
@@ -0,0 +1,9 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { forensicEvidenceScaleCalculator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+ interface Props { locale?: KnownLocale; }
6
+ const { locale = 'en' } = Astro.props;
7
+ const content = await forensicEvidenceScaleCalculator.i18n[locale]?.();
8
+ ---
9
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from "../../types";
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: "NIST and NIJ: Dimensional Review of Scales for Forensic Photography",
6
+ url: "https://www.nist.gov/forensics/research/upload/Dimensional-Review-of-Scales-for-Forensic-Photography.pdf",
7
+ },
8
+ {
9
+ name: "SWGDE: Guidelines for Evidence Photography in a Controlled Setting",
10
+ url: "https://www.nist.gov/osac/standards-library/swgde-18-p-001-20",
11
+ },
12
+ {
13
+ name: "Guardia Civil: Guía de servicios de criminalística",
14
+ url: "https://www.interior.gob.es/opencms/pdf/archivos-y-documentacion/documentacion-y-publicaciones/publicaciones-descargables/seguridad-ciudadana/Guia_de_servicios_CRIMINALISTICA_126240639_pdfWEB.pdf",
15
+ },
16
+ ];
@@ -0,0 +1,154 @@
1
+ ---
2
+ import './forensic-evidence-scale-calculator.css';
3
+ import { calculateEvidenceScale, formatMeasurement } from './logic';
4
+ import type { ForensicEvidenceScaleUI } from './ui';
5
+
6
+ interface Props {
7
+ ui: ForensicEvidenceScaleUI;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ const initialInput = {
12
+ referenceRealSize: 25,
13
+ referencePixelLength: 200,
14
+ evidencePixelLength: 80,
15
+ uncertaintyPercent: 5,
16
+ unit: 'mm' as const,
17
+ samePlane: true,
18
+ };
19
+ const initialResult = calculateEvidenceScale(initialInput);
20
+ ---
21
+
22
+ <div class="evidence-scale-tool" id="forensic-evidence-scale-calculator" data-ready="true">
23
+ <section class="evidence-scale-scene" data-scene data-warning="false" aria-label={ui.sceneAria}>
24
+ <div class="evidence-scale-scene-head">
25
+ <div>
26
+ <span class="evidence-scale-kicker">{ui.sceneKicker}</span>
27
+ <h2>{ui.sceneTitle}</h2>
28
+ </div>
29
+ <span class="evidence-scale-scene-note">{ui.sceneNote}</span>
30
+ </div>
31
+
32
+ <div class="evidence-scale-illustration">
33
+ <svg viewBox="0 0 640 410" role="img" aria-label={ui.sceneAria}>
34
+ <defs>
35
+ <linearGradient id="evidence-surface" x1="0" y1="0" x2="1" y2="1">
36
+ <stop offset="0" stop-color="var(--evidence-svg-surface-start)" />
37
+ <stop offset="1" stop-color="var(--evidence-svg-surface-end)" />
38
+ </linearGradient>
39
+ <filter id="evidence-shadow" x="-30%" y="-30%" width="160%" height="160%">
40
+ <feGaussianBlur stdDeviation="8" />
41
+ </filter>
42
+ </defs>
43
+ <rect width="640" height="410" rx="22" fill="url(#evidence-surface)" />
44
+ <path d="M18 350 C138 305 218 366 327 330 S516 296 622 344" fill="none" stroke="var(--evidence-svg-wash)" stroke-width="26" opacity=".4" />
45
+ <path d="M40 54 L606 54 M40 92 L606 92 M40 130 L606 130 M40 168 L606 168 M40 206 L606 206 M40 244 L606 244 M40 54 L40 244 M134 54 L134 244 M228 54 L228 244 M322 54 L322 244 M416 54 L416 244 M510 54 L510 244 M604 54 L604 244" stroke="var(--evidence-svg-grid)" stroke-width="1" opacity=".4" />
46
+ <path d="M172 202 C176 157 207 122 253 119 C287 116 319 132 340 156 C369 119 417 116 446 140 C480 168 472 222 443 248 C406 281 356 269 327 249 C293 284 236 280 201 252 C183 238 172 221 172 202Z" fill="var(--evidence-svg-silhouette)" filter="url(#evidence-shadow)" />
47
+ <path d="M166 190 C172 145 207 111 250 111 C286 110 315 126 336 150 C366 114 414 111 444 135 C476 161 468 214 439 239 C405 269 357 259 325 239 C291 273 235 267 200 241 C179 225 165 208 166 190Z" fill="var(--evidence-svg-object)" stroke="var(--evidence-svg-object-border)" stroke-width="4" />
48
+ <path d="M191 184 C221 150 256 139 299 153 M204 220 C245 188 292 183 334 197 M350 167 C374 153 410 157 441 180 M355 221 C387 210 416 210 438 218" fill="none" stroke="var(--evidence-svg-object-mark)" stroke-width="8" opacity=".7" />
49
+ <path d="M218 143 L249 238 M272 128 L293 247 M382 130 L359 245 M423 148 L395 246" stroke="var(--evidence-svg-object-shadow)" stroke-width="5" opacity=".75" />
50
+ <line x1="220" y1="296" x2="390" y2="296" stroke="#db5d55" stroke-width="4" stroke-linecap="round" data-scene="evidence-line" />
51
+ <path d="M220 282 L220 310 M390 282 L390 310" stroke="#db5d55" stroke-width="4" />
52
+ <line x1="220" y1="318" x2="380" y2="318" stroke="#39c2d2" stroke-width="4" stroke-dasharray="10 9" stroke-linecap="round" data-scene="reference-line" />
53
+ <path d="M220 306 L220 330 M380 306 L380 330" stroke="#39c2d2" stroke-width="4" />
54
+ <rect x="62" y="342" width="516" height="34" rx="7" fill="var(--evidence-svg-ruler)" stroke="var(--evidence-svg-ruler-ink)" stroke-width="4" />
55
+ <path d="M78 342 V362 M90 342 V354 M102 342 V362 M114 342 V354 M126 342 V362 M138 342 V354 M150 342 V362 M162 342 V354 M174 342 V362 M186 342 V354 M198 342 V362 M210 342 V354 M222 342 V362 M234 342 V354 M246 342 V362 M258 342 V354 M270 342 V362 M282 342 V354 M294 342 V362 M306 342 V354 M318 342 V362 M330 342 V354 M342 342 V362 M354 342 V354 M366 342 V362 M378 342 V354 M390 342 V362 M402 342 V354 M414 342 V362 M426 342 V354 M438 342 V362 M450 342 V354 M462 342 V362 M474 342 V354 M486 342 V362 M498 342 V354 M510 342 V362 M522 342 V354 M534 342 V362 M546 342 V354 M558 342 V362" stroke="var(--evidence-svg-ruler-ink)" stroke-width="2" />
56
+ </svg>
57
+ </div>
58
+
59
+ <div class="evidence-scale-scene-rail" aria-label={ui.sceneTitle}>
60
+ <div class="evidence-scale-rail-item">
61
+ <span class="evidence-scale-rail-number">01</span>
62
+ <span class="evidence-scale-rail-dot legend-cyan"></span>
63
+ <span><strong>{ui.referenceLength}</strong><small>{ui.referencePixelLength}</small></span>
64
+ </div>
65
+ <div class="evidence-scale-rail-item">
66
+ <span class="evidence-scale-rail-number">02</span>
67
+ <span class="evidence-scale-rail-dot legend-coral"></span>
68
+ <span><strong>{ui.evidenceLength}</strong><small>{ui.evidencePixelLength}</small></span>
69
+ </div>
70
+ <div class="evidence-scale-rail-item">
71
+ <span class="evidence-scale-rail-number">03</span>
72
+ <span class="evidence-scale-rail-mark">≈</span>
73
+ <span><strong>{ui.results}</strong><small>{ui.interval}</small></span>
74
+ </div>
75
+ </div>
76
+ </section>
77
+
78
+ <div class="evidence-scale-side">
79
+ <section class="evidence-scale-controls" aria-label={ui.controls}>
80
+ <div class="evidence-scale-control-head">
81
+ <span class="evidence-scale-kicker">{ui.controlKicker}</span>
82
+ <p>{ui.controlHelper}</p>
83
+ </div>
84
+
85
+ <form class="evidence-scale-form">
86
+ <div class="evidence-scale-field-grid">
87
+ <label class="evidence-scale-field">
88
+ <span>{ui.referenceRealSize}</span>
89
+ <input name="referenceRealSize" type="number" min="0.001" step="any" value={initialInput.referenceRealSize} inputmode="decimal" />
90
+ <small>{ui.referenceRealHelp}</small>
91
+ </label>
92
+ <label class="evidence-scale-field">
93
+ <span>{ui.referencePixelLength}</span>
94
+ <input name="referencePixelLength" type="number" min="0.001" step="any" value={initialInput.referencePixelLength} inputmode="decimal" />
95
+ <small>{ui.pixelHelp}</small>
96
+ </label>
97
+ <label class="evidence-scale-field">
98
+ <span>{ui.evidencePixelLength}</span>
99
+ <input name="evidencePixelLength" type="number" min="0.001" step="any" value={initialInput.evidencePixelLength} inputmode="decimal" />
100
+ <small>{ui.pixelHelp}</small>
101
+ </label>
102
+ <label class="evidence-scale-field">
103
+ <span>{ui.uncertaintyPercent}</span>
104
+ <input name="uncertaintyPercent" type="number" min="0" max="100" step="0.1" value={initialInput.uncertaintyPercent} inputmode="decimal" />
105
+ <small>{ui.uncertaintyHelp}</small>
106
+ </label>
107
+ </div>
108
+
109
+ <div class="evidence-scale-field evidence-scale-unit" data-unit-choice>
110
+ <span>{ui.unit}</span>
111
+ <input name="unit" type="hidden" value={initialInput.unit} />
112
+ <button class="evidence-scale-choice-trigger" type="button" data-unit-trigger aria-haspopup="listbox" aria-expanded="false" aria-label={ui.unit} aria-controls="evidence-scale-unit-menu">
113
+ <span data-unit-label>{ui.millimetres}</span>
114
+ <span class="evidence-scale-choice-chevron" aria-hidden="true">⌄</span>
115
+ </button>
116
+ <div class="evidence-scale-choice-menu" id="evidence-scale-unit-menu" data-unit-menu role="listbox" aria-label={ui.unit} hidden>
117
+ <button type="button" role="option" data-unit-option="mm" aria-selected="true">{ui.millimetres}</button>
118
+ <button type="button" role="option" data-unit-option="cm" aria-selected="false">{ui.centimetres}</button>
119
+ <button type="button" role="option" data-unit-option="in" aria-selected="false">{ui.inches}</button>
120
+ </div>
121
+ </div>
122
+
123
+ <label class="evidence-scale-plane">
124
+ <input name="samePlane" type="checkbox" checked={initialInput.samePlane} />
125
+ <span><strong>{ui.samePlane}</strong><small>{ui.samePlaneHelp}</small></span>
126
+ </label>
127
+ </form>
128
+ <p class="evidence-scale-disclaimer">{ui.disclaimer}</p>
129
+ </section>
130
+
131
+ <section class="evidence-scale-results" aria-label={ui.results}>
132
+ <div class="evidence-scale-result-lead">
133
+ <span class="evidence-scale-kicker">{ui.resultKicker}</span>
134
+ <output data-result="size" aria-live="polite">{formatMeasurement(initialResult.evidenceSize, initialResult.unit)}</output>
135
+ <p data-result="message">{ui.calculatedMessage}</p>
136
+ </div>
137
+ <div class="evidence-scale-readouts">
138
+ <div><span>{ui.interval}</span><strong data-result="interval">{formatMeasurement(initialResult.lowerBound, initialResult.unit)} - {formatMeasurement(initialResult.upperBound, initialResult.unit)}</strong></div>
139
+ <div><span>{ui.scaleFactor}</span><strong data-result="scale">{initialResult.scalePerPixel} mm/px</strong></div>
140
+ <div><span>{ui.margin}</span><strong data-result="margin">{initialInput.uncertaintyPercent}%</strong></div>
141
+ </div>
142
+ <div class="evidence-scale-trace"><span>{ui.traceLabel}</span><code data-result="formula">{initialResult.formula}</code></div>
143
+ <p class="evidence-scale-warning" data-result="warning" data-visible="false">{ui.samePlaneMessage}</p>
144
+ </section>
145
+ </div>
146
+ </div>
147
+
148
+ <script is:inline id="forensic-evidence-scale-ui" type="application/json" set:html={JSON.stringify(ui)}></script>
149
+ <script>
150
+ import { initForensicEvidenceScale } from './view';
151
+ const root = document.getElementById('forensic-evidence-scale-calculator');
152
+ const uiElement = document.getElementById('forensic-evidence-scale-ui');
153
+ if (root) initForensicEvidenceScale(root, JSON.parse(uiElement?.textContent ?? '{}'));
154
+ </script>
@@ -0,0 +1,29 @@
1
+ import type { ScienceToolEntry, ToolLocaleContent } from "../../types";
2
+ import type { ForensicEvidenceScaleUI } from "./ui";
3
+ export type { ForensicEvidenceScaleUI } from "./ui";
4
+
5
+ export type ForensicEvidenceScaleLocaleContent =
6
+ ToolLocaleContent<ForensicEvidenceScaleUI>;
7
+
8
+ export const forensicEvidenceScaleCalculator: ScienceToolEntry<ForensicEvidenceScaleUI> =
9
+ {
10
+ id: "forensic-evidence-scale-calculator",
11
+ icons: { bg: "mdi:camera-outline", fg: "mdi:ruler-square" },
12
+ i18n: {
13
+ en: () => import("./i18n/en").then((m) => m.content),
14
+ de: () => import("./i18n/de").then((m) => m.content),
15
+ es: () => import("./i18n/es").then((m) => m.content),
16
+ fr: () => import("./i18n/fr").then((m) => m.content),
17
+ id: () => import("./i18n/id").then((m) => m.content),
18
+ it: () => import("./i18n/it").then((m) => m.content),
19
+ ja: () => import("./i18n/ja").then((m) => m.content),
20
+ ko: () => import("./i18n/ko").then((m) => m.content),
21
+ nl: () => import("./i18n/nl").then((m) => m.content),
22
+ pl: () => import("./i18n/pl").then((m) => m.content),
23
+ pt: () => import("./i18n/pt").then((m) => m.content),
24
+ ru: () => import("./i18n/ru").then((m) => m.content),
25
+ sv: () => import("./i18n/sv").then((m) => m.content),
26
+ tr: () => import("./i18n/tr").then((m) => m.content),
27
+ zh: () => import("./i18n/zh").then((m) => m.content),
28
+ },
29
+ };