diff --git a/src/components/advanced/AdvancedTools.tsx b/src/components/advanced/AdvancedTools.tsx new file mode 100644 index 0000000..78f7fe1 --- /dev/null +++ b/src/components/advanced/AdvancedTools.tsx @@ -0,0 +1,87 @@ +/** + * Advanced Analysis tools container. + */ + +import { useState } from 'react'; +import { TreatmentDetection } from './TreatmentDetection'; +import { OriginCharacteristics } from './OriginCharacteristics'; +import { ProportionAnalyzer } from './ProportionAnalyzer'; +import { CalculatorCard } from '../calculator/CalculatorCard'; + +type AdvancedId = 'treatment' | 'origin' | 'proportion'; + +interface AdvancedConfig { + id: AdvancedId; + label: string; + iconPath: string; + description: string; + component: React.ReactNode; +} + +const ICON_PATHS: Record = { + treatment: 'M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z', + globe: 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z', + ruler: 'M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01', +}; + +const ADVANCED_TOOLS: AdvancedConfig[] = [ + { + id: 'treatment', + label: 'Treatment Detection', + iconPath: ICON_PATHS.treatment, + description: 'Visual & instrumental indicators for gem treatments', + component: , + }, + { + id: 'origin', + label: 'Origin Characteristics', + iconPath: ICON_PATHS.globe, + description: 'Geographic origin determination features', + component: , + }, + { + id: 'proportion', + label: 'Proportion Analyzer', + iconPath: ICON_PATHS.ruler, + description: 'Cut quality analysis from proportion measurements', + component: , + }, +]; + +export function AdvancedTools({ defaultExpanded = 'treatment' }: { defaultExpanded?: AdvancedId | null }) { + const [expandedTool, setExpandedTool] = useState(defaultExpanded); + + const handleToggle = (id: AdvancedId) => { + setExpandedTool(prev => (prev === id ? null : id)); + }; + + return ( +
+
+

+ Advanced gemmological analysis tools including treatment detection, origin determination characteristics, and cut proportion evaluation. +

+
+ +
+ {ADVANCED_TOOLS.map(tool => ( + handleToggle(tool.id)} + > + {tool.component} + + ))} +
+ +

+ Tip: Click on any tool header to expand or collapse it. +

+
+ ); +} diff --git a/src/components/advanced/OriginCharacteristics.tsx b/src/components/advanced/OriginCharacteristics.tsx new file mode 100644 index 0000000..bef26e7 --- /dev/null +++ b/src/components/advanced/OriginCharacteristics.tsx @@ -0,0 +1,274 @@ +/** + * Origin Determination Reference + * Geographic origin characteristics for major gem types + */ + +import { useState } from 'react'; + +interface OriginData { + gem: string; + origin: string; + colorCharacteristics: string; + inclusionFingerprints: string; + chemicalIndicators: string; + premium: string; +} + +interface OriginNote { + title: string; + description: string; + importance: 'High' | 'Medium' | 'Low'; +} + +const ORIGIN_DATA: OriginData[] = [ + { + gem: 'Ruby', + origin: 'Myanmar (Burma)', + colorCharacteristics: 'Pure red "pigeon blood", strong fluorescence', + inclusionFingerprints: 'Silk (rutile needles), calcite crystals, liquid feathers', + chemicalIndicators: 'Low Fe content, high Cr, specific trace element ratios', + premium: 'Highest - 20-40% premium over other origins' + }, + { + gem: 'Ruby', + origin: 'Mozambique', + colorCharacteristics: 'Purplish-red to orangey-red, moderate fluorescence', + inclusionFingerprints: 'Amphibole needles, graphite platelets, zircon crystals', + chemicalIndicators: 'Higher Fe content than Burma, V present', + premium: 'Moderate - 10-20% below Burma equivalent' + }, + { + gem: 'Sapphire', + origin: 'Kashmir', + colorCharacteristics: 'Velvety blue, sleepy/milky appearance, strong saturation', + inclusionFingerprints: 'Fine silk creating velvety appearance, negative crystals', + chemicalIndicators: 'Specific Fe/Ti ratios, low Mg', + premium: 'Highest - 50-100% premium, rare and sought-after' + }, + { + gem: 'Sapphire', + origin: 'Myanmar (Burma)', + colorCharacteristics: 'Royal blue, strong saturation, excellent transparency', + inclusionFingerprints: 'Silk (rutile needles), calcite, liquid inclusions', + chemicalIndicators: 'Similar to Kashmir but distinguishable by inclusion scene', + premium: 'High - 30-50% premium' + }, + { + gem: 'Sapphire', + origin: 'Sri Lanka (Ceylon)', + colorCharacteristics: 'Light to medium blue, good transparency, pastel tones', + inclusionFingerprints: 'Zircon halos, liquid inclusions, silk', + chemicalIndicators: 'Low Fe content, often heat-treated', + premium: 'Moderate - 10-20% premium for fine color' + }, + { + gem: 'Emerald', + origin: 'Colombia', + colorCharacteristics: 'Pure green (no blue), warm tone, high saturation', + inclusionFingerprints: 'Three-phase inclusions (liquid, gas, crystal), pyrite, calcite', + chemicalIndicators: 'Cr-colored, low Fe, specific trace elements', + premium: 'Highest - 30-50% premium, especially Muzo' + }, + { + gem: 'Emerald', + origin: 'Zambia', + colorCharacteristics: 'Bluish-green, cool tone, good clarity', + inclusionFingerprints: 'Mica plates, tremolite needles, two-phase inclusions', + chemicalIndicators: 'Fe + Cr colored, higher Fe than Colombia', + premium: 'Moderate - competitive pricing for fine stones' + }, + { + gem: 'Emerald', + origin: 'Brazil', + colorCharacteristics: 'Light to medium green, yellowish tones possible', + inclusionFingerprints: 'Mica, biotite, two-phase inclusions', + chemicalIndicators: 'Variable Cr/V/Fe content', + premium: 'Lower - 20-30% below Colombian equivalent' + }, + { + gem: 'Paraíba Tourmaline', + origin: 'Brazil (Paraíba)', + colorCharacteristics: 'Neon blue to green, electric appearance', + inclusionFingerprints: 'Liquid inclusions, growth tubes', + chemicalIndicators: 'High Cu content (0.3-1.5%), Mn present', + premium: 'Highest - significant premium for true Paraíba' + }, + { + gem: 'Paraíba Tourmaline', + origin: 'Mozambique', + colorCharacteristics: 'Similar neon colors, slightly different hue range', + inclusionFingerprints: 'Similar to Brazil but distinguishable by experts', + chemicalIndicators: 'Cu content (often lower than Brazil), trace differences', + premium: 'High - 30-50% below Brazilian stones' + }, + { + gem: 'Spinel', + origin: 'Myanmar (Burma)', + colorCharacteristics: 'Vivid red "Jedi" spinel, excellent transparency', + inclusionFingerprints: 'Octahedral negative crystals, calcite, dolomite', + chemicalIndicators: 'Low Fe, high Cr (for red)', + premium: 'High - 30-40% premium for fine reds' + }, + { + gem: 'Tanzanite', + origin: 'Tanzania (only source)', + colorCharacteristics: 'Blue-violet, trichroic, heat-treated for blue', + inclusionFingerprints: 'Graphite, apatite needles, liquid inclusions', + chemicalIndicators: 'V-colored, specific to Merelani Hills deposit', + premium: 'N/A - single origin, premium for fine color/size' + }, +]; + +const ORIGIN_NOTES: OriginNote[] = [ + { + title: 'Geographic Origin ≠ Quality', + description: 'Origin premiums exist due to rarity and historical reputation, not inherent quality. A fine Mozambique ruby can be superior to a poor Burma ruby.', + importance: 'High' + }, + { + title: 'Laboratory Reports Required', + description: 'Origin determination requires advanced laboratory testing (FTIR, LA-ICP-MS, UV-Vis spectroscopy). Visual inspection alone is insufficient.', + importance: 'High' + }, + { + title: 'Origin Certainty Levels', + description: 'Labs use terms like "determination," "indication," or "opinion" based on confidence level. Not all stones can be definitively origin-determined.', + importance: 'Medium' + }, + { + title: 'Heat Treatment Complicates Origin', + description: 'Heat treatment can alter or destroy diagnostic inclusions, making origin determination more difficult or impossible.', + importance: 'Medium' + }, +]; + +export function OriginCharacteristics() { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedGem, setSelectedGem] = useState('all'); + + const uniqueGems = ['all', ...new Set(ORIGIN_DATA.map(d => d.gem))]; + + const filteredData = ORIGIN_DATA.filter(data => { + const matchesSearch = data.origin.toLowerCase().includes(searchTerm.toLowerCase()) || + data.gem.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesGem = selectedGem === 'all' || data.gem === selectedGem; + return matchesSearch && matchesGem; + }); + + return ( +
+
+

Important Considerations

+
+ {ORIGIN_NOTES.map(note => ( +
+
+
{note.title}
+ + {note.importance} + +
+

{note.description}

+
+ ))} +
+
+ +
+

Origin Characteristics Database

+ +
+ setSearchTerm(e.target.value)} + className="flex-1 px-3 py-2 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-crystal-500" + /> + +
+ +
+ {filteredData.map((data, idx) => ( +
+
+
+
{data.gem}
+

{data.origin}

+
+ + {data.premium.split(' - ')[0]} + +
+ +
+
+ Color Characteristics: +

{data.colorCharacteristics}

+
+
+ Inclusion Fingerprints: +

{data.inclusionFingerprints}

+
+
+ Chemical Indicators: +

{data.chemicalIndicators}

+
+
+ Market Premium: +

{data.premium}

+
+
+
+ ))} +
+ + {filteredData.length === 0 && ( +

No origin data found matching your criteria.

+ )} +
+ +
+

Origin Determination Methods

+
    +
  • • FTIR Spectroscopy: Detects water content, structural features
  • +
  • • UV-Vis Spectroscopy: Analyzes color-causing elements and ratios
  • +
  • • LA-ICP-MS: Trace element "chemical fingerprinting"
  • +
  • • Microscopy: Inclusion scene examination
  • +
  • • Oxygen Isotope Analysis: Distinguishes geological environments (research tool)
  • +
+
+ +
+

⚠️ Commercial Considerations

+
    +
  • • Origin reports from reputable labs (GIA, Gübelin, SSEF, AGL) are essential
  • +
  • • Premiums vary by market - Asian markets often pay higher for certain origins
  • +
  • • Heat treatment disclosure affects origin premium (unheated commands higher prices)
  • +
  • • New deposits constantly emerge, changing market dynamics
  • +
  • • Focus on beauty and quality, not just origin certificates
  • +
+
+
+ ); +} diff --git a/src/components/advanced/ProportionAnalyzer.tsx b/src/components/advanced/ProportionAnalyzer.tsx new file mode 100644 index 0000000..4d760a0 --- /dev/null +++ b/src/components/advanced/ProportionAnalyzer.tsx @@ -0,0 +1,390 @@ +/** + * Gemstone Cut Proportion Analyzer + * Evaluate cut quality based on proportion measurements + */ + +import { useState } from 'react'; + +interface ProportionStandard { + cut: string; + gem: string; + tablePercent: { ideal: string; acceptable: string }; + depthPercent: { ideal: string; acceptable: string }; + crownAngle: { ideal: string; acceptable: string }; + pavilionAngle: { ideal: string; acceptable: string }; + notes: string; +} + +interface CutGrade { + grade: 'Excellent' | 'Very Good' | 'Good' | 'Fair' | 'Poor'; + description: string; + marketImpact: string; +} + +const PROPORTION_STANDARDS: ProportionStandard[] = [ + { + cut: 'Round Brilliant', + gem: 'Diamond', + tablePercent: { ideal: '53-58%', acceptable: '52-62%' }, + depthPercent: { ideal: '59-62.5%', acceptable: '57-64%' }, + crownAngle: { ideal: '34-35°', acceptable: '32-37°' }, + pavilionAngle: { ideal: '40.6-41.0°', acceptable: '39.5-42.0°' }, + notes: 'Tolkowsky proportions for maximum brilliance and fire' + }, + { + cut: 'Round Brilliant', + gem: 'Sapphire/Ruby', + tablePercent: { ideal: '55-65%', acceptable: '50-70%' }, + depthPercent: { ideal: '65-75%', acceptable: '60-80%' }, + crownAngle: { ideal: '35-40°', acceptable: '30-45°' }, + pavilionAngle: { ideal: '42-45°', acceptable: '40-48°' }, + notes: 'Higher RI requires steeper pavilion than diamond' + }, + { + cut: 'Emerald Cut', + gem: 'Diamond', + tablePercent: { ideal: '60-68%', acceptable: '55-75%' }, + depthPercent: { ideal: '60-68%', acceptable: '58-72%' }, + crownAngle: { ideal: '12-16°', acceptable: '10-18°' }, + pavilionAngle: { ideal: '42-44°', acceptable: '40-46°' }, + notes: 'Step cut emphasizes clarity over brilliance' + }, + { + cut: 'Oval Brilliant', + gem: 'Diamond', + tablePercent: { ideal: '53-62%', acceptable: '50-68%' }, + depthPercent: { ideal: '58-65%', acceptable: '56-70%' }, + crownAngle: { ideal: '33-36°', acceptable: '30-38°' }, + pavilionAngle: { ideal: '40-42°', acceptable: '38-44°' }, + notes: 'Length-to-width ratio: 1.35-1.50 ideal' + }, + { + cut: 'Cushion Brilliant', + gem: 'Diamond', + tablePercent: { ideal: '58-68%', acceptable: '55-75%' }, + depthPercent: { ideal: '61-68%', acceptable: '58-72%' }, + crownAngle: { ideal: '33-38°', acceptable: '30-40°' }, + pavilionAngle: { ideal: '40-42°', acceptable: '38-44°' }, + notes: 'Wide range of acceptable proportions; modern vs antique styles' + }, + { + cut: 'Cabochon', + gem: 'Corundum (Star)', + tablePercent: { ideal: 'N/A', acceptable: 'N/A' }, + depthPercent: { ideal: '50-70%', acceptable: '40-80%' }, + crownAngle: { ideal: 'Dome shape', acceptable: 'Varies' }, + pavilionAngle: { ideal: 'Flat to slight dome', acceptable: 'Varies' }, + notes: 'Height critical for star centering; base must be flat or slightly domed' + }, + { + cut: 'Oval Cabochon', + gem: 'Opal', + tablePercent: { ideal: 'N/A', acceptable: 'N/A' }, + depthPercent: { ideal: '30-50%', acceptable: '25-60%' }, + crownAngle: { ideal: 'Low to medium dome', acceptable: 'Varies' }, + pavilionAngle: { ideal: 'Flat base', acceptable: 'Flat to slight dome' }, + notes: 'Avoid high domes (magnifies cracks); thicker = more durable' + }, + { + cut: 'Emerald Cut', + gem: 'Emerald', + tablePercent: { ideal: '60-70%', acceptable: '55-75%' }, + depthPercent: { ideal: '60-70%', acceptable: '55-75%' }, + crownAngle: { ideal: '12-18°', acceptable: '10-20°' }, + pavilionAngle: { ideal: '40-44°', acceptable: '38-46°' }, + notes: 'Step cut minimizes visibility of inclusions' + }, +]; + +const CUT_GRADES: CutGrade[] = [ + { + grade: 'Excellent', + description: 'Proportions within ideal range, maximum brilliance and fire', + marketImpact: 'Premium pricing - 10-20% above average cut' + }, + { + grade: 'Very Good', + description: 'Proportions within acceptable range, excellent light performance', + marketImpact: 'Slight premium - 5-10% above average cut' + }, + { + grade: 'Good', + description: 'Proportions acceptable, good light return', + marketImpact: 'Market average pricing' + }, + { + grade: 'Fair', + description: 'Proportions outside ideal, noticeable light leakage', + marketImpact: 'Discount - 10-20% below average cut' + }, + { + grade: 'Poor', + description: 'Severe proportion issues, significant windowing or extinction', + marketImpact: 'Heavy discount - 30-50% below average cut' + }, +]; + +export function ProportionAnalyzer() { + const [selectedCut, setSelectedCut] = useState(''); + const [table, setTable] = useState(''); + const [depth, setDepth] = useState(''); + const [crownAngle, setCrownAngle] = useState(''); + const [pavilionAngle, setPavilionAngle] = useState(''); + + const selectedStandard = PROPORTION_STANDARDS.find( + s => `${s.cut} (${s.gem})` === selectedCut + ); + + const analyzeProportions = () => { + if (!selectedStandard || !table || !depth) { + return null; + } + + const tableNum = parseFloat(table); + const depthNum = parseFloat(depth); + + // Simple analysis based on table and depth being within ideal ranges + const tableIdealRange = selectedStandard.tablePercent.ideal; + const depthIdealRange = selectedStandard.depthPercent.ideal; + + const tableAcceptableRange = selectedStandard.tablePercent.acceptable; + const depthAcceptableRange = selectedStandard.depthPercent.acceptable; + + const parseRange = (range: string) => { + if (range === 'N/A') return null; + const match = range.match(/(\d+\.?\d*)-(\d+\.?\d*)%?/); + if (match) { + return { min: parseFloat(match[1]), max: parseFloat(match[2]) }; + } + return null; + }; + + const tableIdeal = parseRange(tableIdealRange); + const depthIdeal = parseRange(depthIdealRange); + const tableAcceptable = parseRange(tableAcceptableRange); + const depthAcceptable = parseRange(depthAcceptableRange); + + let score = 0; + const issues: string[] = []; + + // Table analysis + if (tableIdeal && tableNum >= tableIdeal.min && tableNum <= tableIdeal.max) { + score += 2; + } else if (tableAcceptable && tableNum >= tableAcceptable.min && tableNum <= tableAcceptable.max) { + score += 1; + issues.push('Table outside ideal range'); + } else if (tableAcceptable) { + issues.push('Table significantly outside acceptable range'); + } + + // Depth analysis + if (depthIdeal && depthNum >= depthIdeal.min && depthNum <= depthIdeal.max) { + score += 2; + } else if (depthAcceptable && depthNum >= depthAcceptable.min && depthNum <= depthAcceptable.max) { + score += 1; + issues.push('Depth outside ideal range'); + } else if (depthAcceptable) { + issues.push('Depth significantly outside acceptable range'); + } + + // Determine grade + let grade: CutGrade['grade']; + if (score >= 4) { + grade = 'Excellent'; + } else if (score === 3) { + grade = 'Very Good'; + } else if (score === 2) { + grade = 'Good'; + } else if (score === 1) { + grade = 'Fair'; + } else { + grade = 'Poor'; + } + + const gradeInfo = CUT_GRADES.find(g => g.grade === grade)!; + + return { grade: gradeInfo, issues }; + }; + + const analysis = analyzeProportions(); + + return ( +
+
+

Cut Grade Reference

+
+ {CUT_GRADES.map(grade => ( +
+
+
{grade.grade}
+ + {grade.marketImpact.split(' - ')[0]} + +
+

{grade.description}

+

+ Market Impact: {grade.marketImpact} +

+
+ ))} +
+
+ +
+

Proportion Analyzer

+ +
+
+ + +
+ + {selectedStandard && ( + <> +
+
Ideal Proportions
+
+
+ Table: {selectedStandard.tablePercent.ideal} +
+
+ Depth: {selectedStandard.depthPercent.ideal} +
+
+ Crown Angle: {selectedStandard.crownAngle.ideal} +
+
+ Pavilion Angle: {selectedStandard.pavilionAngle.ideal} +
+
+

+ Notes: {selectedStandard.notes} +

+
+ +
+
+ + setTable(e.target.value)} + className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-crystal-500" + /> +
+
+ + setDepth(e.target.value)} + className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-crystal-500" + /> +
+
+ + setCrownAngle(e.target.value)} + className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-crystal-500" + /> +
+
+ + setPavilionAngle(e.target.value)} + className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-crystal-500" + /> +
+
+ + {analysis && ( +
+
+
+ Cut Grade: {analysis.grade.grade} +
+ + {analysis.grade.marketImpact} + +
+

{analysis.grade.description}

+ {analysis.issues.length > 0 && ( +
+ Issues Detected: +
    + {analysis.issues.map((issue, idx) => ( +
  • • {issue}
  • + ))} +
+
+ )} +
+ )} + + )} +
+
+ +
+

⚠️ Important Notes

+
    +
  • • This is a simplified analysis - full cut grading requires symmetry, polish, and other factors
  • +
  • • Colored gemstones have wider acceptable ranges than diamonds
  • +
  • • Some cuts (antique cushions, Portuguese cuts) intentionally vary from modern standards
  • +
  • • Professional cut grading requires calibrated proportionscopes and gemological training
  • +
+
+
+ ); +} diff --git a/src/components/advanced/TreatmentDetection.tsx b/src/components/advanced/TreatmentDetection.tsx new file mode 100644 index 0000000..ec79792 --- /dev/null +++ b/src/components/advanced/TreatmentDetection.tsx @@ -0,0 +1,265 @@ +/** + * Treatment Detection Reference + * Visual and instrumental indicators for common gem treatments + */ + +import { useState } from 'react'; + +interface Treatment { + treatment: string; + gems: string; + visualIndicators: string; + instrumentalTests: string; + permanence: string; + disclosure: string; +} + +interface TreatmentType { + name: string; + description: string; + purpose: string; + detectability: 'Easy' | 'Moderate' | 'Difficult' | 'Very Difficult'; +} + +const TREATMENT_TYPES: TreatmentType[] = [ + { + name: 'Heat Treatment', + description: 'Controlled heating to improve color and clarity', + purpose: 'Color improvement, inclusion dissolving', + detectability: 'Moderate' + }, + { + name: 'Fracture Filling', + description: 'Glass/resin filling of surface-reaching fractures', + purpose: 'Clarity enhancement', + detectability: 'Easy' + }, + { + name: 'Dyeing', + description: 'Adding color through dyes or stains', + purpose: 'Color enhancement', + detectability: 'Easy' + }, + { + name: 'Oiling/Resin', + description: 'Filling fissures with oil or epoxy resin', + purpose: 'Clarity enhancement (especially emerald)', + detectability: 'Moderate' + }, + { + name: 'Irradiation', + description: 'Exposure to radiation to alter color', + purpose: 'Color change (blue topaz, colored diamonds)', + detectability: 'Difficult' + }, + { + name: 'Diffusion', + description: 'Surface or bulk diffusion of color-causing elements', + purpose: 'Color enhancement', + detectability: 'Moderate' + }, + { + name: 'Coating', + description: 'Thin surface coating for color/effect', + purpose: 'Color, iridescence, or protective layer', + detectability: 'Easy' + }, + { + name: 'HPHT', + description: 'High Pressure High Temperature processing', + purpose: 'Color improvement (diamonds, synthetic gems)', + detectability: 'Very Difficult' + }, +]; + +const TREATMENT_INDICATORS: Treatment[] = [ + { + treatment: 'Heat (Corundum)', + gems: 'Ruby, Sapphire', + visualIndicators: 'Silk dissolution, rounded inclusions, color zoning changes, stress fractures', + instrumentalTests: 'Microscopy (inclusion changes), spectroscopy (Fe³⁺ peaks)', + permanence: 'Permanent', + disclosure: 'Standard practice - disclosure varies by market' + }, + { + treatment: 'Beryllium Diffusion', + gems: 'Sapphire (orange padparadscha)', + visualIndicators: 'Color concentration at facet junctions, uneven color', + instrumentalTests: 'EDXRF (beryllium detection), FTIR, UV-Vis', + permanence: 'Permanent', + disclosure: 'Required - significantly affects value' + }, + { + treatment: 'Fracture Filling (Glass)', + gems: 'Ruby, Diamond', + visualIndicators: 'Flash effect, color flashes in fissures, surface residue', + instrumentalTests: 'Microscopy (gas bubbles, flow structures)', + permanence: 'Not permanent - avoid heat/ultrasonic', + disclosure: 'Required - major value impact' + }, + { + treatment: 'Oiling (Emerald)', + gems: 'Emerald', + visualIndicators: 'Oil in fissures (UV fluorescence), surface residue', + instrumentalTests: 'UV lamp (yellow-green fluorescence), FTIR', + permanence: 'Not permanent - oil can dry out', + disclosure: 'Standard practice - grade depends on extent' + }, + { + treatment: 'Irradiation + Heat', + gems: 'Blue Topaz, Fancy Diamonds', + visualIndicators: 'Uniform color, specific color types (Swiss/London blue)', + instrumentalTests: 'Spectroscopy (radiation-induced defects), color centers', + permanence: 'Permanent (stable)', + disclosure: 'Required for blue topaz' + }, + { + treatment: 'Dyeing', + gems: 'Jade, Chalcedony, Pearls', + visualIndicators: 'Color in pores/fractures, unnatural colors, concentrations', + instrumentalTests: 'Microscopy (dye concentrations), Chelsea filter, spectroscopy', + permanence: 'Not permanent - can fade', + disclosure: 'Required - major value impact' + }, + { + treatment: 'Coating', + gems: 'Topaz (Mystic), Diamond', + visualIndicators: 'Wear at facet edges, scratches revealing base color', + instrumentalTests: 'Microscopy (edge chipping), spectroscopy', + permanence: 'Not permanent - wears off', + disclosure: 'Required - temporary treatment' + }, + { + treatment: 'Lattice Diffusion', + gems: 'Sapphire (full color)', + visualIndicators: 'Even color distribution, possible surface concentration', + instrumentalTests: 'EDXRF (Ti, Fe detection at surface), FTIR', + permanence: 'Permanent', + disclosure: 'Required - affects value' + }, + { + treatment: 'Clarity Enhancement (Diamonds)', + gems: 'Diamond', + visualIndicators: 'Flash effect in fractures, laser drill holes', + instrumentalTests: 'Microscopy (drill holes, filler in fractures)', + permanence: 'Varies - laser drilling permanent, filling not', + disclosure: 'Required - significant value reduction' + }, +]; + +export function TreatmentDetection() { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedGem, setSelectedGem] = useState('all'); + + const uniqueGems = ['all', ...new Set(TREATMENT_INDICATORS.flatMap(t => t.gems.split(', ')))]; + + const filteredTreatments = TREATMENT_INDICATORS.filter(treatment => { + const matchesSearch = treatment.treatment.toLowerCase().includes(searchTerm.toLowerCase()) || + treatment.gems.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesGem = selectedGem === 'all' || treatment.gems.includes(selectedGem); + return matchesSearch && matchesGem; + }); + + return ( +
+
+

Common Treatment Types

+
+ {TREATMENT_TYPES.map(type => ( +
+
+
{type.name}
+ + {type.detectability} + +
+

{type.description}

+

+ Purpose: {type.purpose} +

+
+ ))} +
+
+ +
+

Detection Indicators

+ +
+ setSearchTerm(e.target.value)} + className="flex-1 px-3 py-2 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-crystal-500" + /> + +
+ +
+ {filteredTreatments.map((treatment, idx) => ( +
+
+
+
{treatment.treatment}
+

{treatment.gems}

+
+ + {treatment.permanence.startsWith('Permanent') ? 'Permanent' : 'Temporary'} + +
+ +
+
+ Visual Indicators: +

{treatment.visualIndicators}

+
+
+ Instrumental Tests: +

{treatment.instrumentalTests}

+
+
+ Disclosure: +

{treatment.disclosure}

+
+
+
+ ))} +
+ + {filteredTreatments.length === 0 && ( +

No treatments found matching your criteria.

+ )} +
+ +
+

⚠️ Important Disclosure Requirements

+
    +
  • • All treatments must be disclosed to buyers in most markets
  • +
  • • Undisclosed treatments can result in legal liability
  • +
  • • Some treatments (fracture filling, dyeing) drastically reduce value
  • +
  • • Standard treatments (heat in corundum) may not require disclosure in some markets
  • +
  • • When in doubt, always disclose - transparency builds trust
  • +
+
+
+ ); +} diff --git a/src/components/advanced/index.ts b/src/components/advanced/index.ts new file mode 100644 index 0000000..f40a293 --- /dev/null +++ b/src/components/advanced/index.ts @@ -0,0 +1,7 @@ +/** + * Advanced analysis tools exports. + */ + +export { TreatmentDetection } from './TreatmentDetection'; +export { OriginCharacteristics } from './OriginCharacteristics'; +export { ProportionAnalyzer } from './ProportionAnalyzer'; diff --git a/src/components/auth/ProtectedAdvanced.tsx b/src/components/auth/ProtectedAdvanced.tsx new file mode 100644 index 0000000..2fb8271 --- /dev/null +++ b/src/components/auth/ProtectedAdvanced.tsx @@ -0,0 +1,24 @@ +/** + * Protected Advanced Analysis tools wrapper. + */ + +import { LockGate } from './LockGate'; +import { AdvancedTools } from '../advanced/AdvancedTools'; + +export function ProtectedAdvanced() { + return ( + +
+
+
+

Advanced Analysis

+

+ Advanced gemmological analysis tools including treatment detection reference, origin determination characteristics, and gemstone cut proportion analyzer. +

+
+ +
+
+
+ ); +} diff --git a/src/components/auth/ProtectedConversions.tsx b/src/components/auth/ProtectedConversions.tsx new file mode 100644 index 0000000..e2a23e3 --- /dev/null +++ b/src/components/auth/ProtectedConversions.tsx @@ -0,0 +1,24 @@ +/** + * Protected Conversions tools component wrapper. + */ + +import { LockGate } from './LockGate'; +import { ConversionTools } from '../calculator'; + +export function ProtectedConversions() { + return ( + +
+
+
+

Unit Conversions

+

+ Convert between carats, grams, millimeters, inches, Celsius, Fahrenheit, and calculate price per carat with markup. +

+
+ +
+
+
+ ); +} diff --git a/src/components/auth/ProtectedIdentification.tsx b/src/components/auth/ProtectedIdentification.tsx new file mode 100644 index 0000000..a3c837b --- /dev/null +++ b/src/components/auth/ProtectedIdentification.tsx @@ -0,0 +1,24 @@ +/** + * Protected Gem Identification tools wrapper. + */ + +import { LockGate } from './LockGate'; +import { IdentificationTools } from '../identification/IdentificationTools'; + +export function ProtectedIdentification() { + return ( + +
+
+
+

Gem Identification

+

+ Comprehensive identification tools including side-by-side gem comparison, Mohs hardness reference, and cleavage/fracture patterns. +

+
+ +
+
+
+ ); +} diff --git a/src/components/auth/ProtectedLab.tsx b/src/components/auth/ProtectedLab.tsx new file mode 100644 index 0000000..d30876a --- /dev/null +++ b/src/components/auth/ProtectedLab.tsx @@ -0,0 +1,24 @@ +/** + * Protected Lab tools wrapper. + */ + +import { LockGate } from './LockGate'; +import { LabTools } from '../lab/LabTools'; + +export function ProtectedLab() { + return ( + +
+
+
+

Lab Equipment

+

+ Reference tools for gemological lab equipment including Chelsea filter reactions, spectroscope absorption lines, and heavy liquid separation. +

+
+ +
+
+
+ ); +} diff --git a/src/components/auth/ProtectedMeasurement.tsx b/src/components/auth/ProtectedMeasurement.tsx new file mode 100644 index 0000000..ef650b7 --- /dev/null +++ b/src/components/auth/ProtectedMeasurement.tsx @@ -0,0 +1,28 @@ +/** + * Protected Measurement tools component wrapper. + * Wraps the measurement tools interface with LockGate authentication. + */ + +import { LockGate } from './LockGate'; +import { MeasurementTools } from '../calculator'; + +export function ProtectedMeasurement() { + return ( + +
+
+ {/* Header */} +
+

Measurement & Calculation

+

+ Essential measurement and calculation tools for gem identification, including SG, RI, birefringence, dispersion, and carat estimation. +

+
+ + {/* Tools */} + +
+
+
+ ); +} diff --git a/src/components/auth/ProtectedOptical.tsx b/src/components/auth/ProtectedOptical.tsx new file mode 100644 index 0000000..59a15e7 --- /dev/null +++ b/src/components/auth/ProtectedOptical.tsx @@ -0,0 +1,24 @@ +/** + * Protected Optical tools wrapper. + */ + +import { LockGate } from './LockGate'; +import { OpticalTools } from '../optical/OpticalTools'; + +export function ProtectedOptical() { + return ( + +
+
+
+

Optical Properties

+

+ Interactive tools for optical testing including dichroscope interpretation, polariscope reactions, refractometer reading practice, and pleochroism reference. +

+
+ +
+
+
+ ); +} diff --git a/src/components/auth/ProtectedTools.tsx b/src/components/auth/ProtectedTools.tsx index 5a611de..b427187 100644 --- a/src/components/auth/ProtectedTools.tsx +++ b/src/components/auth/ProtectedTools.tsx @@ -9,16 +9,56 @@ import { IconBox } from '../ui/IconBox'; const tools = [ { - title: 'Calculators', - description: 'Specific gravity, birefringence, critical angle, and unit conversions for gemmologists.', - href: '/tools/calculator', + title: 'Measurement & Calculation', + description: 'SG, RI, birefringence, critical angle, dispersion, carat estimation, and density calculations.', + href: '/tools/measurement', icon: 'calculator', variant: 'emerald' as const, }, + { + title: 'Unit Conversions', + description: 'Convert between carats, grams, millimeters, inches, Celsius, Fahrenheit, and calculate price per carat.', + href: '/tools/conversions', + icon: 'arrows', + variant: 'sapphire' as const, + }, + { + title: 'Optical Properties', + description: 'Dichroscope results, polariscope interpretation, refractometer simulator, and pleochroism reference.', + href: '/tools/optical', + icon: 'eye', + variant: 'amethyst' as const, + }, + { + title: 'Lab Equipment', + description: 'Chelsea filter simulator, spectroscope line calculator, and heavy liquid SG reference.', + href: '/tools/lab', + icon: 'beaker', + variant: 'ruby' as const, + }, + { + title: 'Gem Identification', + description: 'Compare gems side-by-side, hardness reference, and fracture/cleavage identification guide.', + href: '/tools/identification', + icon: 'search', + variant: 'topaz' as const, + }, + { + title: 'Advanced Analysis', + description: 'Treatment detection checklist, origin characteristics guide, and proportion analyzer.', + href: '/tools/advanced', + icon: 'chart', + variant: 'crystal' as const, + }, ]; const iconPaths: Record = { calculator: 'M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z', + arrows: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4', + eye: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z', + beaker: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z', + search: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z', + chart: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z', }; export function ProtectedTools() { @@ -59,32 +99,12 @@ export function ProtectedTools() { ))} - {/* Coming Soon */} -
-

More Tools Coming Soon

+ {/* Info */} +
+

Professional Gemmological Tools

- We're working on additional tools including: + Each tool category contains multiple calculators, reference guides, and interactive tools designed for FGA students and professional gemmologists. All tools are optimized for accuracy and ease of use in lab and field environments.

-
    -
  • - - - - Fluorescence reference charts -
  • -
  • - - - - Absorption spectrum viewer -
  • -
  • - - - - Inclusions reference database -
  • -
diff --git a/src/components/calculator/ConversionTools.tsx b/src/components/calculator/ConversionTools.tsx new file mode 100644 index 0000000..5158961 --- /dev/null +++ b/src/components/calculator/ConversionTools.tsx @@ -0,0 +1,101 @@ +/** + * Unit Conversion tools container. + * Shows all conversion tools in expandable cards. + */ + +import { useState } from 'react'; +import { WeightConverter } from './WeightConverter'; +import { LengthConverter } from './LengthConverter'; +import { TemperatureConverter } from './TemperatureConverter'; +import { PricePerCaratConverter } from './PricePerCaratConverter'; +import { CalculatorCard } from './CalculatorCard'; + +type ConversionId = 'weight' | 'length' | 'temperature' | 'price'; + +interface ConversionConfig { + id: ConversionId; + label: string; + iconPath: string; + description: string; + component: React.ReactNode; +} + +const ICON_PATHS: Record = { + weight: 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z', + ruler: 'M6 20h12M6 4h12m-6 0v16', + thermometer: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z', + currency: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z', +}; + +const CONVERSIONS: ConversionConfig[] = [ + { + id: 'weight', + label: 'Weight Converter', + iconPath: ICON_PATHS.weight, + description: 'Convert ct/g/mg', + component: , + }, + { + id: 'length', + label: 'Length Converter', + iconPath: ICON_PATHS.ruler, + description: 'Convert mm/inches', + component: , + }, + { + id: 'temperature', + label: 'Temperature Converter', + iconPath: ICON_PATHS.thermometer, + description: 'Convert °C/°F', + component: , + }, + { + id: 'price', + label: 'Price Per Carat', + iconPath: ICON_PATHS.currency, + description: 'Calculate total price and markup', + component: , + }, +]; + +interface ConversionToolsProps { + defaultExpanded?: ConversionId | null; +} + +export function ConversionTools({ defaultExpanded = 'weight' }: ConversionToolsProps) { + const [expandedCalc, setExpandedCalc] = useState(defaultExpanded); + + const handleToggle = (id: ConversionId) => { + setExpandedCalc(prev => (prev === id ? null : id)); + }; + + return ( +
+
+

+ Unit conversion tools for weight, length, temperature, and pricing calculations. +

+
+ +
+ {CONVERSIONS.map(calc => ( + handleToggle(calc.id)} + > + {calc.component} + + ))} +
+ +

+ Tip: Click on any converter header to expand or collapse it. +

+
+ ); +} diff --git a/src/components/calculator/DensityEstimator.tsx b/src/components/calculator/DensityEstimator.tsx new file mode 100644 index 0000000..f9e6761 --- /dev/null +++ b/src/components/calculator/DensityEstimator.tsx @@ -0,0 +1,246 @@ +/** + * Density Estimator + * Alternative SG calculation for irregular shapes using volume estimation + */ + +import { useState } from 'react'; +import { ResultCard } from './ResultCard'; +import { ValidationMessage, validateNumber } from './ValidationMessage'; + +const VOLUME_METHODS = [ + { id: 'displacement', label: 'Water Displacement', formula: 'Volume from water displacement' }, + { id: 'geometric', label: 'Geometric Approximation', formula: 'L × W × H × shape factor' }, + { id: 'direct', label: 'Direct Volume Input', formula: 'Enter measured volume' }, +]; + +const SHAPE_FACTORS = [ + { id: 'sphere', label: 'Sphere', factor: 0.5236 }, // (4/3)π/8 + { id: 'ellipsoid', label: 'Ellipsoid', factor: 0.5236 }, + { id: 'cube', label: 'Cube/Block', factor: 1.0 }, + { id: 'irregular', label: 'Irregular (est.)', factor: 0.65 }, +]; + +export function DensityEstimator() { + const [method, setMethod] = useState('displacement'); + const [weight, setWeight] = useState(''); + const [volume, setVolume] = useState(''); + const [length, setLength] = useState(''); + const [width, setWidth] = useState(''); + const [height, setHeight] = useState(''); + const [shapeFactor, setShapeFactor] = useState('0.65'); + + const weightNum = parseFloat(weight); + const volumeNum = parseFloat(volume); + const lengthNum = parseFloat(length); + const widthNum = parseFloat(width); + const heightNum = parseFloat(height); + const factorNum = parseFloat(shapeFactor); + + const isValidWeight = validateNumber(weightNum, 0.01, 1000); + + let calculatedVolume: number | null = null; + let density: number | null = null; + + if (method === 'direct' && validateNumber(volumeNum, 0.01, 1000)) { + calculatedVolume = volumeNum; + } else if (method === 'displacement' && validateNumber(volumeNum, 0.01, 1000)) { + calculatedVolume = volumeNum; + } else if ( + method === 'geometric' && + validateNumber(lengthNum, 0.01, 100) && + validateNumber(widthNum, 0.01, 100) && + validateNumber(heightNum, 0.01, 100) && + validateNumber(factorNum, 0.01, 2) + ) { + calculatedVolume = lengthNum * widthNum * heightNum * factorNum; + } + + if (isValidWeight && calculatedVolume !== null && calculatedVolume > 0) { + density = weightNum / calculatedVolume; + } + + return ( +
+
+

+ Calculate density (SG) for irregular or fragile stones using volume estimation methods. +

+

+ Formula: Density = Weight ÷ Volume +

+
+ +
+ + +

+ {VOLUME_METHODS.find((m) => m.id === method)?.formula} +

+
+ +
+ + setWeight(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 3.52" + /> + validateNumber(v, 0.01, 1000)} /> +
+ + {method === 'direct' && ( +
+ + setVolume(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 1.0" + /> + validateNumber(v, 0.01, 1000)} /> +
+ )} + + {method === 'displacement' && ( +
+ + setVolume(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 1.0" + /> +

+ Submerge stone in graduated cylinder and measure volume change +

+ validateNumber(v, 0.01, 1000)} /> +
+ )} + + {method === 'geometric' && ( + <> +
+
+ + setLength(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 6.5" + /> +
+
+ + setWidth(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 6.5" + /> +
+
+ + setHeight(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 4.0" + /> +
+
+ +
+ + +

+ Volume = L × W × H × {factorNum} (in mm³, converted to cm³) +

+
+ + {calculatedVolume !== null && ( +
+ Calculated Volume: {calculatedVolume.toFixed(3)} cm³ +
+ )} + + )} + + {density !== null && ( + +

+ Note: This is an estimate. Actual SG from hydrostatic weighing is more accurate. +

+
+ )} + +
+

When to Use This Tool

+
    +
  • • Fragile or porous stones that can't be submerged
  • +
  • • Irregular rough specimens without standard shapes
  • +
  • • Quick field estimates when lab equipment isn't available
  • +
  • • Large specimens too big for standard SG equipment
  • +
+
+
+ ); +} diff --git a/src/components/calculator/DispersionCalculator.tsx b/src/components/calculator/DispersionCalculator.tsx new file mode 100644 index 0000000..9fe64c3 --- /dev/null +++ b/src/components/calculator/DispersionCalculator.tsx @@ -0,0 +1,140 @@ +/** + * Dispersion Calculator + * Calculate fire/dispersion from RI at different wavelengths + */ + +import { useState } from 'react'; +import { ResultCard } from './ResultCard'; +import { ValidationMessage, validateRI } from './ValidationMessage'; + +const COMMON_GEMS_DISPERSION = [ + { name: 'Diamond', dispersion: 0.044, ri: 2.417 }, + { name: 'Zircon', dispersion: 0.039, ri: 1.960 }, + { name: 'Sphene', dispersion: 0.051, ri: 1.900 }, + { name: 'Demantoid Garnet', dispersion: 0.057, ri: 1.888 }, + { name: 'Ruby/Sapphire', dispersion: 0.018, ri: 1.762 }, + { name: 'Spinel', dispersion: 0.020, ri: 1.718 }, + { name: 'Topaz', dispersion: 0.014, ri: 1.619 }, + { name: 'Tourmaline', dispersion: 0.017, ri: 1.624 }, + { name: 'Quartz', dispersion: 0.013, ri: 1.544 }, + { name: 'Emerald', dispersion: 0.014, ri: 1.577 }, +]; + +export function DispersionCalculator() { + const [riRed, setRiRed] = useState(''); + const [riViolet, setRiViolet] = useState(''); + + const riRedNum = parseFloat(riRed); + const riVioletNum = parseFloat(riViolet); + + const isValidRed = validateRI(riRedNum); + const isValidViolet = validateRI(riVioletNum); + + let dispersion: number | null = null; + let category = ''; + + if (isValidRed && isValidViolet && riVioletNum > riRedNum) { + dispersion = riVioletNum - riRedNum; + + if (dispersion < 0.020) category = 'Low'; + else if (dispersion < 0.030) category = 'Moderate'; + else if (dispersion < 0.040) category = 'High'; + else category = 'Very High'; + } + + return ( +
+
+

+ Enter the refractive index at red (C-line, 656nm) and violet (F-line, 486nm) wavelengths to calculate dispersion. +

+

+ Formula: Dispersion = RI(violet) − RI(red) +

+
+ +
+
+ + setRiRed(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 2.407" + /> + +
+ +
+ + setRiViolet(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 2.451" + /> + +
+
+ + {dispersion !== null && ( + +

+ Fire Potential: {category === 'Very High' || category === 'High' ? 'Excellent' : category === 'Moderate' ? 'Good' : 'Low'} spectral color separation +

+
+ )} + +
+

Common Gem Dispersion Reference

+
+ + + + + + + + + + + {COMMON_GEMS_DISPERSION.map((gem) => ( + + + + + + + ))} + +
GemDispersionRI (avg)Fire
{gem.name}{gem.dispersion.toFixed(3)}{gem.ri.toFixed(3)} + {gem.dispersion >= 0.040 ? 'Very High' : gem.dispersion >= 0.030 ? 'High' : gem.dispersion >= 0.020 ? 'Moderate' : 'Low'} +
+
+
+ +
+

Why Dispersion Matters

+

+ Dispersion measures how much a gem splits white light into spectral colors. Higher dispersion creates more "fire" — the rainbow flashes seen in a well-cut stone. Diamond's high dispersion (0.044) is why it shows exceptional fire, while quartz's low dispersion (0.013) produces minimal color flashes. +

+
+
+ ); +} diff --git a/src/components/calculator/MeasurementTools.tsx b/src/components/calculator/MeasurementTools.tsx new file mode 100644 index 0000000..841874e --- /dev/null +++ b/src/components/calculator/MeasurementTools.tsx @@ -0,0 +1,138 @@ +/** + * Measurement & Calculation tools container. + * Shows all measurement tools in expandable cards. + */ + +import { useState } from 'react'; +import { SGCalculator } from './SGCalculator'; +import { RICalculator } from './RICalculator'; +import { BirefringenceCalc } from './BirefringenceCalc'; +import { CriticalAngleCalc } from './CriticalAngleCalc'; +import { CaratEstimator } from './CaratEstimator'; +import { DispersionCalculator } from './DispersionCalculator'; +import { DensityEstimator } from './DensityEstimator'; +import { CalculatorCard } from './CalculatorCard'; + +type MeasurementId = + | 'sg' + | 'ri-lookup' + | 'birefringence' + | 'critical-angle' + | 'carat-estimate' + | 'dispersion' + | 'density'; + +interface MeasurementConfig { + id: MeasurementId; + label: string; + iconPath: string; + description: string; + component: React.ReactNode; +} + +const ICON_PATHS: Record = { + scale: 'M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3', + search: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z', + layers: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z', + 'arrow-right': 'M13 7l5 5m0 0l-5 5m5-5H6', + cube: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4', + sparkles: 'M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z', + beaker: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z', +}; + +const MEASUREMENTS: MeasurementConfig[] = [ + { + id: 'sg', + label: 'Specific Gravity', + iconPath: ICON_PATHS.scale, + description: 'Calculate SG from hydrostatic weighing', + component: , + }, + { + id: 'ri-lookup', + label: 'RI Lookup', + iconPath: ICON_PATHS.search, + description: 'Find gems by refractive index', + component: , + }, + { + id: 'birefringence', + label: 'Birefringence', + iconPath: ICON_PATHS.layers, + description: 'Calculate birefringence from RI values', + component: , + }, + { + id: 'critical-angle', + label: 'Critical Angle', + iconPath: ICON_PATHS['arrow-right'], + description: 'Calculate TIR critical angle', + component: , + }, + { + id: 'dispersion', + label: 'Dispersion', + iconPath: ICON_PATHS.sparkles, + description: 'Calculate fire from RI at different wavelengths', + component: , + }, + { + id: 'carat-estimate', + label: 'Carat Estimate', + iconPath: ICON_PATHS.cube, + description: 'Estimate weight from dimensions', + component: , + }, + { + id: 'density', + label: 'Density Estimator', + iconPath: ICON_PATHS.beaker, + description: 'Alternative SG for irregular shapes', + component: , + }, +]; + +interface MeasurementToolsProps { + defaultExpanded?: MeasurementId | null; +} + +export function MeasurementTools({ defaultExpanded = 'sg' }: MeasurementToolsProps) { + const [expandedCalc, setExpandedCalc] = useState(defaultExpanded); + + const handleToggle = (id: MeasurementId) => { + setExpandedCalc(prev => (prev === id ? null : id)); + }; + + return ( +
+ {/* Introduction */} +
+

+ Select a tool below to expand it. These measurement and calculation tools help with gem identification, grading, and analysis. +

+
+ + {/* Tools grid */} +
+ {MEASUREMENTS.map(calc => ( + handleToggle(calc.id)} + > + {calc.component} + + ))} +
+ + {/* Tip */} +

+ Tip: Click on any tool header to expand or collapse it. +

+
+ ); +} diff --git a/src/components/calculator/PricePerCaratConverter.tsx b/src/components/calculator/PricePerCaratConverter.tsx new file mode 100644 index 0000000..4b80787 --- /dev/null +++ b/src/components/calculator/PricePerCaratConverter.tsx @@ -0,0 +1,177 @@ +/** + * Price Per Carat Converter + * Calculate total price from price per carat and vice versa + */ + +import { useState } from 'react'; +import { ResultCard } from './ResultCard'; +import { ValidationMessage, validateNumber } from './ValidationMessage'; + +export function PricePerCaratConverter() { + const [totalPrice, setTotalPrice] = useState(''); + const [pricePerCarat, setPricePerCarat] = useState(''); + const [carats, setCarats] = useState(''); + const [markup, setMarkup] = useState('0'); + + const totalNum = parseFloat(totalPrice); + const ppcNum = parseFloat(pricePerCarat); + const caratsNum = parseFloat(carats); + const markupNum = parseFloat(markup); + + const isValidTotal = validateNumber(totalNum, 0, 1000000); + const isValidPpc = validateNumber(ppcNum, 0, 1000000); + const isValidCarats = validateNumber(caratsNum, 0.01, 1000); + const isValidMarkup = validateNumber(markupNum, -100, 1000); + + // Calculate missing values + let calculatedTotal: number | null = null; + let calculatedPpc: number | null = null; + let calculatedCarats: number | null = null; + let retailPrice: number | null = null; + + if (isValidPpc && isValidCarats) { + calculatedTotal = ppcNum * caratsNum; + if (isValidMarkup) { + retailPrice = calculatedTotal * (1 + markupNum / 100); + } + } else if (isValidTotal && isValidCarats && caratsNum > 0) { + calculatedPpc = totalNum / caratsNum; + if (isValidMarkup) { + retailPrice = totalNum * (1 + markupNum / 100); + } + } else if (isValidTotal && isValidPpc && ppcNum > 0) { + calculatedCarats = totalNum / ppcNum; + } + + return ( +
+
+

+ Convert between total price and price per carat. Optionally add markup percentage for retail calculations. +

+

+ Formula: Total Price = Price per Carat × Carats +

+
+ +
+
+ + setTotalPrice(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 5000" + /> + validateNumber(v, 0, 1000000)} /> +
+ +
+ + setPricePerCarat(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 10000" + /> + validateNumber(v, 0, 1000000)} /> +
+ +
+ + setCarats(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 0.50" + /> + validateNumber(v, 0.01, 1000)} /> +
+
+ +
+ + setMarkup(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 30 (or -10 for discount)" + /> +

+ Positive for retail markup, negative for discount +

+ validateNumber(v, -100, 1000)} /> +
+ + {calculatedTotal !== null && ( + + )} + + {calculatedPpc !== null && ( + + )} + + {calculatedCarats !== null && ( + + )} + + {retailPrice !== null && markupNum !== 0 && ( +
+
+ {markupNum > 0 ? 'Retail Price' : 'Discounted Price'} ({markupNum > 0 ? '+' : ''}{markupNum}%) +
+
+ ${retailPrice.toFixed(2)} +
+

+ {markupNum > 0 ? 'Markup' : 'Discount'}: ${Math.abs(retailPrice - (calculatedTotal || 0)).toFixed(2)} +

+
+ )} + +
+

Pricing Examples

+
    +
  • • 1.0ct diamond at $10,000/ct = $10,000 total
  • +
  • • $5,000 sapphire ÷ 0.50ct = $10,000/ct
  • +
  • • 30% retail markup on $5,000 = $6,500 retail
  • +
  • • 10% trade discount on $5,000 = $4,500 net
  • +
+
+
+ ); +} diff --git a/src/components/calculator/index.ts b/src/components/calculator/index.ts index 4f6b40a..8f9f43f 100644 --- a/src/components/calculator/index.ts +++ b/src/components/calculator/index.ts @@ -3,6 +3,8 @@ */ export { Calculator } from './Calculator'; +export { MeasurementTools } from './MeasurementTools'; +export { ConversionTools } from './ConversionTools'; export { SGCalculator } from './SGCalculator'; export { BirefringenceCalc } from './BirefringenceCalc'; export { CriticalAngleCalc } from './CriticalAngleCalc'; @@ -11,6 +13,9 @@ export { LengthConverter } from './LengthConverter'; export { TemperatureConverter } from './TemperatureConverter'; export { CaratEstimator } from './CaratEstimator'; export { RICalculator } from './RICalculator'; +export { DispersionCalculator } from './DispersionCalculator'; +export { DensityEstimator } from './DensityEstimator'; +export { PricePerCaratConverter } from './PricePerCaratConverter'; // Validation export { diff --git a/src/components/identification/FractureCleavageGuide.tsx b/src/components/identification/FractureCleavageGuide.tsx new file mode 100644 index 0000000..6a048f9 --- /dev/null +++ b/src/components/identification/FractureCleavageGuide.tsx @@ -0,0 +1,196 @@ +/** + * Fracture and Cleavage Visual Reference + * Guide to cleavage directions and fracture patterns + */ + +import { useState } from 'react'; + +interface CleavageData { + gem: string; + cleavage: string; + directions: string; + quality: string; + fracture: string; + notes: string; +} + +interface FractureType { + type: string; + description: string; + examples: string; + appearance: string; +} + +const CLEAVAGE_DATA: CleavageData[] = [ + { gem: 'Diamond', cleavage: 'Perfect', directions: '4 directions (octahedral)', quality: 'Excellent', fracture: 'Conchoidal', notes: 'Cleaves easily - avoid sharp blows' }, + { gem: 'Topaz', cleavage: 'Perfect', directions: '1 direction (basal)', quality: 'Excellent', fracture: 'Conchoidal to uneven', notes: 'Very easy to cleave - handle carefully' }, + { gem: 'Kunzite', cleavage: 'Perfect', directions: '2 directions (prismatic)', quality: 'Excellent', fracture: 'Uneven', notes: 'Extremely fragile - not for rings' }, + { gem: 'Feldspar (Moonstone)', cleavage: 'Perfect', directions: '2 directions (90°)', quality: 'Good', fracture: 'Uneven to conchoidal', notes: 'Two perfect cleavages at right angles' }, + { gem: 'Tanzanite', cleavage: 'Perfect', directions: '1 direction (basal)', quality: 'Good', fracture: 'Uneven to conchoidal', notes: 'Very fragile - protective settings required' }, + { gem: 'Emerald', cleavage: 'Indistinct', directions: '1 direction (basal)', quality: 'Poor', fracture: 'Conchoidal to uneven', notes: 'Usually heavily included - brittle' }, + { gem: 'Aquamarine', cleavage: 'Indistinct', directions: '1 direction (basal)', quality: 'Poor', fracture: 'Conchoidal to uneven', notes: 'Better toughness than emerald' }, + { gem: 'Quartz', cleavage: 'None', directions: 'None', quality: 'N/A', fracture: 'Conchoidal', notes: 'Excellent toughness - very durable' }, + { gem: 'Garnet', cleavage: 'None', directions: 'None', quality: 'N/A', fracture: 'Conchoidal to uneven', notes: 'No cleavage - good durability' }, + { gem: 'Spinel', cleavage: 'None', directions: 'None', quality: 'N/A', fracture: 'Conchoidal', notes: 'Excellent toughness - very durable' }, + { gem: 'Ruby', cleavage: 'None', directions: 'None (parting sometimes)', quality: 'N/A', fracture: 'Conchoidal to uneven', notes: 'Excellent toughness - very durable' }, + { gem: 'Sapphire', cleavage: 'None', directions: 'None (parting sometimes)', quality: 'N/A', fracture: 'Conchoidal to uneven', notes: 'Excellent toughness - very durable' }, + { gem: 'Chrysoberyl', cleavage: 'Distinct', directions: '3 directions', quality: 'Fair', fracture: 'Conchoidal to uneven', notes: 'Good toughness despite cleavage' }, + { gem: 'Tourmaline', cleavage: 'Indistinct', directions: 'None to indistinct', quality: 'Poor', fracture: 'Uneven to conchoidal', notes: 'Can be brittle - avoid sharp blows' }, + { gem: 'Peridot', cleavage: 'Distinct', directions: '2 directions', quality: 'Fair', fracture: 'Conchoidal', notes: 'Moderate toughness' }, +]; + +const FRACTURE_TYPES: FractureType[] = [ + { + type: 'Conchoidal', + description: 'Smooth, curved surfaces like broken glass', + examples: 'Quartz, obsidian, most amorphous materials', + appearance: 'Curved, shell-like fracture with concentric ridges' + }, + { + type: 'Uneven', + description: 'Rough, irregular surfaces', + examples: 'Most crystalline gems, corundum, garnet', + appearance: 'Irregular, non-smooth fracture surfaces' + }, + { + type: 'Splintery', + description: 'Fractures into elongated splinters', + examples: 'Fibrous minerals, some tourmalines', + appearance: 'Long, thin, needle-like fragments' + }, + { + type: 'Hackly', + description: 'Jagged, sharp points', + examples: 'Native metals, some sulfides', + appearance: 'Sharp, irregular, jagged edges' + }, +]; + +export function FractureCleavageGuide() { + const [searchTerm, setSearchTerm] = useState(''); + const [filterCleavage, setFilterCleavage] = useState('all'); + + const filteredGems = CLEAVAGE_DATA.filter(gem => { + const matchesSearch = gem.gem.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesFilter = filterCleavage === 'all' || gem.cleavage === filterCleavage; + return matchesSearch && matchesFilter; + }); + + return ( +
+
+

Fracture Types

+
+ {FRACTURE_TYPES.map(fracture => ( +
+
{fracture.type}
+

{fracture.description}

+

+ Appearance: {fracture.appearance} +

+

+ Examples: {fracture.examples} +

+
+ ))} +
+
+ +
+

Cleavage Reference

+ +
+ setSearchTerm(e.target.value)} + className="flex-1 px-3 py-2 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-crystal-500" + /> + +
+ +
+ + + + + + + + + + + + {filteredGems.map(gem => ( + + + + + + + + ))} + +
GemstoneCleavageDirectionsFractureNotes
{gem.gem} + + {gem.cleavage} + + {gem.directions}{gem.fracture}{gem.notes}
+
+ + {filteredGems.length === 0 && ( +

No gemstones found matching your criteria.

+ )} +
+ +
+
+

Understanding Cleavage

+
    +
  • • Perfect: Splits easily along smooth planes
  • +
  • • Distinct: Visible planes, moderate ease of splitting
  • +
  • • Indistinct: Difficult to observe/achieve
  • +
  • • None: No cleavage planes present
  • +
+
+ +
+

Cleavage vs Fracture

+
    +
  • • Cleavage: Breaks along crystallographic planes
  • +
  • • Fracture: Random breaking not related to structure
  • +
  • • Cleavage surfaces are smooth and planar
  • +
  • • Fracture surfaces are irregular and curved
  • +
+
+
+ +
+

Handling Gems with Perfect Cleavage

+
    +
  • • Avoid ultrasonic cleaners (vibrations can cause cleavage)
  • +
  • • Use bezel or protective settings instead of prong settings
  • +
  • • Avoid steam cleaning for gems with cleavage
  • +
  • • Handle with extreme care during setting and sizing
  • +
  • • Not recommended for rings (high-impact jewelry)
  • +
+
+
+ ); +} diff --git a/src/components/identification/GemComparison.tsx b/src/components/identification/GemComparison.tsx new file mode 100644 index 0000000..3f9fe0d --- /dev/null +++ b/src/components/identification/GemComparison.tsx @@ -0,0 +1,187 @@ +/** + * Gem Comparison Tool + * Side-by-side property comparison (2-4 gems) + */ + +import { useState } from 'react'; + +interface GemData { + name: string; + ri_low: number; + ri_high: number; + sg: number; + hardness: number; + dispersion: number; + system: string; + pleochroism: string; +} + +const GEM_DATABASE: GemData[] = [ + { name: 'Diamond', ri_low: 2.417, ri_high: 2.417, sg: 3.52, hardness: 10.0, dispersion: 0.044, system: 'Cubic', pleochroism: 'None' }, + { name: 'Ruby', ri_low: 1.762, ri_high: 1.770, sg: 4.00, hardness: 9.0, dispersion: 0.018, system: 'Trigonal', pleochroism: 'Distinct' }, + { name: 'Sapphire', ri_low: 1.762, ri_high: 1.770, sg: 4.00, hardness: 9.0, dispersion: 0.018, system: 'Trigonal', pleochroism: 'Distinct' }, + { name: 'Emerald', ri_low: 1.570, ri_high: 1.590, sg: 2.72, hardness: 7.5, dispersion: 0.014, system: 'Hexagonal', pleochroism: 'Distinct' }, + { name: 'Spinel', ri_low: 1.718, ri_high: 1.718, sg: 3.60, hardness: 8.0, dispersion: 0.020, system: 'Cubic', pleochroism: 'None' }, + { name: 'Topaz', ri_low: 1.609, ri_high: 1.617, sg: 3.53, hardness: 8.0, dispersion: 0.014, system: 'Orthorhombic', pleochroism: 'Weak' }, + { name: 'Tourmaline', ri_low: 1.624, ri_high: 1.644, sg: 3.10, hardness: 7.0, dispersion: 0.017, system: 'Trigonal', pleochroism: 'Strong' }, + { name: 'Quartz', ri_low: 1.544, ri_high: 1.553, sg: 2.65, hardness: 7.0, dispersion: 0.013, system: 'Trigonal', pleochroism: 'Weak' }, + { name: 'Zircon', ri_low: 1.925, ri_high: 1.984, sg: 4.70, hardness: 7.5, dispersion: 0.039, system: 'Tetragonal', pleochroism: 'Weak' }, + { name: 'Garnet (Pyrope)', ri_low: 1.740, ri_high: 1.740, sg: 3.76, hardness: 7.5, dispersion: 0.022, system: 'Cubic', pleochroism: 'None' }, + { name: 'Garnet (Almandine)', ri_low: 1.780, ri_high: 1.780, sg: 4.13, hardness: 7.5, dispersion: 0.024, system: 'Cubic', pleochroism: 'None' }, + { name: 'Peridot', ri_low: 1.654, ri_high: 1.690, sg: 3.34, hardness: 6.5, dispersion: 0.020, system: 'Orthorhombic', pleochroism: 'Weak' }, + { name: 'Aquamarine', ri_low: 1.570, ri_high: 1.590, sg: 2.71, hardness: 7.5, dispersion: 0.014, system: 'Hexagonal', pleochroism: 'Weak' }, + { name: 'Tanzanite', ri_low: 1.691, ri_high: 1.700, sg: 3.35, hardness: 6.5, dispersion: 0.030, system: 'Orthorhombic', pleochroism: 'Very Strong' }, + { name: 'Alexandrite', ri_low: 1.745, ri_high: 1.755, sg: 3.71, hardness: 8.5, dispersion: 0.015, system: 'Orthorhombic', pleochroism: 'Very Strong' }, +]; + +export function GemComparison() { + const [selected, setSelected] = useState(['Diamond', 'Ruby']); + + const handleToggleGem = (gemName: string) => { + if (selected.includes(gemName)) { + if (selected.length > 1) { + setSelected(selected.filter(n => n !== gemName)); + } + } else { + if (selected.length < 4) { + setSelected([...selected, gemName]); + } + } + }; + + const selectedGems = selected.map(name => GEM_DATABASE.find(g => g.name === name)!); + + return ( +
+
+

+ Select 2-4 gems to compare their optical and physical properties side-by-side. +

+
+ +
+
+

+ Selected Gems ({selected.length}/4) +

+ {selected.length > 1 && ( + + )} +
+
+ {GEM_DATABASE.map(gem => ( + + ))} +
+
+ + {/* Comparison Table */} +
+ + + + + {selectedGems.map(gem => ( + + ))} + + + + + + {selectedGems.map(gem => ( + + ))} + + + + {selectedGems.map(gem => ( + + ))} + + + + {selectedGems.map(gem => ( + + ))} + + + + {selectedGems.map(gem => ( + + ))} + + + + {selectedGems.map(gem => ( + + ))} + + + + {selectedGems.map(gem => ( + + ))} + + + + {selectedGems.map(gem => ( + + ))} + + + + {selectedGems.map(gem => ( + + ))} + + +
Property + {gem.name} +
RI (Low) + {gem.ri_low.toFixed(3)} +
RI (High) + {gem.ri_high.toFixed(3)} +
Birefringence + {(gem.ri_high - gem.ri_low).toFixed(3)} +
Specific Gravity + {gem.sg.toFixed(2)} +
Hardness (Mohs) + {gem.hardness} +
Dispersion + {gem.dispersion.toFixed(3)} +
Crystal System + {gem.system} +
Pleochroism + {gem.pleochroism} +
+
+ +
+

Comparison Tips

+
    +
  • • Compare RI to separate gems with similar appearance
  • +
  • • SG and hardness help distinguish gems of similar RI
  • +
  • • Birefringence and pleochroism indicate crystal system
  • +
  • • Dispersion affects brilliance and fire in well-cut stones
  • +
+
+
+ ); +} diff --git a/src/components/identification/HardnessReference.tsx b/src/components/identification/HardnessReference.tsx new file mode 100644 index 0000000..159da96 --- /dev/null +++ b/src/components/identification/HardnessReference.tsx @@ -0,0 +1,167 @@ +/** + * Mohs Hardness Scale Reference + * Includes wear resistance notes and gemstone hardness lookup + */ + +import { useState } from 'react'; + +interface MohsLevel { + hardness: number; + mineral: string; + description: string; + wearResistance: string; +} + +interface GemHardness { + name: string; + hardness: number | string; + wearability: 'Excellent' | 'Good' | 'Fair' | 'Poor'; + notes: string; +} + +const MOHS_SCALE: MohsLevel[] = [ + { hardness: 10, mineral: 'Diamond', description: 'Hardest natural material', wearResistance: 'Excellent for all jewelry' }, + { hardness: 9, mineral: 'Corundum (Ruby/Sapphire)', description: 'Extremely durable', wearResistance: 'Excellent for all jewelry' }, + { hardness: 8, mineral: 'Topaz/Spinel', description: 'Very durable', wearResistance: 'Excellent for daily wear' }, + { hardness: 7.5, mineral: 'Beryl (Emerald/Aquamarine)', description: 'Durable with care', wearResistance: 'Good for most jewelry' }, + { hardness: 7, mineral: 'Quartz', description: 'Resistant to scratching', wearResistance: 'Good for most jewelry' }, + { hardness: 6.5, mineral: 'Tanzanite/Peridot', description: 'Moderate durability', wearResistance: 'Fair - avoid rings' }, + { hardness: 6, mineral: 'Feldspar (Moonstone)', description: 'Easily scratched', wearResistance: 'Fair - pendants/earrings' }, + { hardness: 5, mineral: 'Apatite', description: 'Fragile', wearResistance: 'Poor - collector only' }, + { hardness: 4, mineral: 'Fluorite', description: 'Very fragile', wearResistance: 'Poor - collector only' }, + { hardness: 3, mineral: 'Calcite', description: 'Extremely fragile', wearResistance: 'Not suitable for jewelry' }, +]; + +const COMMON_GEMS: GemHardness[] = [ + { name: 'Diamond', hardness: 10.0, wearability: 'Excellent', notes: 'Perfect cleavage - avoid sharp blows' }, + { name: 'Ruby', hardness: 9.0, wearability: 'Excellent', notes: 'No cleavage - extremely durable' }, + { name: 'Sapphire', hardness: 9.0, wearability: 'Excellent', notes: 'No cleavage - extremely durable' }, + { name: 'Alexandrite', hardness: 8.5, wearability: 'Excellent', notes: 'Good toughness' }, + { name: 'Chrysoberyl', hardness: 8.5, wearability: 'Excellent', notes: 'Excellent toughness' }, + { name: 'Spinel', hardness: 8.0, wearability: 'Excellent', notes: 'No cleavage - excellent toughness' }, + { name: 'Topaz', hardness: 8.0, wearability: 'Good', notes: 'Perfect cleavage - handle with care' }, + { name: 'Emerald', hardness: '7.5-8', wearability: 'Fair', notes: 'Usually included - avoid ultrasonic' }, + { name: 'Aquamarine', hardness: '7.5-8', wearability: 'Good', notes: 'Better toughness than emerald' }, + { name: 'Tourmaline', hardness: '7-7.5', wearability: 'Good', notes: 'Can be brittle - avoid sharp blows' }, + { name: 'Garnet', hardness: '6.5-7.5', wearability: 'Good', notes: 'Varies by type - generally durable' }, + { name: 'Amethyst', hardness: 7.0, wearability: 'Good', notes: 'Durable but can fade in sunlight' }, + { name: 'Citrine', hardness: 7.0, wearability: 'Good', notes: 'Durable quartz variety' }, + { name: 'Peridot', hardness: '6.5-7', wearability: 'Fair', notes: 'Avoid rings - better for pendants' }, + { name: 'Tanzanite', hardness: '6-6.5', wearability: 'Fair', notes: 'Perfect cleavage - very fragile' }, + { name: 'Moonstone', hardness: '6-6.5', wearability: 'Fair', notes: 'Two cleavage directions - fragile' }, + { name: 'Opal', hardness: '5-6.5', wearability: 'Poor', notes: 'Hydrous - can crack/craze with drying' }, + { name: 'Turquoise', hardness: '5-6', wearability: 'Poor', notes: 'Porous - avoid chemicals/heat' }, + { name: 'Pearl', hardness: '2.5-4.5', wearability: 'Poor', notes: 'Organic - avoid acids/cosmetics/heat' }, +]; + +export function HardnessReference() { + const [searchTerm, setSearchTerm] = useState(''); + const [filterWearability, setFilterWearability] = useState('all'); + + const filteredGems = COMMON_GEMS.filter(gem => { + const matchesSearch = gem.name.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesFilter = filterWearability === 'all' || gem.wearability === filterWearability; + return matchesSearch && matchesFilter; + }); + + return ( +
+
+

Mohs Hardness Scale

+
+ + + + + + + + + + + {MOHS_SCALE.map(level => ( + + + + + + + ))} + +
HardnessReference MineralDescriptionWear Resistance
{level.hardness}{level.mineral}{level.description}{level.wearResistance}
+
+
+ +
+

Gemstone Hardness Lookup

+ +
+ setSearchTerm(e.target.value)} + className="flex-1 px-3 py-2 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-crystal-500" + /> + +
+ +
+ + + + + + + + + + + {filteredGems.map(gem => ( + + + + + + + ))} + +
GemstoneHardnessWearabilitySpecial Notes
{gem.name}{gem.hardness} + + {gem.wearability} + + {gem.notes}
+
+ + {filteredGems.length === 0 && ( +

No gemstones found matching your criteria.

+ )} +
+ +
+

Hardness vs Toughness

+
    +
  • • Hardness = resistance to scratching (Mohs scale)
  • +
  • • Toughness = resistance to breaking/chipping
  • +
  • • Diamond has high hardness but lower toughness (perfect cleavage)
  • +
  • • Jade has moderate hardness (6-7) but excellent toughness (no cleavage)
  • +
+
+
+ ); +} diff --git a/src/components/identification/IdentificationTools.tsx b/src/components/identification/IdentificationTools.tsx new file mode 100644 index 0000000..0299ded --- /dev/null +++ b/src/components/identification/IdentificationTools.tsx @@ -0,0 +1,87 @@ +/** + * Gem Identification tools container. + */ + +import { useState } from 'react'; +import { GemComparison } from './GemComparison'; +import { HardnessReference } from './HardnessReference'; +import { FractureCleavageGuide } from './FractureCleavageGuide'; +import { CalculatorCard } from '../calculator/CalculatorCard'; + +type IdentificationId = 'comparison' | 'hardness' | 'fracture'; + +interface IdentificationConfig { + id: IdentificationId; + label: string; + iconPath: string; + description: string; + component: React.ReactNode; +} + +const ICON_PATHS: Record = { + compare: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01', + hardness: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z', + split: 'M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5', +}; + +const IDENTIFICATION_TOOLS: IdentificationConfig[] = [ + { + id: 'comparison', + label: 'Gem Comparison', + iconPath: ICON_PATHS.compare, + description: 'Side-by-side property comparison (2-4 gems)', + component: , + }, + { + id: 'hardness', + label: 'Hardness Reference', + iconPath: ICON_PATHS.hardness, + description: 'Mohs scale + wearability guide', + component: , + }, + { + id: 'fracture', + label: 'Fracture & Cleavage', + iconPath: ICON_PATHS.split, + description: 'Cleavage directions + fracture patterns', + component: , + }, +]; + +export function IdentificationTools({ defaultExpanded = 'comparison' }: { defaultExpanded?: IdentificationId | null }) { + const [expandedTool, setExpandedTool] = useState(defaultExpanded); + + const handleToggle = (id: IdentificationId) => { + setExpandedTool(prev => (prev === id ? null : id)); + }; + + return ( +
+
+

+ Gem identification tools including side-by-side comparison, Mohs hardness reference, and cleavage/fracture patterns. +

+
+ +
+ {IDENTIFICATION_TOOLS.map(tool => ( + handleToggle(tool.id)} + > + {tool.component} + + ))} +
+ +

+ Tip: Click on any tool header to expand or collapse it. +

+
+ ); +} diff --git a/src/components/identification/index.ts b/src/components/identification/index.ts new file mode 100644 index 0000000..5214ae4 --- /dev/null +++ b/src/components/identification/index.ts @@ -0,0 +1,7 @@ +/** + * Gem identification tools exports. + */ + +export { GemComparison } from './GemComparison'; +export { HardnessReference } from './HardnessReference'; +export { FractureCleavageGuide } from './FractureCleavageGuide'; diff --git a/src/components/lab/ChelseaFilter.tsx b/src/components/lab/ChelseaFilter.tsx new file mode 100644 index 0000000..a327f52 --- /dev/null +++ b/src/components/lab/ChelseaFilter.tsx @@ -0,0 +1,195 @@ +/** + * Chelsea Filter Simulator + * Show expected color reactions under Chelsea filter + */ + +import { useState } from 'react'; + +interface ChelseaReaction { + gem: string; + natural: string; + synthetic: string; + notes: string; + category: string; +} + +const REACTIONS: ChelseaReaction[] = [ + { + gem: 'Emerald (Natural Colombian)', + natural: 'Strong red/pink', + synthetic: 'Strong red/pink', + notes: 'Chrome-colored emeralds fluoresce', + category: 'emerald', + }, + { + gem: 'Emerald (Natural Zambian)', + natural: 'Weak/None', + synthetic: 'Weak/None', + notes: 'Iron-rich, less chromium', + category: 'emerald', + }, + { + gem: 'Emerald (Synthetic)', + natural: 'N/A', + synthetic: 'Very strong red', + notes: 'Often brighter than natural', + category: 'emerald', + }, + { + gem: 'Ruby (Natural)', + natural: 'Strong red (brighter)', + synthetic: 'Very strong red', + notes: 'Chromium fluorescence', + category: 'corundum', + }, + { + gem: 'Sapphire (Blue)', + natural: 'Remains blue', + synthetic: 'Remains blue', + notes: 'No chromium, no reaction', + category: 'corundum', + }, + { + gem: 'Synthetic Blue Spinel', + natural: 'N/A', + synthetic: 'Red', + notes: 'Cobalt-colored shows red', + category: 'synthetic', + }, + { + gem: 'Synthetic Green Glass', + natural: 'N/A', + synthetic: 'Brown/Red', + notes: 'Chromium in glass', + category: 'synthetic', + }, + { + gem: 'Alexandrite', + natural: 'Red (enhanced)', + synthetic: 'Very strong red', + notes: 'Strong chromium content', + category: 'chrysoberyl', + }, + { + gem: 'Jadeite (Natural)', + natural: 'Remains green', + synthetic: 'Weak pink (if dyed)', + notes: 'Dyed jadeite may show pink', + category: 'jade', + }, + { + gem: 'Peridot', + natural: 'Remains green', + synthetic: 'Remains green', + notes: 'Iron-colored, no reaction', + category: 'other', + }, + { + gem: 'Diamond', + natural: 'Remains white/colorless', + synthetic: 'Remains white/colorless', + notes: 'No reaction expected', + category: 'other', + }, +]; + +export function ChelseaFilter() { + const [search, setSearch] = useState(''); + const [categoryFilter, setCategoryFilter] = useState('all'); + + const filtered = REACTIONS.filter(r => { + const matchSearch = !search || r.gem.toLowerCase().includes(search.toLowerCase()); + const matchCategory = categoryFilter === 'all' || r.category === categoryFilter; + return matchSearch && matchCategory; + }); + + return ( +
+
+

+ The Chelsea filter transmits deep red and yellow-green light, filtering out other wavelengths. Chromium-bearing gems fluoresce red. +

+
+ +
+
+ + setSearch(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., emerald, ruby" + /> +
+ +
+ + +
+
+ +
+ {filtered.map((reaction, idx) => ( +
+
{reaction.gem}
+
+ {reaction.natural !== 'N/A' && ( +
+
Natural
+
{reaction.natural}
+
+ )} + {reaction.synthetic !== 'N/A' && ( +
+
Synthetic/Treated
+
{reaction.synthetic}
+
+ )} +
+

{reaction.notes}

+
+ ))} +
+ + {filtered.length === 0 && ( +
+ No gems match your search. +
+ )} + +
+

Important Limitations

+
    +
  • • Not definitive: Chelsea filter alone cannot prove natural vs synthetic
  • +
  • • Origin matters: Different emerald sources react differently
  • +
  • • Lighting critical: Use strong light source for best results
  • +
  • • Combine tests: Always use with other identification methods
  • +
+
+ +
+

How It Works

+

+ The Chelsea filter is a didymium glass filter that transmits deep red (around 690nm) and yellow-green (around 570nm) light. Gems containing chromium fluoresce under the filter, appearing red or pink. This is especially useful for screening emeralds and detecting some synthetic stones. +

+
+
+ ); +} diff --git a/src/components/lab/HeavyLiquidReference.tsx b/src/components/lab/HeavyLiquidReference.tsx new file mode 100644 index 0000000..227d008 --- /dev/null +++ b/src/components/lab/HeavyLiquidReference.tsx @@ -0,0 +1,206 @@ +/** + * Heavy Liquid SG Reference + * Which liquids for which gem SG ranges + */ + +import { useState } from 'react'; + +interface HeavyLiquid { + name: string; + sg: number; + floats: string[]; + sinks: string[]; + notes: string; + safety: string; +} + +const LIQUIDS: HeavyLiquid[] = [ + { + name: 'Water', + sg: 1.00, + floats: [], + sinks: ['All gems'], + notes: 'Baseline reference', + safety: 'Safe', + }, + { + name: 'Toluene', + sg: 0.87, + floats: ['Amber (0.96-1.10)'], + sinks: ['Most gems'], + notes: 'For very light organics', + safety: 'Toxic - use in fume hood', + }, + { + name: 'Methylene Iodide (Pure)', + sg: 3.32, + floats: ['Quartz (2.65)', 'Beryl (2.70)', 'Tourmaline (3.06)'], + sinks: ['Diamond (3.52)', 'Corundum (4.00)', 'Spinel (3.60)'], + notes: 'Standard heavy liquid', + safety: 'Very toxic - use gloves, fume hood', + }, + { + name: 'Methylene Iodide + Toluene (3.06)', + sg: 3.06, + floats: ['Quartz (2.65)', 'Beryl (2.70)', 'Tourmaline (3.06)'], + sinks: ['Diamond (3.52)', 'Corundum (4.00)', 'Topaz (3.53)'], + notes: 'Diluted for tourmaline separation', + safety: 'Toxic - use gloves, fume hood', + }, + { + name: 'Clerici Solution', + sg: 4.25, + floats: ['Diamond (3.52)', 'Corundum (4.00)', 'Topaz (3.53)'], + sinks: ['Zircon (4.70)', 'Cassiterite (6.99)'], + notes: 'High density option', + safety: 'Toxic - corrosive', + }, + { + name: 'Bromoform', + sg: 2.89, + floats: ['Quartz (2.65)', 'Beryl (2.70)', 'Feldspar (2.56)'], + sinks: ['Tourmaline (3.06)', 'Diamond (3.52)', 'Corundum (4.00)'], + notes: 'Lower than MI', + safety: 'Toxic - use gloves', + }, +]; + +const COMMON_GEMS = [ + { name: 'Amber', sg: 1.08 }, + { name: 'Opal', sg: 2.10 }, + { name: 'Quartz', sg: 2.65 }, + { name: 'Beryl', sg: 2.70 }, + { name: 'Tourmaline', sg: 3.06 }, + { name: 'Diamond', sg: 3.52 }, + { name: 'Topaz', sg: 3.53 }, + { name: 'Spinel', sg: 3.60 }, + { name: 'Corundum', sg: 4.00 }, + { name: 'Zircon', sg: 4.70 }, +]; + +export function HeavyLiquidReference() { + const [selectedSG, setSelectedSG] = useState(null); + + return ( +
+
+

+ Heavy liquids separate gems by specific gravity. Select a liquid to see which gems float or sink. +

+
+ +
+

Heavy Liquids

+
+ {LIQUIDS.map((liquid) => ( + + ))} +
+
+ +
+

Common Gem SG Reference

+
+ + + + + + + + + + {COMMON_GEMS.map((gem) => ( + + + + + + ))} + +
GemSGLiquid Test
{gem.name}{gem.sg.toFixed(2)} + {gem.sg < 2.89 ? 'Floats in bromoform (2.89)' : + gem.sg < 3.32 ? 'Sinks in bromoform, floats in MI (3.32)' : + gem.sg < 4.25 ? 'Sinks in MI, floats in Clerici (4.25)' : + 'Sinks in Clerici solution'} +
+
+
+ +
+

⚠️ Safety Warning

+
    +
  • • Heavy liquids are highly toxic - avoid skin contact and fumes
  • +
  • • Always use gloves, safety glasses, and work in a fume hood
  • +
  • • Store in sealed containers away from light (causes decomposition)
  • +
  • • Never use with porous or fractured stones - liquid can be absorbed
  • +
  • • Dispose of according to hazardous waste regulations
  • +
+
+ +
+

Alternative: Hydrostatic Weighing

+

+ For most gem identification, hydrostatic weighing (measuring weight in air vs water) is safer and more accurate than heavy liquids. Heavy liquids are mainly used for quick separation of parcels or when hydrostatic equipment isn't available. +

+
+
+ ); +} diff --git a/src/components/lab/LabTools.tsx b/src/components/lab/LabTools.tsx new file mode 100644 index 0000000..3ffa757 --- /dev/null +++ b/src/components/lab/LabTools.tsx @@ -0,0 +1,87 @@ +/** + * Lab Equipment tools container. + */ + +import { useState } from 'react'; +import { ChelseaFilter } from './ChelseaFilter'; +import { SpectroscopeCalculator } from './SpectroscopeCalculator'; +import { HeavyLiquidReference } from './HeavyLiquidReference'; +import { CalculatorCard } from '../calculator/CalculatorCard'; + +type LabId = 'chelsea' | 'spectroscope' | 'heavy-liquid'; + +interface LabConfig { + id: LabId; + label: string; + iconPath: string; + description: string; + component: React.ReactNode; +} + +const ICON_PATHS: Record = { + filter: 'M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V4z', + spectrum: 'M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01', + beaker: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z', +}; + +const LAB_TOOLS: LabConfig[] = [ + { + id: 'chelsea', + label: 'Chelsea Filter', + iconPath: ICON_PATHS.filter, + description: 'Expected color reactions', + component: , + }, + { + id: 'spectroscope', + label: 'Spectroscope Calculator', + iconPath: ICON_PATHS.spectrum, + description: 'Wavelength ↔ color + absorption lines', + component: , + }, + { + id: 'heavy-liquid', + label: 'Heavy Liquid Reference', + iconPath: ICON_PATHS.beaker, + description: 'SG separation by liquid density', + component: , + }, +]; + +export function LabTools({ defaultExpanded = 'chelsea' }: { defaultExpanded?: LabId | null }) { + const [expandedTool, setExpandedTool] = useState(defaultExpanded); + + const handleToggle = (id: LabId) => { + setExpandedTool(prev => (prev === id ? null : id)); + }; + + return ( +
+
+

+ Lab equipment reference tools including Chelsea filter reactions, spectroscope line calculator, and heavy liquid SG separation guide. +

+
+ +
+ {LAB_TOOLS.map(tool => ( + handleToggle(tool.id)} + > + {tool.component} + + ))} +
+ +

+ Tip: Click on any tool header to expand or collapse it. +

+
+ ); +} diff --git a/src/components/lab/SpectroscopeCalculator.tsx b/src/components/lab/SpectroscopeCalculator.tsx new file mode 100644 index 0000000..0b9a50f --- /dev/null +++ b/src/components/lab/SpectroscopeCalculator.tsx @@ -0,0 +1,194 @@ +/** + * Spectroscope Line Calculator + * Wavelength ↔ color + common absorption lines + */ + +import { useState } from 'react'; + +interface AbsorptionLine { + gem: string; + wavelength: number; + color: string; + strength: string; + notes: string; +} + +const COMMON_LINES: AbsorptionLine[] = [ + { gem: 'Ruby', wavelength: 694, color: 'Deep red', strength: 'Strong', notes: 'Chromium doublet at 694.2 and 692.9nm' }, + { gem: 'Ruby', wavelength: 668, color: 'Red', strength: 'Medium', notes: 'Broad band' }, + { gem: 'Ruby', wavelength: 659, color: 'Red-orange', strength: 'Medium', notes: 'Fine line' }, + { gem: 'Emerald', wavelength: 683, color: 'Red', strength: 'Weak', notes: 'Chromium line' }, + { gem: 'Emerald', wavelength: 637, color: 'Orange-red', strength: 'Weak', notes: 'Chromium doublet' }, + { gem: 'Alexandrite', wavelength: 680, color: 'Red', strength: 'Strong', notes: 'Chromium doublet' }, + { gem: 'Jadeite (Green)', wavelength: 691, color: 'Red', strength: 'Medium', notes: 'Chromium (dyed may show)' }, + { gem: 'Demantoid Garnet', wavelength: 443, color: 'Blue-violet', strength: 'Strong', notes: 'Characteristic horsetail band' }, + { gem: 'Zircon', wavelength: 653, color: 'Red-orange', strength: 'Strong', notes: 'Uranium lines' }, + { gem: 'Diamond (Yellow)', wavelength: 415, color: 'Violet', strength: 'Medium', notes: 'Cape series' }, + { gem: 'Peridot', wavelength: 497, color: 'Blue-green', strength: 'Strong', notes: 'Iron triplet' }, + { gem: 'Peridot', wavelength: 493, color: 'Blue-green', strength: 'Strong', notes: 'Central line of triplet' }, + { gem: 'Peridot', wavelength: 473, color: 'Blue', strength: 'Strong', notes: 'Iron triplet' }, + { gem: 'Sapphire (Blue)', wavelength: 450, color: 'Blue-violet', strength: 'Medium', notes: 'Iron-titanium' }, + { gem: 'Aquamarine', wavelength: 537, color: 'Green', strength: 'Weak', notes: 'Iron absorption' }, +]; + +function wavelengthToRGB(wavelength: number): string { + let r = 0, g = 0, b = 0; + + if (wavelength >= 380 && wavelength < 440) { + r = -(wavelength - 440) / (440 - 380); + b = 1.0; + } else if (wavelength >= 440 && wavelength < 490) { + g = (wavelength - 440) / (490 - 440); + b = 1.0; + } else if (wavelength >= 490 && wavelength < 510) { + g = 1.0; + b = -(wavelength - 510) / (510 - 490); + } else if (wavelength >= 510 && wavelength < 580) { + r = (wavelength - 510) / (580 - 510); + g = 1.0; + } else if (wavelength >= 580 && wavelength < 645) { + r = 1.0; + g = -(wavelength - 645) / (645 - 580); + } else if (wavelength >= 645 && wavelength <= 780) { + r = 1.0; + } + + // Intensity falloff at edges + let factor = 1.0; + if (wavelength >= 380 && wavelength < 420) { + factor = 0.3 + 0.7 * (wavelength - 380) / (420 - 380); + } else if (wavelength >= 700 && wavelength <= 780) { + factor = 0.3 + 0.7 * (780 - wavelength) / (780 - 700); + } + + r = Math.round(r * factor * 255); + g = Math.round(g * factor * 255); + b = Math.round(b * factor * 255); + + return `rgb(${r}, ${g}, ${b})`; +} + +export function SpectroscopeCalculator() { + const [wavelength, setWavelength] = useState('589'); + const [gemFilter, setGemFilter] = useState('all'); + + const wl = parseFloat(wavelength); + const isValid = !isNaN(wl) && wl >= 380 && wl <= 780; + + const color = isValid ? wavelengthToRGB(wl) : ''; + + const filtered = COMMON_LINES.filter(line => { + const matchGem = gemFilter === 'all' || line.gem === gemFilter; + return matchGem; + }); + + const uniqueGems = Array.from(new Set(COMMON_LINES.map(l => l.gem))).sort(); + + return ( +
+
+

+ Enter a wavelength (380-780nm) to see its color, or browse common absorption lines by gem. +

+
+ +
+
+ + setWavelength(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., 694" + /> + {!isValid && wavelength && ( +

Valid range: 380-780nm

+ )} +
+ +
+ + +
+
+ + {isValid && ( +
+
+
Color at {wl}nm
+
+
+ {wl < 450 ? 'Violet-Blue' : + wl < 495 ? 'Blue' : + wl < 570 ? 'Green' : + wl < 590 ? 'Yellow-Green' : + wl < 620 ? 'Yellow-Orange' : + wl < 750 ? 'Red' : 'Deep Red'} +
+
+
+ )} + +
+

+ Common Absorption Lines ({filtered.length}) +

+
+ {filtered.map((line, idx) => ( +
+
+
+
+
{line.gem}
+ + {line.strength} + +
+
+ {line.wavelength}nm — {line.color} +
+

{line.notes}

+
+
+
+
+ ))} +
+
+ +
+

Spectroscope Usage

+
    +
  • • Use a strong white light source for best results
  • +
  • • Absorption lines appear as dark bands against bright spectrum
  • +
  • • Some gems show many lines, others show few or none
  • +
  • • Position matters - some lines only visible in certain orientations
  • +
+
+
+ ); +} diff --git a/src/components/lab/index.ts b/src/components/lab/index.ts new file mode 100644 index 0000000..a3b5c79 --- /dev/null +++ b/src/components/lab/index.ts @@ -0,0 +1,7 @@ +/** + * Lab equipment tools exports. + */ + +export { ChelseaFilter } from './ChelseaFilter'; +export { SpectroscopeCalculator } from './SpectroscopeCalculator'; +export { HeavyLiquidReference } from './HeavyLiquidReference'; diff --git a/src/components/optical/DichroscopeResults.tsx b/src/components/optical/DichroscopeResults.tsx new file mode 100644 index 0000000..544dece --- /dev/null +++ b/src/components/optical/DichroscopeResults.tsx @@ -0,0 +1,152 @@ +/** + * Dichroscope Results Lookup + * Input colors seen → suggest possible gems + */ + +import { useState } from 'react'; + +interface DichroscopeData { + gem: string; + color1: string; + color2: string; + strength: string; + notes: string; +} + +const DICHROIC_GEMS: DichroscopeData[] = [ + { gem: 'Ruby', color1: 'Red', color2: 'Orange-red', strength: 'Strong', notes: 'Best seen in thick stones' }, + { gem: 'Sapphire (Blue)', color1: 'Blue', color2: 'Greenish-blue', strength: 'Strong', notes: 'Varies with saturation' }, + { gem: 'Emerald', color1: 'Blue-green', color2: 'Yellow-green', strength: 'Moderate', notes: 'More visible in darker stones' }, + { gem: 'Alexandrite', color1: 'Red/Purple', color2: 'Orange/Green', strength: 'Very Strong', notes: 'Color change + pleochroism' }, + { gem: 'Tanzanite', color1: 'Blue/Violet', color2: 'Purple/Red', strength: 'Very Strong', notes: 'Trichroic - see 3 colors' }, + { gem: 'Tourmaline (Green)', color1: 'Dark green', color2: 'Light green', strength: 'Strong', notes: 'Best down c-axis' }, + { gem: 'Tourmaline (Pink)', color1: 'Dark pink', color2: 'Light pink', strength: 'Strong', notes: 'Visible in most directions' }, + { gem: 'Iolite', color1: 'Violet-blue', color2: 'Pale yellow', strength: 'Very Strong', notes: 'Dramatic color shift' }, + { gem: 'Andalusite', color1: 'Red-brown', color2: 'Yellow-green', strength: 'Very Strong', notes: 'Distinctive colors' }, + { gem: 'Kunzite', color1: 'Violet', color2: 'Colorless/Pink', strength: 'Strong', notes: 'Fades in light' }, + { gem: 'Peridot', color1: 'Green', color2: 'Yellow-green', strength: 'Weak', notes: 'Subtle difference' }, + { gem: 'Aquamarine', color1: 'Blue', color2: 'Colorless/Pale blue', strength: 'Weak', notes: 'Often not visible' }, + { gem: 'Topaz (Blue)', color1: 'Colorless', color2: 'Pale blue', strength: 'Weak', notes: 'Difficult to detect' }, + { gem: 'Morganite', color1: 'Pink', color2: 'Pale pink', strength: 'Weak', notes: 'Requires good stone' }, +]; + +export function DichroscopeResults() { + const [color1, setColor1] = useState(''); + const [color2, setColor2] = useState(''); + const [strengthFilter, setStrengthFilter] = useState('all'); + + const filtered = DICHROIC_GEMS.filter(gem => { + const matchColor1 = !color1 || gem.color1.toLowerCase().includes(color1.toLowerCase()) || gem.color2.toLowerCase().includes(color1.toLowerCase()); + const matchColor2 = !color2 || gem.color1.toLowerCase().includes(color2.toLowerCase()) || gem.color2.toLowerCase().includes(color2.toLowerCase()); + const matchStrength = strengthFilter === 'all' || gem.strength.toLowerCase() === strengthFilter; + return matchColor1 && matchColor2 && matchStrength; + }); + + return ( +
+
+

+ Enter the colors you observe through the dichroscope to identify possible gems. Rotate the stone to see both colors. +

+
+ +
+
+ + setColor1(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., blue, red, green" + /> +
+ +
+ + setColor2(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., greenish-blue" + /> +
+ +
+ + +
+
+ + {filtered.length > 0 ? ( +
+

+ Matching Gems ({filtered.length}) +

+ {filtered.map((gem, idx) => ( +
+
+
+
{gem.gem}
+
+
+ Color 1:{' '} + {gem.color1} +
+
+ Color 2:{' '} + {gem.color2} +
+
+

{gem.notes}

+
+
+ + {gem.strength} + +
+
+
+ ))} +
+ ) : ( +
+ No matching gems found. Try different color terms. +
+ )} + +
+

Using a Dichroscope

+
    +
  • • View the stone through both windows while rotating
  • +
  • • Isotropic gems (cubic) show NO pleochroism
  • +
  • • Uniaxial gems (tetragonal, hexagonal, trigonal) show 2 colors
  • +
  • • Biaxial gems (orthorhombic, monoclinic, triclinic) show 2-3 colors
  • +
  • • Best viewed in strong light against white background
  • +
+
+
+ ); +} diff --git a/src/components/optical/OpticalTools.tsx b/src/components/optical/OpticalTools.tsx new file mode 100644 index 0000000..1d97b91 --- /dev/null +++ b/src/components/optical/OpticalTools.tsx @@ -0,0 +1,96 @@ +/** + * Optical Properties tools container. + */ + +import { useState } from 'react'; +import { DichroscopeResults } from './DichroscopeResults'; +import { PolariscopeGuide } from './PolariscopeGuide'; +import { RefractometerSimulator } from './RefractometerSimulator'; +import { PleochroismReference } from './PleochroismReference'; +import { CalculatorCard } from '../calculator/CalculatorCard'; + +type OpticalId = 'dichroscope' | 'polariscope' | 'refractometer' | 'pleochroism'; + +interface OpticalConfig { + id: OpticalId; + label: string; + iconPath: string; + description: string; + component: React.ReactNode; +} + +const ICON_PATHS: Record = { + eye: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z', + adjust: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z', + beaker: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z', + book: 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253', +}; + +const OPTICAL_TOOLS: OpticalConfig[] = [ + { + id: 'dichroscope', + label: 'Dichroscope Results', + iconPath: ICON_PATHS.eye, + description: 'Input colors seen → suggest gems', + component: , + }, + { + id: 'polariscope', + label: 'Polariscope Guide', + iconPath: ICON_PATHS.adjust, + description: 'Interpret isotropic/anisotropic reactions', + component: , + }, + { + id: 'refractometer', + label: 'Refractometer Simulator', + iconPath: ICON_PATHS.beaker, + description: 'Practice reading shadow edges', + component: , + }, + { + id: 'pleochroism', + label: 'Pleochroism Reference', + iconPath: ICON_PATHS.book, + description: 'Expected colors by gem species', + component: , + }, +]; + +export function OpticalTools({ defaultExpanded = 'dichroscope' }: { defaultExpanded?: OpticalId | null }) { + const [expandedTool, setExpandedTool] = useState(defaultExpanded); + + const handleToggle = (id: OpticalId) => { + setExpandedTool(prev => (prev === id ? null : id)); + }; + + return ( +
+
+

+ Optical property tools for gem identification using dichroscope, polariscope, refractometer, and pleochroism analysis. +

+
+ +
+ {OPTICAL_TOOLS.map(tool => ( + handleToggle(tool.id)} + > + {tool.component} + + ))} +
+ +

+ Tip: Click on any tool header to expand or collapse it. +

+
+ ); +} diff --git a/src/components/optical/PleochroismReference.tsx b/src/components/optical/PleochroismReference.tsx new file mode 100644 index 0000000..905fd74 --- /dev/null +++ b/src/components/optical/PleochroismReference.tsx @@ -0,0 +1,264 @@ +/** + * Pleochroism Reference Guide + * Expected colors by gem and crystal axis + */ + +import { useState } from 'react'; + +interface PleochroismData { + gem: string; + system: string; + direction1: string; + color1: string; + direction2: string; + color2: string; + direction3?: string; + color3?: string; + strength: string; + notes: string; +} + +const PLEOCHROIC_GEMS: PleochroismData[] = [ + { + gem: 'Ruby', + system: 'Trigonal', + direction1: 'O-ray', + color1: 'Purplish red', + direction2: 'E-ray', + color2: 'Orange-red', + strength: 'Distinct', + notes: 'Best seen in thick stones', + }, + { + gem: 'Sapphire (Blue)', + system: 'Trigonal', + direction1: 'O-ray', + color1: 'Deep blue', + direction2: 'E-ray', + color2: 'Greenish-blue', + strength: 'Distinct', + notes: 'Varies with saturation', + }, + { + gem: 'Emerald', + system: 'Hexagonal', + direction1: 'O-ray', + color1: 'Blue-green', + direction2: 'E-ray', + color2: 'Yellow-green', + strength: 'Distinct', + notes: 'More visible in darker stones', + }, + { + gem: 'Tanzanite', + system: 'Orthorhombic', + direction1: 'Alpha', + color1: 'Blue', + direction2: 'Beta', + color2: 'Violet', + direction3: 'Gamma', + color3: 'Purple/Red', + strength: 'Very Strong', + notes: 'Trichroic - heat treated shows 2 colors', + }, + { + gem: 'Alexandrite', + system: 'Orthorhombic', + direction1: 'Alpha', + color1: 'Green', + direction2: 'Beta', + color2: 'Orange', + direction3: 'Gamma', + color3: 'Red/Purple', + strength: 'Very Strong', + notes: 'Color change + strong pleochroism', + }, + { + gem: 'Andalusite', + system: 'Orthorhombic', + direction1: 'Alpha', + color1: 'Yellow-green', + direction2: 'Beta', + color2: 'Brown-green', + direction3: 'Gamma', + color3: 'Red-brown', + strength: 'Very Strong', + notes: 'Distinctive multicolor effect', + }, + { + gem: 'Iolite', + system: 'Orthorhombic', + direction1: 'Alpha', + color1: 'Violet-blue', + direction2: 'Beta', + color2: 'Blue', + direction3: 'Gamma', + color3: 'Pale yellow', + strength: 'Very Strong', + notes: 'Water sapphire - dramatic colors', + }, + { + gem: 'Kunzite', + system: 'Monoclinic', + direction1: 'Alpha', + color1: 'Colorless', + direction2: 'Beta', + color2: 'Pink', + direction3: 'Gamma', + color3: 'Violet', + strength: 'Distinct', + notes: 'Fades in light over time', + }, + { + gem: 'Tourmaline (Green)', + system: 'Trigonal', + direction1: 'O-ray', + color1: 'Dark green', + direction2: 'E-ray', + color2: 'Light green', + strength: 'Strong', + notes: 'Best viewed down c-axis', + }, + { + gem: 'Tourmaline (Pink)', + system: 'Trigonal', + direction1: 'O-ray', + color1: 'Dark pink', + direction2: 'E-ray', + color2: 'Light pink', + strength: 'Strong', + notes: 'Orientation important for cutting', + }, + { + gem: 'Aquamarine', + system: 'Hexagonal', + direction1: 'O-ray', + color1: 'Blue', + direction2: 'E-ray', + color2: 'Colorless/Pale blue', + strength: 'Weak', + notes: 'Often not visible', + }, + { + gem: 'Peridot', + system: 'Orthorhombic', + direction1: 'Alpha', + color1: 'Colorless/Pale green', + direction2: 'Beta', + color2: 'Yellow-green', + direction3: 'Gamma', + color3: 'Green', + strength: 'Weak', + notes: 'Subtle color difference', + }, +]; + +export function PleochroismReference() { + const [filter, setFilter] = useState('all'); + const [search, setSearch] = useState(''); + + const filtered = PLEOCHROIC_GEMS.filter(gem => { + const matchStrength = filter === 'all' || gem.strength.toLowerCase() === filter; + const matchSearch = !search || gem.gem.toLowerCase().includes(search.toLowerCase()); + return matchStrength && matchSearch; + }); + + return ( +
+
+

+ Reference guide for expected pleochroic colors in common gems. Use with dichroscope for identification. +

+
+ +
+
+ + setSearch(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-crystal-500 focus:border-crystal-500" + placeholder="e.g., ruby, tanzanite" + /> +
+ +
+ + +
+
+ +
+ {filtered.map((gem, idx) => ( +
+
+
+
{gem.gem}
+

{gem.system} • {gem.strength}

+
+ + {gem.strength} + +
+ +
+
+
{gem.direction1}
+
{gem.color1}
+
+
+
{gem.direction2}
+
{gem.color2}
+
+ {gem.direction3 && gem.color3 && ( +
+
{gem.direction3}
+
{gem.color3}
+
+ )} +
+ +

{gem.notes}

+
+ ))} +
+ + {filtered.length === 0 && ( +
+ No gems match your search criteria. +
+ )} + +
+

Crystal Systems & Pleochroism

+
    +
  • • Cubic: No pleochroism (isotropic)
  • +
  • • Tetragonal, Hexagonal, Trigonal: 2 colors (uniaxial)
  • +
  • • Orthorhombic, Monoclinic, Triclinic: 2-3 colors (biaxial)
  • +
  • • O-ray/E-ray: Ordinary/Extraordinary rays in uniaxial gems
  • +
  • • Alpha/Beta/Gamma: Three optical directions in biaxial gems
  • +
+
+
+ ); +} diff --git a/src/components/optical/PolariscopeGuide.tsx b/src/components/optical/PolariscopeGuide.tsx new file mode 100644 index 0000000..b1b7791 --- /dev/null +++ b/src/components/optical/PolariscopeGuide.tsx @@ -0,0 +1,151 @@ +/** + * Polariscope Interpretation Guide + * Interactive guide to polariscope reactions + */ + +import { useState } from 'react'; + +const REACTIONS = [ + { + id: 'dark', + name: 'Remains Dark (Extinct)', + interpretation: 'Isotropic (Cubic system or Amorphous)', + examples: ['Diamond', 'Spinel', 'Garnet', 'Opal', 'Glass'], + color: 'slate', + }, + { + id: 'blink', + name: 'Blinks 4 Times (Light/Dark/Light/Dark)', + interpretation: 'Anisotropic - Doubly Refractive', + examples: ['Ruby', 'Sapphire', 'Emerald', 'Topaz', 'Tourmaline', 'Quartz'], + color: 'emerald', + }, + { + id: 'adr', + name: 'Anomalous Double Refraction (ADR)', + interpretation: 'Isotropic with strain or twinning', + examples: ['Synthetic spinel', 'Some garnets', 'Some diamonds'], + color: 'amber', + }, + { + id: 'optic', + name: 'Bull\'s Eye or Cross Pattern', + interpretation: 'Optic axis visible (uniaxial or biaxial figure)', + examples: ['View down c-axis in corundum, quartz, tourmaline'], + color: 'sapphire', + }, +]; + +const PROCEDURES = [ + { + step: '1', + title: 'Set Up', + description: 'Cross the polarizers so no light passes through', + details: 'Rotate top polarizer until the field goes completely dark', + }, + { + step: '2', + title: 'Place Stone', + description: 'Insert the gem between crossed polarizers', + details: 'Use a bezel setter or immersion cell if needed', + }, + { + step: '3', + title: 'Rotate Stone', + description: 'Rotate the stone 360° and observe changes', + details: 'Count how many times it goes from light to dark', + }, + { + step: '4', + title: 'Interpret', + description: 'Match the observed reaction to the patterns below', + details: 'Some gems may show weak or partial reactions', + }, +]; + +export function PolariscopeGuide() { + const [selectedReaction, setSelectedReaction] = useState(null); + + return ( +
+
+

+ Interactive guide to interpreting polariscope reactions. Select a reaction pattern to learn more. +

+
+ +
+

Testing Procedure

+
+ {PROCEDURES.map((proc) => ( +
+
+
+ {proc.step} +
+
{proc.title}
+
+

{proc.description}

+

{proc.details}

+
+ ))} +
+
+ +
+

Reaction Patterns

+
+ {REACTIONS.map((reaction) => ( + + ))} +
+
+ +
+

Important Notes

+
    +
  • • Optic axis: Stones viewed down optic axis remain dark even if anisotropic
  • +
  • • Thin sections: Very thin stones may show weak reactions
  • +
  • • ADR: Strain patterns indicate heat treatment or synthetic origin in some cases
  • +
  • • Conoscope: Use convergent light to see interference figures
  • +
+
+ +
+

Troubleshooting

+
    +
  • • If no reaction at all, check polarizers are properly crossed
  • +
  • • Weak reactions may need immersion fluid to reduce surface reflections
  • +
  • • Multiple orientations needed for comprehensive testing
  • +
  • • Clean polarizers and stone surface for best results
  • +
+
+
+ ); +} diff --git a/src/components/optical/RefractometerSimulator.tsx b/src/components/optical/RefractometerSimulator.tsx new file mode 100644 index 0000000..293ffaf --- /dev/null +++ b/src/components/optical/RefractometerSimulator.tsx @@ -0,0 +1,174 @@ +/** + * Refractometer Reading Simulator + * Interactive teaching tool for learning to read shadow edges + */ + +import { useState } from 'react'; + +const SIMULATION_GEMS = [ + { name: 'Diamond', ri_low: 2.417, ri_high: 2.417, isotropic: true }, + { name: 'Spinel', ri_low: 1.718, ri_high: 1.718, isotropic: true }, + { name: 'Garnet (Pyrope)', ri_low: 1.740, ri_high: 1.740, isotropic: true }, + { name: 'Ruby', ri_low: 1.762, ri_high: 1.770, isotropic: false }, + { name: 'Sapphire', ri_low: 1.762, ri_high: 1.770, isotropic: false }, + { name: 'Emerald', ri_low: 1.570, ri_high: 1.590, isotropic: false }, + { name: 'Quartz', ri_low: 1.544, ri_high: 1.553, isotropic: false }, + { name: 'Topaz', ri_low: 1.609, ri_high: 1.617, isotropic: false }, + { name: 'Tourmaline', ri_low: 1.624, ri_high: 1.644, isotropic: false }, + { name: 'Zircon', ri_low: 1.925, ri_high: 1.984, isotropic: false }, +]; + +export function RefractometerSimulator() { + const [selectedGem, setSelectedGem] = useState(SIMULATION_GEMS[0]); + const [showAnswer, setShowAnswer] = useState(false); + + const birefringence = selectedGem.ri_high - selectedGem.ri_low; + + // Calculate visual position (1.40 to 1.80 scale for display) + const scale_min = 1.40; + const scale_max = 1.80; + const scale_range = scale_max - scale_min; + + const pos_low = ((selectedGem.ri_low - scale_min) / scale_range) * 100; + const pos_high = ((selectedGem.ri_high - scale_min) / scale_range) * 100; + + return ( +
+
+

+ Practice reading refractometer shadow edges. Select a gem to simulate its reading on the scale. +

+
+ +
+ + +
+ + {/* Simulated Refractometer Scale */} +
+

Refractometer Scale View

+ + {/* Scale */} +
+ {/* Scale markings */} +
+ {[1.40, 1.45, 1.50, 1.55, 1.60, 1.65, 1.70, 1.75, 1.80].map((val) => { + const pos = ((val - scale_min) / scale_range) * 100; + return ( +
+
+ {val.toFixed(2)} +
+
+ ); + })} +
+ + {/* Shadow edge(s) */} + {!showAnswer ? ( +
+
Click "Show Reading" to see shadow edge
+
+ ) : ( + <> + {/* Dark area (low RI side) */} +
+ + {/* Shadow edge marker (low) */} +
+
+ {selectedGem.ri_low.toFixed(3)} +
+
+ + {/* If DR, show high edge */} + {!selectedGem.isotropic && birefringence > 0.001 && ( + <> +
+
+
+ {selectedGem.ri_high.toFixed(3)} +
+
+ + )} + + )} +
+ +
+ +
+
+ + {showAnswer && ( +
+
+
Low RI
+
{selectedGem.ri_low.toFixed(3)}
+
+ +
+
High RI
+
{selectedGem.ri_high.toFixed(3)}
+
+ +
+
Birefringence
+
{birefringence.toFixed(3)}
+
+ {selectedGem.isotropic ? 'Isotropic (SR)' : 'Anisotropic (DR)'} +
+
+
+ )} + +
+

Reading Tips

+
    +
  • • Single shadow edge: Isotropic gem (cubic or amorphous)
  • +
  • • Double shadow edge: Doubly refractive gem - rotate to see both
  • +
  • • Blurry edge: Poor contact with hemisphere - add RI fluid
  • +
  • • No reading: RI above 1.81 (over the limit)
  • +
+
+
+ ); +} diff --git a/src/components/optical/index.ts b/src/components/optical/index.ts new file mode 100644 index 0000000..687afef --- /dev/null +++ b/src/components/optical/index.ts @@ -0,0 +1,8 @@ +/** + * Optical properties tools exports. + */ + +export { DichroscopeResults } from './DichroscopeResults'; +export { PolariscopeGuide } from './PolariscopeGuide'; +export { RefractometerSimulator } from './RefractometerSimulator'; +export { PleochroismReference } from './PleochroismReference'; diff --git a/src/pages/tools/advanced.astro b/src/pages/tools/advanced.astro new file mode 100644 index 0000000..081eda4 --- /dev/null +++ b/src/pages/tools/advanced.astro @@ -0,0 +1,14 @@ +--- +/** + * Advanced Analysis tools page. + */ +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { ProtectedAdvanced } from '../../components/auth/ProtectedAdvanced'; +--- + + + + diff --git a/src/pages/tools/conversions.astro b/src/pages/tools/conversions.astro new file mode 100644 index 0000000..ee197a7 --- /dev/null +++ b/src/pages/tools/conversions.astro @@ -0,0 +1,15 @@ +--- +/** + * Unit Conversions tools page. + * Protected by LockGate - requires authentication to access. + */ +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { ProtectedConversions } from '../../components/auth/ProtectedConversions'; +--- + + + + diff --git a/src/pages/tools/identification.astro b/src/pages/tools/identification.astro new file mode 100644 index 0000000..501e371 --- /dev/null +++ b/src/pages/tools/identification.astro @@ -0,0 +1,14 @@ +--- +/** + * Gem Identification tools page. + */ +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { ProtectedIdentification } from '../../components/auth/ProtectedIdentification'; +--- + + + + diff --git a/src/pages/tools/lab.astro b/src/pages/tools/lab.astro new file mode 100644 index 0000000..c9b638f --- /dev/null +++ b/src/pages/tools/lab.astro @@ -0,0 +1,14 @@ +--- +/** + * Lab Equipment tools page. + */ +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { ProtectedLab } from '../../components/auth/ProtectedLab'; +--- + + + + diff --git a/src/pages/tools/measurement.astro b/src/pages/tools/measurement.astro new file mode 100644 index 0000000..a9df4bf --- /dev/null +++ b/src/pages/tools/measurement.astro @@ -0,0 +1,15 @@ +--- +/** + * Measurement & Calculation tools page. + * Protected by LockGate - requires authentication to access. + */ +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { ProtectedMeasurement } from '../../components/auth/ProtectedMeasurement'; +--- + + + + diff --git a/src/pages/tools/optical.astro b/src/pages/tools/optical.astro new file mode 100644 index 0000000..88f64e1 --- /dev/null +++ b/src/pages/tools/optical.astro @@ -0,0 +1,14 @@ +--- +/** + * Optical Properties tools page. + */ +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { ProtectedOptical } from '../../components/auth/ProtectedOptical'; +--- + + + +