@jjlmoya/utils-chrono 1.11.0 → 1.17.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 +6 -0
- package/src/entries.ts +10 -1
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/de.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/es.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/fr.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/id.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/it.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/nl.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/pl.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/pt.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/ru.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/sv.ts +1 -1
- package/src/tool/gear-train-explorer/i18n/tr.ts +1 -1
- package/src/tool/gmt-world-timer/bibliography.astro +11 -0
- package/src/tool/gmt-world-timer/bibliography.ts +7 -0
- package/src/tool/gmt-world-timer/client.ts +250 -0
- package/src/tool/gmt-world-timer/component.astro +13 -0
- package/src/tool/gmt-world-timer/components/GmtPanel.astro +18 -0
- package/src/tool/gmt-world-timer/entry.ts +34 -0
- package/src/tool/gmt-world-timer/gmt-world-timer.css +239 -0
- package/src/tool/gmt-world-timer/helpers.ts +28 -0
- package/src/tool/gmt-world-timer/i18n/de.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/en.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/es.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/fr.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/id.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/it.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/ja.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/ko.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/nl.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/pl.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/pt.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/ru.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/sv.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/tr.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/zh.ts +72 -0
- package/src/tool/gmt-world-timer/index.ts +11 -0
- package/src/tool/gmt-world-timer/seo.astro +11 -0
- package/src/tool/perpetual-calendar/bibliography.astro +16 -0
- package/src/tool/perpetual-calendar/bibliography.ts +16 -0
- package/src/tool/perpetual-calendar/calendar.ts +24 -0
- package/src/tool/perpetual-calendar/client.ts +98 -0
- package/src/tool/perpetual-calendar/component.astro +17 -0
- package/src/tool/perpetual-calendar/components/CalendarPanel.astro +49 -0
- package/src/tool/perpetual-calendar/dial.ts +176 -0
- package/src/tool/perpetual-calendar/entry.ts +48 -0
- package/src/tool/perpetual-calendar/helpers.ts +49 -0
- package/src/tool/perpetual-calendar/i18n/de.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/en.ts +102 -0
- package/src/tool/perpetual-calendar/i18n/es.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/fr.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/id.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/it.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/ja.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/ko.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/nl.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/pl.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/pt.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/ru.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/sv.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/tr.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/zh.ts +85 -0
- package/src/tool/perpetual-calendar/index.ts +11 -0
- package/src/tool/perpetual-calendar/perpetual-calendar.css +181 -0
- package/src/tool/perpetual-calendar/seo.astro +16 -0
- package/src/tool/perpetual-calendar/state.ts +26 -0
- package/src/tool/tourbillon-visualizer/bibliography.astro +11 -0
- package/src/tool/tourbillon-visualizer/bibliography.ts +7 -0
- package/src/tool/tourbillon-visualizer/client.ts +122 -0
- package/src/tool/tourbillon-visualizer/component.astro +126 -0
- package/src/tool/tourbillon-visualizer/components/TourbillonPanel.astro +66 -0
- package/src/tool/tourbillon-visualizer/entry.ts +51 -0
- package/src/tool/tourbillon-visualizer/helpers.ts +35 -0
- package/src/tool/tourbillon-visualizer/i18n/de.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/en.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/es.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/fr.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/id.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/it.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/ja.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/ko.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/nl.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/pl.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/pt.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/ru.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/sv.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/tr.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/zh.ts +96 -0
- package/src/tool/tourbillon-visualizer/index.ts +11 -0
- package/src/tool/tourbillon-visualizer/renderer/base.ts +78 -0
- package/src/tool/tourbillon-visualizer/renderer/cage.ts +115 -0
- package/src/tool/tourbillon-visualizer/renderer/esc.ts +160 -0
- package/src/tool/tourbillon-visualizer/seo.astro +11 -0
- package/src/tool/tourbillon-visualizer/state.ts +21 -0
- package/src/tool/tourbillon-visualizer/tourbillon.ts +9 -0
- package/src/tools.ts +6 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { PerpetualCalendarUI } from '../entry';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import { buildSchemas } from '../helpers';
|
|
5
|
+
|
|
6
|
+
const faq = [
|
|
7
|
+
{
|
|
8
|
+
question: 'Saatte ebedi takvim nedir?',
|
|
9
|
+
answer: 'Ebedi takvim, farklı uzunluktaki ayları ve artık yılları otomatik olarak hesaba katarak doğru tarih, gün, ay ve ay evresini gösteren mekanik bir saat komplikasyonudur. 2100 yılına (400\'e bölünemeyen bir sonraki yüzyıl yılı) kadar doğru olacak şekilde programlanmıştır.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
question: 'Ebedi takvim artık yılları nasıl bilir?',
|
|
13
|
+
answer: 'Mekanik program, 29 Şubat\'ı hesaba katan özel tasarlanmış bir kam ile 48 aylık (4 yıl) bir dişli kullanır. Mekanizma, 100\'e bölünebilen yılların, 400\'e de bölünmedikçe artık yıl olmadığını bilir. Çoğu ebedi takvim, bir günlük düzeltme gerektirecek olan 2100 yılına kadar doğrudur.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
question: 'Ebedi takvim ile yıllık takvim arasındaki fark nedir?',
|
|
17
|
+
answer: 'Yıllık takvim yılda bir kez manuel düzeltme gerektirir (Şubat sonunda), oysa ebedi takvim artık yılları otomatik olarak işler ve onlarca yıl boyunca doğru şekilde devam eder. Ebedi takvimler mekanik olarak önemli ölçüde daha karmaşıktır.',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const howTo = [
|
|
22
|
+
{
|
|
23
|
+
name: 'Tarihi ilerletin',
|
|
24
|
+
text: 'Takvimi ilerletmek için G (gün), A (ay) ve Y (yıl) düğmelerini kullanın. Tarih kolunun hareketini ve ay penceresinin değişimini izleyin.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Artık yıl geçişlerini gözlemleyin',
|
|
28
|
+
text: '29\'dan 1 Mart\'a atlamayı görmek için bir artık yılın (ör. 2024) Şubat ayını geçin. 28\'den 1 Mart\'a atlamayı görmek için artık olmayan bir yılı deneyin.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Otomatik oynatmayı kullanın',
|
|
32
|
+
text: 'Takvimin otomatik olarak ilerlemesi için play\'e basın. Bu, ay uzunluklarının tam döngüsünü ve ay evresi ilerlemesini ortaya çıkarır.',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const title = 'Ebedi Takvim Simülatörü: Etkileşimli Saat Komplikasyonu';
|
|
37
|
+
|
|
38
|
+
export const content: ToolLocaleContent<PerpetualCalendarUI> = {
|
|
39
|
+
slug: 'ebedi-takvim',
|
|
40
|
+
title,
|
|
41
|
+
description: 'Ebedi takvim saat komplikasyonunun mekanik dehasını keşfedin. Animasyonlu bir kadranla tarih, gün, ay, artık yıl döngüsü ve ay evresini görselleştirin.',
|
|
42
|
+
ui: {
|
|
43
|
+
title: 'Ebedi Takvim Simülatörü',
|
|
44
|
+
dateLabel: 'Tarih',
|
|
45
|
+
dayLabel: 'Gün',
|
|
46
|
+
monthLabel: 'Ay',
|
|
47
|
+
yearLabel: 'Yıl',
|
|
48
|
+
leapYearLabel: 'Artık Yıl',
|
|
49
|
+
moonPhaseLabel: 'Ay Evresi',
|
|
50
|
+
weekdayLabel: 'Hafta Günü',
|
|
51
|
+
advanceDay: 'Gün İlerlet',
|
|
52
|
+
advanceMonth: 'Ay İlerlet',
|
|
53
|
+
advanceYear: 'Yıl İlerlet',
|
|
54
|
+
autoPlay: 'Otomatik',
|
|
55
|
+
resetBtn: 'Bugün',
|
|
56
|
+
dayNames: 'Pazar,Pazartesi,Salı,Çarşamba,Perşembe,Cuma,Cumartesi',
|
|
57
|
+
monthNames: 'Ocak,Şubat,Mart,Nisan,Mayıs,Haziran,Temmuz,Ağustos,Eylül,Ekim,Kasım,Aralık',
|
|
58
|
+
tipTitle: 'İpucu',
|
|
59
|
+
tipContent: 'Çoğu ebedi takvim saati, değişken uzunlukta çentiklere sahip 48 aylık bir program çarkı kullanır. Şubat en kısa çentiğe sahiptir (normal yıllarda 28 gün, artık yıllarda 29), 30 ve 31 günlük aylar ise giderek daha uzun çentiklere sahiptir.',
|
|
60
|
+
step1: 'Mekanizmanın ay sonu geçişlerini nasıl yönettiğini görmek için Şubat ayını gün gün geçin.',
|
|
61
|
+
step2: 'Ay evresi göstergesinin 29,5 günlük döngüsü boyunca ilerlemesini izleyin.',
|
|
62
|
+
step3: '4 yıllık döngüyü anlamak için artık yıl ve normal yıl Şubat geçişlerini karşılaştırın.',
|
|
63
|
+
},
|
|
64
|
+
seo: [
|
|
65
|
+
{ type: 'title', text: 'Ebedi Takvim Simülatörü: Etkileşimli Komplikasyon', level: 2 },
|
|
66
|
+
{ type: 'paragraph', html: '<strong>Ebedi takvim</strong>, haute horlogerie\'nin en prestijli komplikasyonlarından biridir. Bu etkileşimli simülatör, mekanik bir ebedi takvimin tarih, gün, ay, artık yıl ve ay evresini nasıl takip ettiğini görselleştirir — onlarca yıl boyunca manuel düzeltme gerektirmez. 48 aylık dişli programını keşfedin, Şubat geçişlerinin nasıl çalıştığını görün ve bu mikromekanik başyapıtlarına yerleştirilmiş Gregoryen takvim mantığını anlayın.' },
|
|
67
|
+
{ type: 'title', text: 'Ebedi takvim nasıl çalışır', level: 3 },
|
|
68
|
+
{ type: 'paragraph', html: 'Mekanik bir ebedi takvim, farklı uzunluktaki ayları temsil eden farklı derinliklerde çentiklere sahip bir <strong>program çarkı</strong> kullanır. Bir algılama kolu her çentiğe düşer; daha derin bir çentik kısa bir ayı (28-29 gün) işaret eder ve doğru gün sayısından sonra mekanizmayı bir sonraki ayın 1\'ine atlaması için tetikler. Bir <strong>48 aylık dişli</strong>, 29 Şubat için ek bir çentik ile 4 yıllık artık yıl döngüsünü yönetir. Program, yüzyıl yıllarının (ör. 2100) 400\'e bölünmedikçe artık yılı atladığını bilir.' },
|
|
69
|
+
{ type: 'title', text: 'Karşılaştırma: Ebedi vs Yıllık Takvim', level: 3 },
|
|
70
|
+
{
|
|
71
|
+
type: 'table', headers: ['Özellik', 'Yıllık Takvim', 'Ebedi Takvim'], rows: [
|
|
72
|
+
['Düzeltme gerektirir', 'Yılda bir kez (1 Mart)', 'Yüzyılda bir kez (2100)'],
|
|
73
|
+
['Artık yıl yönetimi', 'Manuel', 'Otomatik (4 yıllık kam)'],
|
|
74
|
+
['Ay tanıma', '30 vs 31 gün', 'Tam 28/29/30/31'],
|
|
75
|
+
['Karmaşıklık', 'Orta (~50 parça)', 'Çok yüksek (~200+ parça)'],
|
|
76
|
+
['Fiyat aralığı', '€3.000-15.000', '€20.000-500.000+'],
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{ type: 'diagnostic', variant: 'info', title: 'Etkileşimli Takvim Simülatörü', icon: 'mdi:calendar-month', badge: 'SAATÇİLİK', html: 'Bu araç, ebedi takvimli bir saatin takvim mekanizmasını simüle eder. Animasyonlu kadran, tarih kolunu, ay penceresini, gün alt kadranını, ay evresini ve artık yıl göstergesini gösterir. Günler, aylar veya yıllar boyunca ilerlemek için kontrolleri kullanın ve mekanik mantığı izleyin.' },
|
|
80
|
+
],
|
|
81
|
+
faq,
|
|
82
|
+
bibliography,
|
|
83
|
+
howTo,
|
|
84
|
+
schemas: buildSchemas(title, faq, howTo),
|
|
85
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { PerpetualCalendarUI } from '../entry';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import { buildSchemas } from '../helpers';
|
|
5
|
+
|
|
6
|
+
const faq = [
|
|
7
|
+
{
|
|
8
|
+
question: '什么是手表的万年历?',
|
|
9
|
+
answer: '万年历是一种机械式手表复杂功能,可自动显示正确的日期、星期、月份和月相,同时考虑不同长度的月份和闰年。它被编程为精确到2100年(下一个不能被400整除的世纪年)。',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
question: '万年历如何知道闰年?',
|
|
13
|
+
answer: '机械程序使用带有特殊设计凸轮的48个月(4年)齿轮,该凸轮考虑到2月29日。该机制知道能被100整除的年份不是闰年,除非它们也能被400整除。大多数万年历精确到2100年,届时将需要一天的修正。',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
question: '万年历和年历有什么区别?',
|
|
17
|
+
answer: '年历每年需要一次手动校正(2月底),而万年历自动处理闰年并持续正确运行数十年。万年历在机械上要复杂得多。',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const howTo = [
|
|
22
|
+
{
|
|
23
|
+
name: '前进日期',
|
|
24
|
+
text: '使用D(日)、M(月)和Y(年)按钮向前推进日历。观察日期指针移动和月份窗口变化。',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: '观察闰年过渡',
|
|
28
|
+
text: '在闰年(例如2024年)的2月中前进,查看从29日跳转到3月1日。尝试非闰年查看从28日跳转到3月1日。',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: '使用自动播放',
|
|
32
|
+
text: '按播放键让日历自动前进。这将显示完整的月份长度周期和月相进展。',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const title = '万年历模拟器:交互式手表复杂功能';
|
|
37
|
+
|
|
38
|
+
export const content: ToolLocaleContent<PerpetualCalendarUI> = {
|
|
39
|
+
slug: 'perpetual-calendar',
|
|
40
|
+
title,
|
|
41
|
+
description: '探索万年历手表复杂功能的机械天才。通过动画表盘可视化日期、星期、月份、闰年周期和月相。',
|
|
42
|
+
ui: {
|
|
43
|
+
title: '万年历模拟器',
|
|
44
|
+
dateLabel: '日期',
|
|
45
|
+
dayLabel: '日',
|
|
46
|
+
monthLabel: '月',
|
|
47
|
+
yearLabel: '年',
|
|
48
|
+
leapYearLabel: '闰年',
|
|
49
|
+
moonPhaseLabel: '月相',
|
|
50
|
+
weekdayLabel: '星期',
|
|
51
|
+
advanceDay: '前进一天',
|
|
52
|
+
advanceMonth: '前进一月',
|
|
53
|
+
advanceYear: '前进一年',
|
|
54
|
+
autoPlay: '自动',
|
|
55
|
+
resetBtn: '今天',
|
|
56
|
+
dayNames: '星期日,星期一,星期二,星期三,星期四,星期五,星期六',
|
|
57
|
+
monthNames: '一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月',
|
|
58
|
+
tipTitle: '小贴士',
|
|
59
|
+
tipContent: '大多数万年历手表使用带有可变长度凹口的48个月程序轮。二月的凹口最短(平年28天,闰年29天),而30天和31天的月份拥有逐渐变长的凹口。',
|
|
60
|
+
step1: '一天天经过二月,观察机制如何处理月末过渡。',
|
|
61
|
+
step2: '观察月相指示器经过其29.5天周期的进展。',
|
|
62
|
+
step3: '比较闰年和平年的二月过渡,以理解4年周期。',
|
|
63
|
+
},
|
|
64
|
+
seo: [
|
|
65
|
+
{ type: 'title', text: '万年历模拟器:交互式复杂功能', level: 2 },
|
|
66
|
+
{ type: 'paragraph', html: '<strong>万年历</strong>是高级制表中最负盛名的复杂功能之一。此交互式模拟器可视化机械万年历如何跟踪日期、星期、月份、闰年和月相——数十年无需手动校正。探索48个月齿轮程序,查看二月过渡如何工作,并理解融入这些微机械杰作中的格里高利历逻辑。' },
|
|
67
|
+
{ type: 'title', text: '万年历的工作原理', level: 3 },
|
|
68
|
+
{ type: 'paragraph', html: '机械万年历使用带有不同深度凹口的<strong>程序轮</strong>来表示不同长度的月份。感应杆落入每个凹口;较深的凹口表示短月(28-29天),触发机制在正确的天数后跳转到下个月的第1天。<strong>48个月齿轮</strong>处理4年的闰年周期,为2月29日设有一个额外凹口。程序知道世纪年(例如2100年)除非能被400整除,否则跳过闰年。' },
|
|
69
|
+
{ type: 'title', text: '比较:万年历 vs 年历', level: 3 },
|
|
70
|
+
{
|
|
71
|
+
type: 'table', headers: ['特点', '年历', '万年历'], rows: [
|
|
72
|
+
['需要调整', '每年一次(3月1日)', '每世纪一次(2100年)'],
|
|
73
|
+
['闰年处理', '手动', '自动(4年凸轮)'],
|
|
74
|
+
['月份识别', '30日 vs 31日', '完整28/29/30/31'],
|
|
75
|
+
['复杂度', '中等(约50个零件)', '非常高(200+个零件)'],
|
|
76
|
+
['价格范围', '€3,000-15,000', '€20,000-500,000+'],
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{ type: 'diagnostic', variant: 'info', title: '交互式日历模拟器', icon: 'mdi:calendar-month', badge: '钟表学', html: '此工具模拟万年历手表的日历机制。动画表盘显示日期指针、月份窗口、日子表盘、月相和闰年指示器。使用控制前进日、月或年,观察机械逻辑的运行。' },
|
|
80
|
+
],
|
|
81
|
+
faq,
|
|
82
|
+
bibliography,
|
|
83
|
+
howTo,
|
|
84
|
+
schemas: buildSchemas(title, faq, howTo),
|
|
85
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { perpetualCalendar } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const PERPETUAL_CALENDAR_TOOL: ToolDefinition = {
|
|
7
|
+
entry: perpetualCalendar,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
.tool-main-card {
|
|
2
|
+
background: var(--bg-surface);
|
|
3
|
+
border: 1px solid var(--border-color);
|
|
4
|
+
border-radius: 1.25rem;
|
|
5
|
+
margin: 0 auto;
|
|
6
|
+
padding: 1.25rem;
|
|
7
|
+
box-shadow: var(--shadow-base);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.calendar-layout {
|
|
11
|
+
display: flex;
|
|
12
|
+
gap: 1rem;
|
|
13
|
+
align-items: stretch;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.dial-section {
|
|
17
|
+
flex: 4;
|
|
18
|
+
position: relative;
|
|
19
|
+
min-width: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
canvas#calendar-canvas {
|
|
23
|
+
display: block;
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: auto;
|
|
26
|
+
border-radius: 0.75rem;
|
|
27
|
+
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0f0f1a 100%);
|
|
28
|
+
border: 1px solid rgba(212, 175, 55, 0.15);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.date-overlay {
|
|
32
|
+
position: absolute;
|
|
33
|
+
bottom: 18%;
|
|
34
|
+
left: 50%;
|
|
35
|
+
transform: translateX(-50%);
|
|
36
|
+
text-align: center;
|
|
37
|
+
pointer-events: none;
|
|
38
|
+
line-height: 1.2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.date-weekday {
|
|
42
|
+
display: block;
|
|
43
|
+
font-size: clamp(0.8rem, 2vw, 1.3rem);
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
color: var(--accent);
|
|
46
|
+
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
|
|
47
|
+
letter-spacing: 0.08em;
|
|
48
|
+
text-transform: uppercase;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.date-full {
|
|
52
|
+
display: block;
|
|
53
|
+
font-size: clamp(0.5rem, 1vw, 0.75rem);
|
|
54
|
+
color: var(--text-base);
|
|
55
|
+
opacity: 0.5;
|
|
56
|
+
margin-top: 0.2em;
|
|
57
|
+
font-variant-numeric: tabular-nums;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sidebar {
|
|
61
|
+
flex: 1;
|
|
62
|
+
min-width: 120px;
|
|
63
|
+
max-width: 180px;
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
gap: 1.25rem;
|
|
67
|
+
padding: 0.5rem 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.sidebar-group {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
gap: 0.5rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.sidebar-label {
|
|
77
|
+
font-size: 0.55rem;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
text-transform: uppercase;
|
|
80
|
+
letter-spacing: 0.08em;
|
|
81
|
+
color: var(--text-base);
|
|
82
|
+
opacity: 0.35;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.sidebar-buttons {
|
|
86
|
+
display: flex;
|
|
87
|
+
gap: 0.35rem;
|
|
88
|
+
flex-wrap: wrap;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.sidebar button {
|
|
92
|
+
min-width: 28px;
|
|
93
|
+
height: 28px;
|
|
94
|
+
padding: 0 0.45rem;
|
|
95
|
+
font-size: 0.65rem;
|
|
96
|
+
font-weight: 500;
|
|
97
|
+
border: 1px solid var(--border-base);
|
|
98
|
+
border-radius: 0.35rem;
|
|
99
|
+
background: var(--bg-page);
|
|
100
|
+
color: var(--text-base);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
transition: all 0.15s ease;
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.sidebar button:hover {
|
|
109
|
+
border-color: var(--accent);
|
|
110
|
+
color: var(--accent);
|
|
111
|
+
background: color-mix(in srgb, var(--accent) 6%, var(--bg-page));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.sidebar button:active {
|
|
115
|
+
transform: scale(0.95);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.sidebar-info {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
gap: 0.3rem;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.sidebar-row {
|
|
125
|
+
display: flex;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
align-items: center;
|
|
128
|
+
padding: 0.2rem 0;
|
|
129
|
+
border-bottom: 1px solid var(--border-base);
|
|
130
|
+
border-bottom-width: 0.5px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.sidebar-row:last-child {
|
|
134
|
+
border-bottom: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.sidebar-row .slbl {
|
|
138
|
+
font-size: 0.55rem;
|
|
139
|
+
text-transform: uppercase;
|
|
140
|
+
letter-spacing: 0.05em;
|
|
141
|
+
color: var(--text-base);
|
|
142
|
+
opacity: 0.4;
|
|
143
|
+
font-weight: 500;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.sidebar-row .sval {
|
|
147
|
+
font-size: 0.65rem;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
color: var(--text-base);
|
|
150
|
+
font-variant-numeric: tabular-nums;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@media (max-width: 520px) {
|
|
154
|
+
.calendar-layout {
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
}
|
|
157
|
+
.sidebar {
|
|
158
|
+
max-width: 100%;
|
|
159
|
+
flex-direction: row;
|
|
160
|
+
flex-wrap: wrap;
|
|
161
|
+
gap: 0.75rem;
|
|
162
|
+
padding: 0;
|
|
163
|
+
}
|
|
164
|
+
.sidebar-group {
|
|
165
|
+
flex-direction: row;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 0.4rem;
|
|
168
|
+
}
|
|
169
|
+
.sidebar-label {
|
|
170
|
+
margin-right: 0.25rem;
|
|
171
|
+
}
|
|
172
|
+
.sidebar-info {
|
|
173
|
+
flex-direction: row;
|
|
174
|
+
flex-wrap: wrap;
|
|
175
|
+
gap: 0.5rem;
|
|
176
|
+
}
|
|
177
|
+
.sidebar-row {
|
|
178
|
+
border-bottom: none;
|
|
179
|
+
gap: 0.3rem;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { perpetualCalendar } 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 = perpetualCalendar.i18n[locale] || perpetualCalendar.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,26 @@
|
|
|
1
|
+
let _ctx: CanvasRenderingContext2D;
|
|
2
|
+
let _isDark = true;
|
|
3
|
+
let _fontFam = 'system-ui, sans-serif';
|
|
4
|
+
|
|
5
|
+
export function getCtx() { return _ctx; }
|
|
6
|
+
export function isDark() { return _isDark; }
|
|
7
|
+
|
|
8
|
+
export function setCtx(ctx: CanvasRenderingContext2D) {
|
|
9
|
+
_ctx = ctx;
|
|
10
|
+
_fontFam = getComputedStyle(document.body).fontFamily || _fontFam;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function detectTheme() {
|
|
14
|
+
const bg = window.getComputedStyle(document.body).backgroundColor;
|
|
15
|
+
_isDark = !bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent' ? true : parseInt(bg.replace(/[^\d,]/g, '').split(',')[0]) < 128;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function c(h: string, l: string): string { return _isDark ? h : l; }
|
|
19
|
+
export function getFontFam() { return _fontFam; }
|
|
20
|
+
|
|
21
|
+
export const W = 600;
|
|
22
|
+
export const H = 600;
|
|
23
|
+
export const CX = 300;
|
|
24
|
+
export const CY = 300;
|
|
25
|
+
export const OUTER_R = 280;
|
|
26
|
+
export const INNER_R = 220;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { tourbillonVisualizer } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
interface Props { locale?: KnownLocale; }
|
|
6
|
+
const { locale = 'en' } = Astro.props as Props;
|
|
7
|
+
const loader = tourbillonVisualizer.i18n[locale] || tourbillonVisualizer.i18n.en;
|
|
8
|
+
const content = await loader?.();
|
|
9
|
+
if (!content) return null;
|
|
10
|
+
---
|
|
11
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{ name: 'Tourbillon - Wikipedia', url: 'https://en.wikipedia.org/wiki/Tourbillon' },
|
|
5
|
+
{ name: 'Breguet Tourbillon History', url: 'https://www.swatchgroup.com/en/services/archive/2021/breguet-inventor-tourbillon' },
|
|
6
|
+
{ name: 'Flying Tourbillon Explained', url: 'https://www.hautehorlogerie.org/zh/watches-and-culture/watchmaking-knowledge/encyclopedia/flying-tourbillon-' },
|
|
7
|
+
];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const canvas = document.getElementById('tourb-canvas') as HTMLCanvasElement;
|
|
2
|
+
const ctx = canvas.getContext('2d')!;
|
|
3
|
+
|
|
4
|
+
import { setCtx, detT, W, H, CX, CY } from './state';
|
|
5
|
+
import { drawBg, drawPlate, drawSecondScale, drawScrews } from './renderer/base';
|
|
6
|
+
import { drawCage } from './renderer/cage';
|
|
7
|
+
import { drawBalance, drawHairspring, drawPallet, drawEscapeWheel } from './renderer/esc';
|
|
8
|
+
import { calc } from './tourbillon';
|
|
9
|
+
|
|
10
|
+
setCtx(ctx);
|
|
11
|
+
|
|
12
|
+
const REF = 700;
|
|
13
|
+
|
|
14
|
+
let cageAngle = 0;
|
|
15
|
+
let balancePhase = 0;
|
|
16
|
+
let palletPhase = 0;
|
|
17
|
+
let escAngle = 0;
|
|
18
|
+
let flying = false;
|
|
19
|
+
let gyro = false;
|
|
20
|
+
let zoom = false;
|
|
21
|
+
let curBeat = 28800;
|
|
22
|
+
let speedM = 1;
|
|
23
|
+
let paused = false;
|
|
24
|
+
let highlight: string | null = null;
|
|
25
|
+
|
|
26
|
+
function resize() {
|
|
27
|
+
const p = canvas.parentElement!;
|
|
28
|
+
const dw = p.clientWidth;
|
|
29
|
+
const dh = dw;
|
|
30
|
+
const dpr = Math.min(devicePixelRatio || 1, 2);
|
|
31
|
+
canvas.style.width = dw + 'px';
|
|
32
|
+
canvas.style.height = dh + 'px';
|
|
33
|
+
canvas.width = dw * dpr;
|
|
34
|
+
canvas.height = dh * dpr;
|
|
35
|
+
ctx.setTransform(dw / REF * dpr, 0, 0, dh / REF * dpr, 0, 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function upUI() {
|
|
39
|
+
const b = calc(curBeat);
|
|
40
|
+
const f = (id: string, v: string) => { const e = document.getElementById(id); if (e) e.textContent = v; };
|
|
41
|
+
f('cage-info', Math.round(((cageAngle * 180 / Math.PI) % 360 + 360) % 360) + '\u00B0');
|
|
42
|
+
f('balance-info', b.hz + ' Hz');
|
|
43
|
+
f('escape-info', b.rpm + ' rpm');
|
|
44
|
+
f('pallet-info', b.bph + ' bph');
|
|
45
|
+
f('spring-info', b.hz + ' Hz');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tick() {
|
|
49
|
+
if (paused) return;
|
|
50
|
+
const b = calc(curBeat);
|
|
51
|
+
const dt = 1 * speedM;
|
|
52
|
+
cageAngle += 0.0015 * dt;
|
|
53
|
+
const balanceSpeed = 0.04 + b.hz * 0.03;
|
|
54
|
+
balancePhase += balanceSpeed * dt;
|
|
55
|
+
palletPhase += b.hz * 0.35 * dt;
|
|
56
|
+
escAngle += b.rpm * 0.0005 * dt;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function render() {
|
|
60
|
+
detT();
|
|
61
|
+
const b = calc(curBeat);
|
|
62
|
+
const cr = zoom ? 60 : 140;
|
|
63
|
+
|
|
64
|
+
ctx.save();
|
|
65
|
+
ctx.clearRect(0, 0, W, H);
|
|
66
|
+
drawBg();
|
|
67
|
+
drawPlate();
|
|
68
|
+
drawSecondScale();
|
|
69
|
+
drawScrews();
|
|
70
|
+
|
|
71
|
+
if (zoom) {
|
|
72
|
+
const zf = 2.5;
|
|
73
|
+
ctx.translate(CX, CY);
|
|
74
|
+
ctx.scale(zf, zf);
|
|
75
|
+
ctx.translate(-CX, -CY);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
drawCage(cageAngle, flying, gyro, zoom);
|
|
79
|
+
ctx.save();
|
|
80
|
+
ctx.beginPath();
|
|
81
|
+
ctx.arc(CX, CY, cr - 5, 0, Math.PI * 2);
|
|
82
|
+
ctx.clip();
|
|
83
|
+
drawHairspring(balancePhase, b.hz, highlight === 'hairspring');
|
|
84
|
+
drawBalance(balancePhase, highlight === 'balance');
|
|
85
|
+
drawPallet(palletPhase, highlight === 'pallet');
|
|
86
|
+
drawEscapeWheel(escAngle, highlight === 'escape');
|
|
87
|
+
ctx.restore();
|
|
88
|
+
ctx.restore();
|
|
89
|
+
upUI();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loop() { tick(); render(); requestAnimationFrame(loop); }
|
|
93
|
+
|
|
94
|
+
function switchOpt(opt: string, val: string) {
|
|
95
|
+
document.querySelectorAll(`[data-opt="${opt}"]`).forEach((b) => {
|
|
96
|
+
b.classList.toggle('active', (b as HTMLElement).dataset.val === val);
|
|
97
|
+
});
|
|
98
|
+
if (opt === 'type') { flying = val === 'flying'; gyro = val === 'gyro'; }
|
|
99
|
+
if (opt === 'speed') { speedM = parseFloat(val); paused = val === '0'; }
|
|
100
|
+
if (opt === 'beat') curBeat = parseInt(val);
|
|
101
|
+
if (opt === 'view') zoom = val === 'zoom';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function init() {
|
|
105
|
+
document.querySelectorAll('[data-opt]').forEach((b) => {
|
|
106
|
+
b.addEventListener('click', () => switchOpt(
|
|
107
|
+
(b as HTMLElement).dataset.opt!,
|
|
108
|
+
(b as HTMLElement).dataset.val!
|
|
109
|
+
));
|
|
110
|
+
});
|
|
111
|
+
document.querySelectorAll('.tourb-cell').forEach((cell) => {
|
|
112
|
+
const target = cell.getAttribute('data-highlight');
|
|
113
|
+
if (!target) return;
|
|
114
|
+
cell.addEventListener('mouseenter', () => { highlight = target; });
|
|
115
|
+
cell.addEventListener('mouseleave', () => { highlight = null; });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
new ResizeObserver(resize).observe(canvas.parentElement!);
|
|
120
|
+
resize();
|
|
121
|
+
init();
|
|
122
|
+
loop();
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
import TourbillonPanel from './components/TourbillonPanel.astro';
|
|
3
|
+
interface Props { ui: Record<string, string>; }
|
|
4
|
+
const { ui } = Astro.props;
|
|
5
|
+
---
|
|
6
|
+
<style is:global>
|
|
7
|
+
.tool-main-card {
|
|
8
|
+
background: var(--bg-surface);
|
|
9
|
+
border: 1px solid var(--border-color);
|
|
10
|
+
border-radius: 1.25rem;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 1.25rem;
|
|
13
|
+
box-shadow: var(--shadow-base);
|
|
14
|
+
}
|
|
15
|
+
.tourb-col {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
gap: 0.75rem;
|
|
19
|
+
}
|
|
20
|
+
.tourb-canvas {
|
|
21
|
+
width: 100%;
|
|
22
|
+
}
|
|
23
|
+
canvas#tourb-canvas {
|
|
24
|
+
display: block;
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: auto;
|
|
27
|
+
border-radius: 0.75rem;
|
|
28
|
+
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0f0f1a 100%);
|
|
29
|
+
border: 1px solid rgba(212,175,55,0.15);
|
|
30
|
+
}
|
|
31
|
+
.tourb-controls {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-wrap: wrap;
|
|
34
|
+
gap: 0.5rem;
|
|
35
|
+
padding: 0.75rem;
|
|
36
|
+
background: var(--bg-page);
|
|
37
|
+
border: 1px solid var(--border-base);
|
|
38
|
+
border-radius: 0.75rem;
|
|
39
|
+
}
|
|
40
|
+
.tourb-block {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 0.5rem;
|
|
44
|
+
padding: 0.35rem 0.6rem;
|
|
45
|
+
border: 1px solid rgba(212,175,55,0.12);
|
|
46
|
+
border-radius: 0.4rem;
|
|
47
|
+
background: color-mix(in srgb, rgba(212,175,55,0.03), var(--bg-surface));
|
|
48
|
+
}
|
|
49
|
+
.tourb-lbl {
|
|
50
|
+
font-size: 0.6rem;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
text-transform: uppercase;
|
|
53
|
+
letter-spacing: 0.08em;
|
|
54
|
+
color: rgba(212,175,55,0.8);
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
}
|
|
57
|
+
.tourb-btns {
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
gap: 0.15rem;
|
|
60
|
+
}
|
|
61
|
+
.tourb-btn {
|
|
62
|
+
padding: 0.25rem 0.5rem;
|
|
63
|
+
font-size: 0.7rem;
|
|
64
|
+
font-weight: 500;
|
|
65
|
+
border: 1px solid rgba(212,175,55,0.15);
|
|
66
|
+
border-radius: 0.3rem;
|
|
67
|
+
background: transparent;
|
|
68
|
+
color: var(--text-base);
|
|
69
|
+
opacity: 0.5;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
transition: all 0.15s ease;
|
|
72
|
+
}
|
|
73
|
+
.tourb-btn:hover {
|
|
74
|
+
border-color: var(--accent);
|
|
75
|
+
color: var(--accent);
|
|
76
|
+
opacity: 1;
|
|
77
|
+
}
|
|
78
|
+
.tourb-btn.active {
|
|
79
|
+
border-color: var(--accent);
|
|
80
|
+
color: var(--accent);
|
|
81
|
+
opacity: 1;
|
|
82
|
+
background: color-mix(in srgb, var(--accent) 10%, var(--bg-page));
|
|
83
|
+
box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 15%, transparent);
|
|
84
|
+
}
|
|
85
|
+
.tourb-cell-val {
|
|
86
|
+
font-size: 0.8rem;
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
color: var(--accent);
|
|
89
|
+
font-variant-numeric: tabular-nums;
|
|
90
|
+
}
|
|
91
|
+
.tourb-data {
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-wrap: wrap;
|
|
94
|
+
gap: 0.35rem;
|
|
95
|
+
padding: 0.6rem 0.75rem;
|
|
96
|
+
background: var(--bg-page);
|
|
97
|
+
border: 1px solid var(--border-base);
|
|
98
|
+
border-radius: 0.6rem;
|
|
99
|
+
}
|
|
100
|
+
.tourb-cell {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 0.4rem;
|
|
104
|
+
padding: 0.2rem 0.5rem;
|
|
105
|
+
border-radius: 0.3rem;
|
|
106
|
+
cursor: default;
|
|
107
|
+
transition: background 0.2s;
|
|
108
|
+
}
|
|
109
|
+
.tourb-cell:hover {
|
|
110
|
+
background: rgba(212,175,55,0.06);
|
|
111
|
+
}
|
|
112
|
+
.tourb-cell.highlight {
|
|
113
|
+
background: rgba(212,175,55,0.1);
|
|
114
|
+
box-shadow: 0 0 6px rgba(212,175,55,0.1);
|
|
115
|
+
}
|
|
116
|
+
.tourb-cell-lbl {
|
|
117
|
+
font-size: 0.6rem;
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
text-transform: uppercase;
|
|
120
|
+
letter-spacing: 0.06em;
|
|
121
|
+
color: var(--text-base);
|
|
122
|
+
opacity: 0.3;
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
125
|
+
<div class="tool-main-card" data-ui={JSON.stringify(ui)}><TourbillonPanel labels={ui} /></div>
|
|
126
|
+
<script src="./client.ts"></script>
|