@jjlmoya/utils-chrono 1.19.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +4 -0
- package/src/entries.ts +7 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/altitude-watch-accuracy-estimator/altitude-watch-accuracy-estimator.css +488 -0
- package/src/tool/altitude-watch-accuracy-estimator/bibliography.astro +16 -0
- package/src/tool/altitude-watch-accuracy-estimator/bibliography.ts +12 -0
- package/src/tool/altitude-watch-accuracy-estimator/client.ts +167 -0
- package/src/tool/altitude-watch-accuracy-estimator/component.astro +15 -0
- package/src/tool/altitude-watch-accuracy-estimator/components/AltitudeScene.astro +138 -0
- package/src/tool/altitude-watch-accuracy-estimator/entry.ts +61 -0
- package/src/tool/altitude-watch-accuracy-estimator/helpers.ts +49 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/de.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/en.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/es.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/fr.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/id.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/it.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/ja.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/ko.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/nl.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/pl.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/pt.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/ru.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/sv.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/tr.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/i18n/zh.ts +101 -0
- package/src/tool/altitude-watch-accuracy-estimator/index.ts +11 -0
- package/src/tool/altitude-watch-accuracy-estimator/logic.ts +61 -0
- package/src/tool/altitude-watch-accuracy-estimator/seo.astro +16 -0
- package/src/tool/watch-crystal-material-comparison/bibliography.astro +16 -0
- package/src/tool/watch-crystal-material-comparison/bibliography.ts +16 -0
- package/src/tool/watch-crystal-material-comparison/client.ts +150 -0
- package/src/tool/watch-crystal-material-comparison/component.astro +15 -0
- package/src/tool/watch-crystal-material-comparison/components/CrystalPanel.astro +20 -0
- package/src/tool/watch-crystal-material-comparison/entry.ts +43 -0
- package/src/tool/watch-crystal-material-comparison/helpers.ts +49 -0
- package/src/tool/watch-crystal-material-comparison/i18n/de.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/en.ts +90 -0
- package/src/tool/watch-crystal-material-comparison/i18n/es.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/fr.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/id.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/it.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/ja.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/ko.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/nl.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/pl.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/pt.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/ru.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/sv.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/tr.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/i18n/zh.ts +35 -0
- package/src/tool/watch-crystal-material-comparison/index.ts +11 -0
- package/src/tool/watch-crystal-material-comparison/logic.ts +55 -0
- package/src/tool/watch-crystal-material-comparison/seo.astro +16 -0
- package/src/tool/watch-crystal-material-comparison/watch-crystal-material-comparison.css +625 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { AltitudeWatchAccuracyEstimatorUI } from '../entry';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import { buildSchemas } from '../helpers';
|
|
5
|
+
|
|
6
|
+
const faq = [
|
|
7
|
+
{
|
|
8
|
+
question: '为什么机械表在高海拔地区走得更快?',
|
|
9
|
+
answer: '机械表在高海拔地区走得更快的主要原因是空气密度降低。较稀薄的空气对摆轮产生的空气阻力更小,使其能够以稍大的振幅摆动。这种增加的振幅会导致手表走快 - 通常每升高1,000米,每天快2-6秒,具体取决于机芯设计。',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
question: '海拔高度也会影响石英表吗?',
|
|
13
|
+
answer: '石英表由于没有摆动的机械摆轮,受海拔影响极小。然而,极端海拔变化可能因温度变化影响电池性能。与机械表相比,这种影响可以忽略不计。',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
question: '海拔变化会损坏我的手表吗?',
|
|
17
|
+
answer: '单纯的海拔变化很少损坏机械表。但是,快速减压(如在飞机上)可能会导致某些手表的防水出现问题。旅行中的正常海拔变化完全在任何手表的设计公差范围内。',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const howTo = [
|
|
22
|
+
{
|
|
23
|
+
name: '选择海拔高度',
|
|
24
|
+
text: '上下拖动海拔滑块,模拟从海平面到8,000米的不同海拔高度。观察摆轮摆动和大气数据如何实时变化。',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: '读取偏差',
|
|
28
|
+
text: '速率偏差显示区显示选定海拔高度下每天估计的快慢秒数。下方的偏差图表显示所有海拔高度的趋势。',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: '考虑影响因素',
|
|
32
|
+
text: '观察空气密度如何随海拔升高而降低,同时速率偏差如何增加。温度和压力数据为环境变化提供了背景信息。',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const title = '海拔精度估算器:海拔高度对机械表的影响';
|
|
37
|
+
|
|
38
|
+
export const content: ToolLocaleContent<AltitudeWatchAccuracyEstimatorUI> = {
|
|
39
|
+
slug: 'altitude-watch-accuracy-estimator',
|
|
40
|
+
title,
|
|
41
|
+
description: '探索海拔高度如何影响机械表的精度。从海平面到山顶调节海拔高度,实时查看摆轮摆动、速率偏差、空气密度、压力和温度的变化。',
|
|
42
|
+
ui: {
|
|
43
|
+
title: '海拔精度估算器',
|
|
44
|
+
altitudeLabel: '海拔',
|
|
45
|
+
altitudeUnit: '米',
|
|
46
|
+
seaLevel: '海平面',
|
|
47
|
+
deviationLabel: '速率偏差',
|
|
48
|
+
deviationUnit: '秒/天',
|
|
49
|
+
pressureLabel: '气压',
|
|
50
|
+
pressureUnit: 'hPa',
|
|
51
|
+
densityLabel: '空气密度',
|
|
52
|
+
densityUnit: 'kg/m³',
|
|
53
|
+
temperatureLabel: '温度',
|
|
54
|
+
temperatureUnit: '°C',
|
|
55
|
+
oscillationLabel: '摆轮',
|
|
56
|
+
oscillationsPerSec: '次/秒',
|
|
57
|
+
rateLabel: '速率',
|
|
58
|
+
atmDataTitle: '大气条件',
|
|
59
|
+
howItWorks: '工作原理',
|
|
60
|
+
howItWorksDesc: '高海拔地区较低的空气密度减少了摆轮的阻力,增加了摆动幅度,导致手表走快。此工具基于标准大气模型估算速率偏差。',
|
|
61
|
+
negligible: '可忽略',
|
|
62
|
+
minor: '轻微',
|
|
63
|
+
noticeable: '明显',
|
|
64
|
+
significant: '显著',
|
|
65
|
+
severe: '严重',
|
|
66
|
+
step1: '拖动滑块模拟从海平面到8,000米的海拔高度。',
|
|
67
|
+
step2: '观察摆轮动画和偏差指示器的实时响应。',
|
|
68
|
+
step3: '查看大气数据以了解环境因素的影响。',
|
|
69
|
+
tipTitle: '提示',
|
|
70
|
+
tipContent: '效果因机芯而异:高振频机芯(36,000次/小时)通常比 vintage 低振频机芯(18,000次/小时)受影响更小。',
|
|
71
|
+
deviationChart: '偏差 vs 海拔',
|
|
72
|
+
altitudeM: '海拔(米)',
|
|
73
|
+
secondsPerDay: '秒/天',
|
|
74
|
+
particleLabel: '空气分子',
|
|
75
|
+
airDensity: '空气密度',
|
|
76
|
+
},
|
|
77
|
+
seo: [
|
|
78
|
+
{ type: 'title', text: '机械表交互式海拔精度估算工具', level: 2 },
|
|
79
|
+
{ type: 'paragraph', html: '<strong>海拔精度估算器</strong>是一个交互式工具,可可视化海拔变化如何影响机械表的精度。通过模拟从海平面到8,000米的海拔高度,您可以查看由空气密度、压力和温度变化引起的估计速率偏差。' },
|
|
80
|
+
{ type: 'title', text: '海拔如何影响手表精度', level: 3 },
|
|
81
|
+
{ type: 'paragraph', html: '在较高海拔高度,<strong>空气密度降低</strong>,从而减少摆轮的空气阻力。这使得摆轮能够以更大的振幅摆动,导致手表略微走快。效果通常为每升高1,000米<strong>每天快2-6秒</strong>。' },
|
|
82
|
+
{ type: 'title', text: '不同海拔高度的速率偏差', level: 3 },
|
|
83
|
+
{
|
|
84
|
+
type: 'table', headers: ['海拔', '空气密度', '气压', '温度', '估计偏差'], rows: [
|
|
85
|
+
['海平面(0米)', '1.225 kg/m³', '1013 hPa', '15°C', '基准'],
|
|
86
|
+
['1,000米', '1.112 kg/m³', '898 hPa', '8.5°C', '+0.4 秒/天'],
|
|
87
|
+
['2,000米', '1.007 kg/m³', '795 hPa', '2°C', '+0.9 秒/天'],
|
|
88
|
+
['3,000米', '0.909 kg/m³', '701 hPa', '-4.5°C', '+1.5 秒/天'],
|
|
89
|
+
['4,000米', '0.819 kg/m³', '616 hPa', '-11°C', '+2.1 秒/天'],
|
|
90
|
+
['5,000米', '0.736 kg/m³', '540 hPa', '-17.5°C', '+2.8 秒/天'],
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
{ type: 'title', text: '环境因素', level: 3 },
|
|
94
|
+
{ type: 'paragraph', html: '除了空气密度,高海拔地区的其他环境因素也会影响手表性能:<strong>温度</strong>影响润滑油粘度和发条弹性,<strong>压力变化</strong>可能影响表壳密封。然而,空气密度对摆轮阻力的影响是与海拔相关的速率变化的主导因素。' },
|
|
95
|
+
{ type: 'diagnostic', variant: 'info', title: '交互式模拟工具', icon: 'mdi:axis-arrow', badge: '钟表学', html: '此工具基于国际标准大气(ISA)模型和经验观测提供估计值。实际结果因机芯型号、状况和制造公差而异。' },
|
|
96
|
+
],
|
|
97
|
+
faq,
|
|
98
|
+
bibliography,
|
|
99
|
+
howTo,
|
|
100
|
+
schemas: buildSchemas(title, faq, howTo),
|
|
101
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { altitudeWatchAccuracyEstimator } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const ALTITUDE_WATCH_ACCURACY_ESTIMATOR_TOOL: ToolDefinition = {
|
|
7
|
+
entry: altitudeWatchAccuracyEstimator,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface AtmosphericData {
|
|
2
|
+
altitude: number;
|
|
3
|
+
pressure: number;
|
|
4
|
+
density: number;
|
|
5
|
+
temperature: number;
|
|
6
|
+
deviation: number;
|
|
7
|
+
deviationDesc: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SEA_LEVEL_PRESSURE = 1013.25;
|
|
11
|
+
const SEA_LEVEL_TEMP_K = 288.15;
|
|
12
|
+
const SEA_LEVEL_TEMP_C = 15;
|
|
13
|
+
const LAPSE_RATE = 0.0065;
|
|
14
|
+
const DEVIATION_FACTOR = 4.2;
|
|
15
|
+
|
|
16
|
+
export function getTemperature(altitude: number): number {
|
|
17
|
+
return SEA_LEVEL_TEMP_C - LAPSE_RATE * altitude;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getPressure(altitude: number): number {
|
|
21
|
+
const tRatio = 1 - LAPSE_RATE * altitude / SEA_LEVEL_TEMP_K;
|
|
22
|
+
if (tRatio <= 0) return 0;
|
|
23
|
+
return SEA_LEVEL_PRESSURE * Math.pow(tRatio, 5.255);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getAirDensity(altitude: number): number {
|
|
27
|
+
const T = getTemperature(altitude) + 273.15;
|
|
28
|
+
const P = getPressure(altitude) * 100;
|
|
29
|
+
return P / (287.058 * T);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getRateDeviation(altitude: number): number {
|
|
33
|
+
const rho0 = getAirDensity(0);
|
|
34
|
+
const rhoH = getAirDensity(altitude);
|
|
35
|
+
return DEVIATION_FACTOR * (rho0 / rhoH - 1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getDeviationDescription(deviation: number): string {
|
|
39
|
+
if (deviation < 0.5) return 'negligible';
|
|
40
|
+
if (deviation < 2) return 'minor';
|
|
41
|
+
if (deviation < 5) return 'noticeable';
|
|
42
|
+
if (deviation < 10) return 'significant';
|
|
43
|
+
return 'severe';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getAtmosphericData(altitude: number): AtmosphericData {
|
|
47
|
+
const deviation = getRateDeviation(altitude);
|
|
48
|
+
return {
|
|
49
|
+
altitude,
|
|
50
|
+
pressure: Math.round(getPressure(altitude) * 10) / 10,
|
|
51
|
+
density: Math.round(getAirDensity(altitude) * 1000) / 1000,
|
|
52
|
+
temperature: Math.round(getTemperature(altitude) * 10) / 10,
|
|
53
|
+
deviation: Math.round(deviation * 10) / 10,
|
|
54
|
+
deviationDesc: getDeviationDescription(deviation),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const DEVIATION_CHART_POINTS = Array.from({ length: 81 }, (_, i) => {
|
|
59
|
+
const alt = i * 100;
|
|
60
|
+
return { altitude: alt, deviation: Math.round(getRateDeviation(alt) * 10) / 10 };
|
|
61
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { altitudeWatchAccuracyEstimator } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const loader = altitudeWatchAccuracyEstimator.i18n[locale] || altitudeWatchAccuracyEstimator.i18n.en;
|
|
12
|
+
const content = await loader?.();
|
|
13
|
+
if (!content) return null;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { watchCrystalMaterialComparison } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props as Props;
|
|
11
|
+
const loader = watchCrystalMaterialComparison.i18n[locale] || watchCrystalMaterialComparison.i18n.en;
|
|
12
|
+
const content = await loader?.();
|
|
13
|
+
if (!content) return null;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Technical Perspective | A Comprehensive Guide to Watch Crystals - Plexiglass, Mineral, Hesalite, Sapphire Crystal - History, Pros and Cons',
|
|
6
|
+
url: 'https://monochrome-watches.com/technical-perspective-comprehensive-guide-to-watch-crystals-plexiglass-mineral-hesalite-sapphire-crystal-history-pros-and-cons/',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Mohs Scale of Mineral Hardness',
|
|
10
|
+
url: 'https://en.wikipedia.org/wiki/Mohs_scale',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Sapphire Crystal in Watches',
|
|
14
|
+
url: 'https://www.longines.com/en-se/universe/blog/sapphire',
|
|
15
|
+
}
|
|
16
|
+
];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { CRYSTAL_TYPES, type CrystalData } from './logic';
|
|
2
|
+
|
|
3
|
+
const STATS = [
|
|
4
|
+
{ key: 'hardness', label: 'HARD' },
|
|
5
|
+
{ key: 'clarity', label: 'CLAR' },
|
|
6
|
+
{ key: 'impactResistance', label: 'IMP' },
|
|
7
|
+
{ key: 'scratchResistance', label: 'SCR' },
|
|
8
|
+
{ key: 'durability', label: 'DUR' },
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
type StatKeys = typeof STATS[number]['key'];
|
|
12
|
+
|
|
13
|
+
const listEl = document.getElementById('crystal-list')!;
|
|
14
|
+
const cardWrap = document.getElementById('crystal-card-wrap')!;
|
|
15
|
+
const fightEl = document.getElementById('crystal-fight')!;
|
|
16
|
+
const dragHint = document.getElementById('crystal-drag-hint')!;
|
|
17
|
+
|
|
18
|
+
let selectedId: string = 'sapphire';
|
|
19
|
+
let fightId: string | null = null;
|
|
20
|
+
let draggedId: string | null = null;
|
|
21
|
+
let hasDragged = false;
|
|
22
|
+
|
|
23
|
+
function priceGems(level: number): string {
|
|
24
|
+
let h = '';
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
h += '<span class="price-gem' + (i < level ? ' filled' : ' empty') + '"></span>';
|
|
27
|
+
}
|
|
28
|
+
return h;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createSparkles(): string {
|
|
32
|
+
let h = '';
|
|
33
|
+
for (let i = 0; i < 12; i++) {
|
|
34
|
+
const x = 5 + Math.random() * 90;
|
|
35
|
+
const y = 5 + Math.random() * 90;
|
|
36
|
+
h += '<div class="card-sparkle" style="left:' + x + '%;top:' + y + '%;animation-delay:' + (Math.random() * 3) + 's;animation-duration:' + (2 + Math.random() * 3) + 's"></div>';
|
|
37
|
+
}
|
|
38
|
+
return h;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderCard(crystal: CrystalData): string {
|
|
42
|
+
let h = '<div class="crystal-card-big" style="background:' + crystal.colorBg + '">';
|
|
43
|
+
h += '<div class="card-orb" style="background:' + crystal.color + '"></div>';
|
|
44
|
+
h += '<div class="card-gold-accent"></div><div class="card-pattern"></div>';
|
|
45
|
+
h += '<div class="card-sparkles">' + createSparkles() + '</div>';
|
|
46
|
+
h += '<div class="card-header">';
|
|
47
|
+
h += '<div class="card-ovr-wrap"><div class="card-ovr-ring"></div><div class="card-ovr" style="background:' + crystal.color + '"><span class="card-ovr-num">' + crystal.overall + '</span><span class="card-ovr-label">OVR</span></div></div>';
|
|
48
|
+
h += '<div class="card-title"><span class="card-name">' + crystal.name + '</span><span class="card-name-sub">Watch Crystal</span></div>';
|
|
49
|
+
h += '<div class="card-price-gems">' + priceGems(crystal.priceLevel) + '</div></div>';
|
|
50
|
+
h += '<div class="card-stats">';
|
|
51
|
+
for (let i = 0; i < STATS.length; i++) {
|
|
52
|
+
const val = crystal[STATS[i].key as StatKeys] as number;
|
|
53
|
+
h += '<div class="card-stat" style="animation-delay:' + (i * 0.07) + 's">';
|
|
54
|
+
h += '<span class="card-stat-label">' + STATS[i].label + '</span>';
|
|
55
|
+
h += '<div class="card-stat-bar"><div class="card-stat-bar-glow"></div><div class="card-stat-fill" style="width:' + val + '%;background:' + crystal.color + '"></div></div>';
|
|
56
|
+
h += '<span class="card-stat-val">' + val + '</span></div>';
|
|
57
|
+
}
|
|
58
|
+
h += '</div></div>';
|
|
59
|
+
return h;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function updateCard(crystal: CrystalData) {
|
|
63
|
+
const existing = cardWrap.querySelector('.crystal-card-big');
|
|
64
|
+
if (existing) {
|
|
65
|
+
existing.classList.add('card-leaving');
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
cardWrap.innerHTML = renderCard(crystal);
|
|
68
|
+
const el = cardWrap.querySelector('.crystal-card-big');
|
|
69
|
+
if (el) el.classList.add('card-entering');
|
|
70
|
+
}, 200);
|
|
71
|
+
} else {
|
|
72
|
+
cardWrap.innerHTML = renderCard(crystal);
|
|
73
|
+
const el = cardWrap.querySelector('.crystal-card-big');
|
|
74
|
+
if (el) el.classList.add('card-entering');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderList() {
|
|
79
|
+
let h = '';
|
|
80
|
+
for (const c of CRYSTAL_TYPES) {
|
|
81
|
+
const active = c.id === selectedId ? ' active' : '';
|
|
82
|
+
h += '<div class="crystal-list-item' + active + '" data-id="' + c.id + '" draggable="true">';
|
|
83
|
+
h += '<div class="list-grip"><span></span><span></span><span></span></div>';
|
|
84
|
+
h += '<div class="list-ovr" style="background:' + c.color + '">' + c.overall + '</div>';
|
|
85
|
+
h += '<div class="list-info"><span class="list-name">' + c.name + '</span><span class="list-hint">Drag to compare</span></div></div>';
|
|
86
|
+
}
|
|
87
|
+
listEl.innerHTML = h;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderFight() {
|
|
91
|
+
if (!fightId) { fightEl.innerHTML = ''; return; }
|
|
92
|
+
const f1 = CRYSTAL_TYPES.find(c => c.id === selectedId) || CRYSTAL_TYPES[0];
|
|
93
|
+
const f2 = CRYSTAL_TYPES.find(c => c.id === fightId) || CRYSTAL_TYPES[0];
|
|
94
|
+
let h = '<div class="crystal-fight"><div class="crystal-fight-grid">';
|
|
95
|
+
h += renderCard(f1) + '<div class="crystal-fight-vs">VS</div>' + renderCard(f2);
|
|
96
|
+
h += '</div><button class="crystal-fight-close" id="fight-close">Close Comparison</button></div>';
|
|
97
|
+
fightEl.innerHTML = h;
|
|
98
|
+
document.getElementById('fight-close')?.addEventListener('click', () => {
|
|
99
|
+
fightId = null;
|
|
100
|
+
render();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleDrop(e: Event, itemId: string) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
const target = e.currentTarget as HTMLElement;
|
|
107
|
+
target.classList.remove('drag-over');
|
|
108
|
+
const dropId = (e as DragEvent).dataTransfer?.getData('text/plain');
|
|
109
|
+
if (dropId && dropId !== itemId) {
|
|
110
|
+
selectedId = dropId;
|
|
111
|
+
fightId = itemId;
|
|
112
|
+
render();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function bindItem(item: Element) {
|
|
117
|
+
const id = (item as HTMLElement).dataset.id || '';
|
|
118
|
+
item.addEventListener('click', () => {
|
|
119
|
+
if (fightId) fightId = null;
|
|
120
|
+
selectedId = id;
|
|
121
|
+
render();
|
|
122
|
+
});
|
|
123
|
+
item.addEventListener('dragstart', (e: Event) => {
|
|
124
|
+
(e as DragEvent).dataTransfer?.setData('text/plain', id);
|
|
125
|
+
draggedId = id;
|
|
126
|
+
hasDragged = true;
|
|
127
|
+
item.classList.add('dragging');
|
|
128
|
+
});
|
|
129
|
+
item.addEventListener('dragend', () => {
|
|
130
|
+
item.classList.remove('dragging');
|
|
131
|
+
draggedId = null;
|
|
132
|
+
});
|
|
133
|
+
item.addEventListener('dragover', (e: Event) => {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
if (id !== draggedId) item.classList.add('drag-over');
|
|
136
|
+
});
|
|
137
|
+
item.addEventListener('dragleave', () => { item.classList.remove('drag-over'); });
|
|
138
|
+
item.addEventListener('drop', (e: Event) => handleDrop(e, id));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function render() {
|
|
142
|
+
renderList();
|
|
143
|
+
const mainCrystal = CRYSTAL_TYPES.find(c => c.id === selectedId) || CRYSTAL_TYPES[0];
|
|
144
|
+
updateCard(mainCrystal);
|
|
145
|
+
renderFight();
|
|
146
|
+
dragHint.classList.toggle('dimmed', hasDragged);
|
|
147
|
+
listEl.querySelectorAll('.crystal-list-item').forEach(bindItem);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
render();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CrystalPanel from './components/CrystalPanel.astro';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="tool-main-card" data-ui={JSON.stringify(ui)}>
|
|
12
|
+
<CrystalPanel labels={ui} />
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<script src="./client.ts"></script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
labels: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { labels } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<link href="./watch-crystal-material-comparison.css" rel="stylesheet" />
|
|
10
|
+
|
|
11
|
+
<div class="crystal-layout">
|
|
12
|
+
<div class="crystal-list" id="crystal-list"></div>
|
|
13
|
+
<div class="crystal-card-wrap" id="crystal-card-wrap"></div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="crystal-drag-hint" id="crystal-drag-hint">
|
|
16
|
+
<span>{labels.dragHint || 'Drag'} </span>
|
|
17
|
+
<span class="drag-arrow">→</span>
|
|
18
|
+
<span> {labels.dragSub || 'crystals to compare'}</span>
|
|
19
|
+
</div>
|
|
20
|
+
<div id="crystal-fight"></div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ChronoToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export type WatchCrystalMaterialComparisonUI = {
|
|
4
|
+
title: string;
|
|
5
|
+
subTitle: string;
|
|
6
|
+
hardness: string;
|
|
7
|
+
clarity: string;
|
|
8
|
+
impactResistance: string;
|
|
9
|
+
scratchResistance: string;
|
|
10
|
+
durability: string;
|
|
11
|
+
priceRange: string;
|
|
12
|
+
step1: string;
|
|
13
|
+
step2: string;
|
|
14
|
+
step3: string;
|
|
15
|
+
tipTitle: string;
|
|
16
|
+
tipContent: string;
|
|
17
|
+
dragHint: string;
|
|
18
|
+
dragSub: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type WatchCrystalMaterialComparisonLocaleContent = ToolLocaleContent<WatchCrystalMaterialComparisonUI>;
|
|
22
|
+
|
|
23
|
+
export const watchCrystalMaterialComparison: ChronoToolEntry<WatchCrystalMaterialComparisonUI> = {
|
|
24
|
+
id: 'watch-crystal-material-comparison',
|
|
25
|
+
icons: { bg: 'mdi:diamond-stone', fg: 'mdi:glass-cocktail' },
|
|
26
|
+
i18n: {
|
|
27
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
28
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
29
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
30
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
31
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
32
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
33
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
34
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
35
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
36
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
37
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
38
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
39
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
40
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
41
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { WithContext, Thing } from 'schema-dts';
|
|
2
|
+
import type { FAQItem, HowToStep } from '../../types';
|
|
3
|
+
|
|
4
|
+
function buildFAQ(faq: FAQItem[]): WithContext<Thing> {
|
|
5
|
+
return {
|
|
6
|
+
'@context': 'https://schema.org',
|
|
7
|
+
'@type': 'FAQPage',
|
|
8
|
+
'mainEntity': faq.map((f) => ({
|
|
9
|
+
'@type': 'Question',
|
|
10
|
+
'name': f.question,
|
|
11
|
+
'acceptedAnswer': {
|
|
12
|
+
'@type': 'Answer',
|
|
13
|
+
'text': f.answer,
|
|
14
|
+
},
|
|
15
|
+
})),
|
|
16
|
+
} as unknown as WithContext<Thing>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildApp(title: string): WithContext<Thing> {
|
|
20
|
+
return {
|
|
21
|
+
'@context': 'https://schema.org',
|
|
22
|
+
'@type': 'SoftwareApplication',
|
|
23
|
+
'name': title,
|
|
24
|
+
'operatingSystem': 'All',
|
|
25
|
+
'applicationCategory': 'UtilitiesApplication',
|
|
26
|
+
'browserRequirements': 'Requires HTML5. Requires JavaScript.',
|
|
27
|
+
} as unknown as WithContext<Thing>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildHowTo(title: string, howTo: HowToStep[]): WithContext<Thing> {
|
|
31
|
+
return {
|
|
32
|
+
'@context': 'https://schema.org',
|
|
33
|
+
'@type': 'HowTo',
|
|
34
|
+
'name': title,
|
|
35
|
+
'step': howTo.map((h) => ({
|
|
36
|
+
'@type': 'HowToStep',
|
|
37
|
+
'name': h.name,
|
|
38
|
+
'text': h.text,
|
|
39
|
+
})),
|
|
40
|
+
} as unknown as WithContext<Thing>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildSchemas(
|
|
44
|
+
title: string,
|
|
45
|
+
faq: FAQItem[],
|
|
46
|
+
howTo: HowToStep[]
|
|
47
|
+
): WithContext<Thing>[] {
|
|
48
|
+
return [buildFAQ(faq), buildApp(title), buildHowTo(title, howTo)];
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { WatchCrystalMaterialComparisonUI } from '../entry';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import { buildSchemas } from '../helpers';
|
|
5
|
+
|
|
6
|
+
const faq = [
|
|
7
|
+
{ question: 'Welches Uhrenglas ist am kratzfestesten?', answer: 'Saphirglas ist mit einer Härte von 9 auf der Mohs-Skala am kratzfestesten - nur Diamant ist härter. Mineralglas (5) ist mittel, während Hesalit (2-3) leicht zerkratzt, aber poliert werden kann.' },
|
|
8
|
+
{ question: 'Kann ein zerkratztes Hesalitglas repariert werden?', answer: 'Ja, Kratzer in Hesalit können mit Polywatch in Minuten wegpoliert werden. Mineral- und Saphirgläser können nicht poliert werden und müssen bei Kratzern oder Bruch ersetzt werden.' },
|
|
9
|
+
{ question: 'Welches Glas ist am besten für Taucheruhren?', answer: 'Saphir ist der Standard für Taucheruhren aufgrund seiner Kratzfestigkeit und Haltbarkeit. Hesalit wird nicht zum Tauchen empfohlen.' },
|
|
10
|
+
];
|
|
11
|
+
const howTo = [
|
|
12
|
+
{ name: 'Kristall auswählen', text: 'Klicken Sie links auf einen Kristall, um seine Statistik-Karte rechts zu sehen.' },
|
|
13
|
+
{ name: 'Zwei Kristalle vergleichen', text: 'Ziehen Sie einen Kristall aus der Liste und legen Sie ihn auf einen anderen, um sie nebeneinander zu vergleichen.' },
|
|
14
|
+
{ name: 'Vergleich schließen', text: 'Klicken Sie auf "Close Comparison", um zur Einzelansicht zurückzukehren.' },
|
|
15
|
+
];
|
|
16
|
+
const title = 'Uhrenglas Vergleich: Hesalit vs Mineralglas vs Saphir';
|
|
17
|
+
|
|
18
|
+
export const content: ToolLocaleContent<WatchCrystalMaterialComparisonUI> = {
|
|
19
|
+
slug: 'uhrenglas-vergleich-hesalit-mineral-saphir',
|
|
20
|
+
title,
|
|
21
|
+
description: 'Vergleichen Sie Hesalit, Mineralglas und Saphir Uhrengläser mit interaktiven Statistik-Karten. Sehen Sie Härte, Klarheit, Schlagfestigkeit, Kratzfestigkeit und Haltbarkeit.',
|
|
22
|
+
ui: {
|
|
23
|
+
title: 'Uhrenglas Vergleich', subTitle: 'Interactive Crystal Stats', hardness: 'Härte', clarity: 'Klarheit',
|
|
24
|
+
impactResistance: 'Schlag', scratchResistance: 'Kratzer', durability: 'Haltbarkeit', priceRange: 'Preis',
|
|
25
|
+
step1: 'Klicken Sie links auf einen Kristall.', step2: 'Ziehen Sie einen auf einen anderen zum Vergleichen.',
|
|
26
|
+
step3: 'Klicken Sie auf Schließen.', tipTitle: 'Tipp',
|
|
27
|
+
tipContent: 'Die Gesamtwertung ist ein Durchschnitt aller Werte. Höher ist nicht immer besser - Hesalit hat die beste Schlagfestigkeit.',
|
|
28
|
+
dragHint: 'Ziehen', dragSub: 'zum Vergleichen',
|
|
29
|
+
},
|
|
30
|
+
seo: [
|
|
31
|
+
{ type: 'title', text: 'Uhrenglas Vergleich: Hesalit vs Mineralglas vs Saphir', level: 2 },
|
|
32
|
+
{ type: 'paragraph', html: 'Vergleichen Sie <strong>Hesalit, Mineralglas und Saphir</strong> Uhrengläser mit interaktiven Statistik-Karten. Sehen Sie Härte, Klarheit, Schlagfestigkeit, Kratzfestigkeit und Haltbarkeit auf einen Blick.' },
|
|
33
|
+
],
|
|
34
|
+
faq, bibliography, howTo, schemas: buildSchemas(title, faq, howTo),
|
|
35
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { WatchCrystalMaterialComparisonUI } from '../entry';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import { buildSchemas } from '../helpers';
|
|
5
|
+
|
|
6
|
+
const faq = [
|
|
7
|
+
{
|
|
8
|
+
question: 'Which is the most scratch resistant watch crystal?',
|
|
9
|
+
answer: 'Sapphire crystal is the most scratch resistant with a hardness of 9 on the Mohs scale - only diamond is harder. It is virtually impossible to scratch in everyday use. Mineral glass (5) is moderately scratch resistant, while hesalite/acrylic (2-3) scratches very easily but can be polished out in minutes with Polywatch.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
question: 'Can a scratched hesalite crystal be repaired?',
|
|
13
|
+
answer: 'Yes, hesalite (acrylic) is the only watch crystal that can be easily repaired. Minor scratches can be buffed out using a product like Polywatch in just a few minutes. This makes hesalite popular for vintage watches and the Omega Speedmaster. Mineral and sapphire crystals cannot be polished and must be replaced if scratched or shattered.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
question: 'Is sapphire crystal shatter proof?',
|
|
17
|
+
answer: 'No, sapphire crystal is not shatter proof. While it is extremely scratch resistant, it is more brittle than hesalite and can shatter on sharp impacts. Hesalite is actually the most impact-resistant crystal type - it will deform rather than shatter. Mineral glass sits in between, offering moderate impact resistance.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
question: 'Which watch crystal is best for a dive watch?',
|
|
21
|
+
answer: 'Sapphire is the standard for dive watches due to its scratch resistance and durability. Most professional dive watches use sapphire crystal because it can withstand the pressures of deep diving without scratching. Hesalite is not recommended for diving as it scratches easily and can deform under pressure, potentially compromising water resistance.',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const howTo = [
|
|
26
|
+
{
|
|
27
|
+
name: 'Select a crystal type',
|
|
28
|
+
text: 'Click any crystal from the list on the left - its stats card will appear on the right with ratings for hardness, clarity, impact resistance, scratch resistance, and durability.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Compare two crystals side by side',
|
|
32
|
+
text: 'Drag one crystal from the list and drop it onto another to start a side-by-side comparison. Both stat cards will appear so you can compare their ratings directly.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'Close the comparison view',
|
|
36
|
+
text: 'Click the "Close comparison" button below the side-by-side view to return to a single crystal display. You can also click any crystal on the left to switch the main view.',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const title = 'Watch Crystal Material Comparison: Hesalite vs Mineral Glass vs Sapphire';
|
|
41
|
+
|
|
42
|
+
export const content: ToolLocaleContent<WatchCrystalMaterialComparisonUI> = {
|
|
43
|
+
slug: 'watch-crystal-material-comparison',
|
|
44
|
+
title,
|
|
45
|
+
description: 'Compare hesalite, mineral glass, and sapphire watch crystals side by side with interactive stat cards. See hardness, clarity, impact resistance, scratch resistance, and durability ratings to choose the best crystal for your watch.',
|
|
46
|
+
ui: {
|
|
47
|
+
title: 'Crystal Material Comparison',
|
|
48
|
+
subTitle: 'Interactive Crystal Stats',
|
|
49
|
+
hardness: 'Hardness',
|
|
50
|
+
clarity: 'Clarity',
|
|
51
|
+
impactResistance: 'Impact',
|
|
52
|
+
scratchResistance: 'Scratch',
|
|
53
|
+
durability: 'Durability',
|
|
54
|
+
priceRange: 'Price',
|
|
55
|
+
step1: 'Click a crystal on the left to view its stats card.',
|
|
56
|
+
step2: 'Drag one crystal onto another to compare them side by side.',
|
|
57
|
+
step3: 'Click close to return to single crystal view.',
|
|
58
|
+
tipTitle: 'Tip',
|
|
59
|
+
tipContent: 'Overall rating is an average of all five stats. Higher overall does not mean it is the best for every use case - hesalite has the highest impact resistance while sapphire is best for scratch resistance.',
|
|
60
|
+
dragHint: 'Drag',
|
|
61
|
+
dragSub: 'crystals to compare',
|
|
62
|
+
},
|
|
63
|
+
seo: [
|
|
64
|
+
{ type: 'title', text: 'Hesalite vs Mineral vs Sapphire: Watch Crystal Comparison Guide', level: 2 },
|
|
65
|
+
{ type: 'paragraph', html: 'Choosing the right <strong>watch crystal</strong> is one of the most important decisions when buying or restoring a watch. This guide compares <strong>hesalite (acrylic), mineral glass, and sapphire crystal</strong> across hardness, clarity, impact resistance, scratch resistance, durability, and price.' },
|
|
66
|
+
{ type: 'title', text: 'Hesalite Crystal (Acrylic / Plexiglass)', level: 3 },
|
|
67
|
+
{ type: 'paragraph', html: 'Hesalite is the original watch crystal material, used since the 1930s. It is soft (2-3 Mohs) and scratches easily, but these scratches can be quickly polished out. It offers the best impact resistance - it flexes rather than shatters. Hesalite can be formed into extreme domes, giving vintage watches their characteristic look. Famous example: Omega Speedmaster Professional - the watch worn on the Moon.' },
|
|
68
|
+
{ type: 'title', text: 'Mineral Glass (Tempered)', level: 3 },
|
|
69
|
+
{ type: 'paragraph', html: 'Mineral glass is made from silicon dioxide and then tempered (heat-treated) to increase its strength. At 5 on the Mohs scale, it is harder than hesalite but still susceptible to scratches. It offers moderate impact resistance but can shatter on hard impacts. Commonly found in mid-range watches from Seiko, Citizen, and Orient.' },
|
|
70
|
+
{ type: 'title', text: 'Sapphire Crystal (Synthetic Corundum)', level: 3 },
|
|
71
|
+
{ type: 'paragraph', html: 'Sapphire crystal is made from lab-grown corundum - the same mineral as natural sapphire. At 9 on the Mohs scale, it is virtually scratch-proof and offers the best optical clarity. However, it is more brittle and can shatter on sharp impacts. It is also the most expensive option and difficult to form into high domes. The standard for luxury watches.' },
|
|
72
|
+
{ type: 'title', text: 'Crystal Comparison Table', level: 3 },
|
|
73
|
+
{
|
|
74
|
+
type: 'table', headers: ['Property', 'Hesalite', 'Mineral Glass', 'Sapphire'], rows: [
|
|
75
|
+
['Mohs Hardness', '2-3/10', '5/10', '9/10'],
|
|
76
|
+
['Clarity', '4/10', '7/10', '10/10'],
|
|
77
|
+
['Impact Resistance', '5/10 (Best)', '3/10', '2/10'],
|
|
78
|
+
['Scratch Resistance', '1/10', '5/10', '10/10'],
|
|
79
|
+
['Repairable', 'Yes (polish)', 'No (replace)', 'No (replace)'],
|
|
80
|
+
['Dome Possible', 'Yes, extreme', 'Limited', 'Limited'],
|
|
81
|
+
['Price Range', '$5 - $30', '$10 - $50', '$30 - $200+'],
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{ type: 'diagnostic', variant: 'info', title: 'Crystal Selection Guide', icon: 'mdi:diamond-stone', badge: 'REFERENCE', html: 'For a <strong>tool watch or daily beater</strong>, choose sapphire for scratch resistance. For a <strong>vintage restoration or budget build</strong>, hesalite offers authentic looks and repairability. <strong>Mineral glass</strong> is the budget-friendly middle ground found in most entry-level automatics.' },
|
|
85
|
+
],
|
|
86
|
+
faq,
|
|
87
|
+
bibliography,
|
|
88
|
+
howTo,
|
|
89
|
+
schemas: buildSchemas(title, faq, howTo),
|
|
90
|
+
};
|