@jjlmoya/utils-diy 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +110 -0
  3. package/src/category/i18n/es.ts +110 -0
  4. package/src/category/i18n/fr.ts +101 -0
  5. package/src/category/index.ts +25 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +57 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/balusterCalculator/bibliography.astro +14 -0
  25. package/src/tool/balusterCalculator/component.astro +605 -0
  26. package/src/tool/balusterCalculator/i18n/en.ts +264 -0
  27. package/src/tool/balusterCalculator/i18n/es.ts +264 -0
  28. package/src/tool/balusterCalculator/i18n/fr.ts +264 -0
  29. package/src/tool/balusterCalculator/index.ts +33 -0
  30. package/src/tool/balusterCalculator/seo.astro +15 -0
  31. package/src/tool/balusterCalculator/ui.ts +15 -0
  32. package/src/tool/clayCalculator/bibliography.astro +14 -0
  33. package/src/tool/clayCalculator/component.astro +731 -0
  34. package/src/tool/clayCalculator/i18n/en.ts +183 -0
  35. package/src/tool/clayCalculator/i18n/es.ts +183 -0
  36. package/src/tool/clayCalculator/i18n/fr.ts +183 -0
  37. package/src/tool/clayCalculator/index.ts +33 -0
  38. package/src/tool/clayCalculator/seo.astro +15 -0
  39. package/src/tool/clayCalculator/ui.ts +19 -0
  40. package/src/tool/concreteCalculator/bibliography.astro +13 -0
  41. package/src/tool/concreteCalculator/component.astro +1089 -0
  42. package/src/tool/concreteCalculator/i18n/en.ts +201 -0
  43. package/src/tool/concreteCalculator/i18n/es.ts +201 -0
  44. package/src/tool/concreteCalculator/i18n/fr.ts +201 -0
  45. package/src/tool/concreteCalculator/index.ts +28 -0
  46. package/src/tool/concreteCalculator/seo.astro +14 -0
  47. package/src/tool/concreteCalculator/ui.ts +35 -0
  48. package/src/tool/cutOptimizer/bibliography.astro +13 -0
  49. package/src/tool/cutOptimizer/component.astro +825 -0
  50. package/src/tool/cutOptimizer/i18n/en.ts +178 -0
  51. package/src/tool/cutOptimizer/i18n/es.ts +178 -0
  52. package/src/tool/cutOptimizer/i18n/fr.ts +178 -0
  53. package/src/tool/cutOptimizer/index.ts +28 -0
  54. package/src/tool/cutOptimizer/optimizer.ts +199 -0
  55. package/src/tool/cutOptimizer/seo.astro +14 -0
  56. package/src/tool/cutOptimizer/ui.ts +25 -0
  57. package/src/tool/drillCalculator/bibliography.astro +13 -0
  58. package/src/tool/drillCalculator/component.astro +1210 -0
  59. package/src/tool/drillCalculator/engine.ts +63 -0
  60. package/src/tool/drillCalculator/i18n/en.ts +137 -0
  61. package/src/tool/drillCalculator/i18n/es.ts +137 -0
  62. package/src/tool/drillCalculator/i18n/fr.ts +137 -0
  63. package/src/tool/drillCalculator/index.ts +28 -0
  64. package/src/tool/drillCalculator/seo.astro +14 -0
  65. package/src/tool/drillCalculator/ui.ts +41 -0
  66. package/src/tool/drillSharpener/bibliography.astro +13 -0
  67. package/src/tool/drillSharpener/component.astro +712 -0
  68. package/src/tool/drillSharpener/engine.ts +42 -0
  69. package/src/tool/drillSharpener/i18n/en.ts +121 -0
  70. package/src/tool/drillSharpener/i18n/es.ts +121 -0
  71. package/src/tool/drillSharpener/i18n/fr.ts +121 -0
  72. package/src/tool/drillSharpener/index.ts +31 -0
  73. package/src/tool/drillSharpener/seo.astro +14 -0
  74. package/src/tool/drillSharpener/ui.ts +53 -0
  75. package/src/tool/epoxyCalculator/bibliography.astro +14 -0
  76. package/src/tool/epoxyCalculator/component.astro +888 -0
  77. package/src/tool/epoxyCalculator/i18n/en.ts +195 -0
  78. package/src/tool/epoxyCalculator/i18n/es.ts +195 -0
  79. package/src/tool/epoxyCalculator/i18n/fr.ts +195 -0
  80. package/src/tool/epoxyCalculator/index.ts +33 -0
  81. package/src/tool/epoxyCalculator/seo.astro +15 -0
  82. package/src/tool/epoxyCalculator/ui.ts +22 -0
  83. package/src/tool/furnitureFit/bibliography.astro +13 -0
  84. package/src/tool/furnitureFit/component.astro +552 -0
  85. package/src/tool/furnitureFit/engine.ts +51 -0
  86. package/src/tool/furnitureFit/i18n/en.ts +152 -0
  87. package/src/tool/furnitureFit/i18n/es.ts +152 -0
  88. package/src/tool/furnitureFit/i18n/fr.ts +152 -0
  89. package/src/tool/furnitureFit/index.ts +28 -0
  90. package/src/tool/furnitureFit/seo.astro +14 -0
  91. package/src/tool/furnitureFit/ui.ts +21 -0
  92. package/src/tool/mortarCalculator/bibliography.astro +13 -0
  93. package/src/tool/mortarCalculator/component.astro +1139 -0
  94. package/src/tool/mortarCalculator/i18n/en.ts +219 -0
  95. package/src/tool/mortarCalculator/i18n/es.ts +205 -0
  96. package/src/tool/mortarCalculator/i18n/fr.ts +220 -0
  97. package/src/tool/mortarCalculator/index.ts +24 -0
  98. package/src/tool/mortarCalculator/seo.astro +14 -0
  99. package/src/tool/mortarCalculator/ui.ts +37 -0
  100. package/src/tool/passepartoutCalculator/bibliography.astro +13 -0
  101. package/src/tool/passepartoutCalculator/component.astro +741 -0
  102. package/src/tool/passepartoutCalculator/i18n/en.ts +178 -0
  103. package/src/tool/passepartoutCalculator/i18n/es.ts +178 -0
  104. package/src/tool/passepartoutCalculator/i18n/fr.ts +178 -0
  105. package/src/tool/passepartoutCalculator/index.ts +28 -0
  106. package/src/tool/passepartoutCalculator/seo.astro +14 -0
  107. package/src/tool/passepartoutCalculator/ui.ts +17 -0
  108. package/src/tool/stairCalculator/bibliography.astro +52 -0
  109. package/src/tool/stairCalculator/component.astro +766 -0
  110. package/src/tool/stairCalculator/engine.ts +128 -0
  111. package/src/tool/stairCalculator/i18n/en.ts +149 -0
  112. package/src/tool/stairCalculator/i18n/es.ts +149 -0
  113. package/src/tool/stairCalculator/i18n/fr.ts +149 -0
  114. package/src/tool/stairCalculator/index.ts +31 -0
  115. package/src/tool/stairCalculator/seo.astro +211 -0
  116. package/src/tool/stairCalculator/ui.ts +109 -0
  117. package/src/tool/thermalExpansionCalculator/bibliography.astro +13 -0
  118. package/src/tool/thermalExpansionCalculator/component.astro +771 -0
  119. package/src/tool/thermalExpansionCalculator/engine.ts +23 -0
  120. package/src/tool/thermalExpansionCalculator/i18n/en.ts +174 -0
  121. package/src/tool/thermalExpansionCalculator/i18n/es.ts +174 -0
  122. package/src/tool/thermalExpansionCalculator/i18n/fr.ts +174 -0
  123. package/src/tool/thermalExpansionCalculator/index.ts +28 -0
  124. package/src/tool/thermalExpansionCalculator/seo.astro +14 -0
  125. package/src/tool/thermalExpansionCalculator/ui.ts +41 -0
  126. package/src/tool/voltageDropCalculator/bibliography.astro +13 -0
  127. package/src/tool/voltageDropCalculator/component.astro +1022 -0
  128. package/src/tool/voltageDropCalculator/i18n/en.ts +155 -0
  129. package/src/tool/voltageDropCalculator/i18n/es.ts +155 -0
  130. package/src/tool/voltageDropCalculator/i18n/fr.ts +148 -0
  131. package/src/tool/voltageDropCalculator/index.ts +28 -0
  132. package/src/tool/voltageDropCalculator/seo.astro +14 -0
  133. package/src/tool/voltageDropCalculator/ui.ts +22 -0
  134. package/src/tools.ts +16 -0
  135. package/src/types.ts +72 -0
@@ -0,0 +1,888 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { KnownLocale } from '../../types';
4
+ import type { EpoxyCalculatorUI } from './ui';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ ui?: Record<string, unknown>;
9
+ }
10
+
11
+ const { ui } = Astro.props;
12
+ const t = (ui ?? {}) as EpoxyCalculatorUI;
13
+ ---
14
+
15
+ <div
16
+ class="ec-root"
17
+ data-shape-rect-label={t.shapeRectLabel}
18
+ data-shape-cylinder-label={t.shapeCylinderLabel}
19
+ data-shape-sphere-label={t.shapeSphereLabel}
20
+ >
21
+ <div class="ec-grid">
22
+ <div class="ec-panel-left">
23
+ <div class="ec-tabs">
24
+ <button class="ec-tab-btn" data-ec-shape="rect" data-ec-active="true">
25
+ <Icon name="mdi:cube-outline" class="ec-tab-icon" />
26
+ {t.shapeRect}
27
+ </button>
28
+ <button class="ec-tab-btn" data-ec-shape="cylinder">
29
+ <Icon name="mdi:cylinder" class="ec-tab-icon" />
30
+ {t.shapeCylinder}
31
+ </button>
32
+ <button class="ec-tab-btn" data-ec-shape="sphere">
33
+ <Icon name="mdi:circle-outline" class="ec-tab-icon" />
34
+ {t.shapeSphere}
35
+ </button>
36
+ </div>
37
+
38
+ <div class="ec-inputs-grid">
39
+ <div class="ec-input-group ec-rect-input">
40
+ <label class="ec-label">{t.labelLength}</label>
41
+ <input type="number" id="ec-length" value="30" class="ec-input" />
42
+ </div>
43
+ <div class="ec-input-group ec-rect-input">
44
+ <label class="ec-label">{t.labelWidth}</label>
45
+ <input type="number" id="ec-width" value="20" class="ec-input" />
46
+ </div>
47
+ <div class="ec-input-group ec-cylinder-input ec-hidden">
48
+ <label class="ec-label">{t.labelDiameter}</label>
49
+ <input type="number" id="ec-diameter" value="15" class="ec-input" />
50
+ </div>
51
+ <div class="ec-input-group ec-sphere-input ec-hidden">
52
+ <label class="ec-label">{t.labelDiameter}</label>
53
+ <input type="number" id="ec-sphere-diameter" value="10" class="ec-input" />
54
+ </div>
55
+ <div class="ec-input-group ec-depth-input">
56
+ <label class="ec-label">{t.labelDepth}</label>
57
+ <input type="number" id="ec-depth" value="5" class="ec-input" />
58
+ </div>
59
+ </div>
60
+
61
+ <div class="ec-divider"></div>
62
+
63
+ <div class="ec-ratio-section">
64
+ <div class="ec-ratio-header">
65
+ <span class="ec-label">{t.labelRatio}</span>
66
+ <span id="ec-ratio-display" class="ec-ratio-badge">2:1</span>
67
+ </div>
68
+ <div class="ec-slider-wrapper">
69
+ <input type="range" id="ec-ratio-slider" min="1" max="5" step="0.5" value="2" class="ec-slider" />
70
+ <div class="ec-ratio-marks">
71
+ <span>1:1</span><span>2:1</span><span>3:1</span><span>4:1</span><span>5:1</span>
72
+ </div>
73
+ </div>
74
+ <label class="ec-custom-check-row">
75
+ <input type="checkbox" id="ec-custom-check" class="ec-checkbox" />
76
+ <span>{t.labelCustomRatio}</span>
77
+ </label>
78
+ <div id="ec-custom-inputs" class="ec-custom-grid ec-hidden">
79
+ <div>
80
+ <label class="ec-label">{t.labelResinA}</label>
81
+ <input type="number" id="ec-ratio-a" value="100" class="ec-input ec-input-sm" />
82
+ </div>
83
+ <div>
84
+ <label class="ec-label">{t.labelHardenerB}</label>
85
+ <input type="number" id="ec-ratio-b" value="50" class="ec-input ec-input-sm" />
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="ec-waste-row">
91
+ <div class="ec-waste-info">
92
+ <div class="ec-waste-icon">
93
+ <Icon name="mdi:flask-outline" />
94
+ </div>
95
+ <div>
96
+ <p class="ec-waste-title">{t.labelWasteTitle}</p>
97
+ <p class="ec-waste-desc">{t.labelWasteDesc}</p>
98
+ </div>
99
+ </div>
100
+ <label class="ec-toggle">
101
+ <input type="checkbox" id="ec-waste-margin" checked class="ec-toggle-input" />
102
+ <span class="ec-toggle-track"></span>
103
+ </label>
104
+ </div>
105
+ </div>
106
+
107
+ <div class="ec-panel-right">
108
+ <div class="ec-result-card">
109
+ <div class="ec-result-blob"></div>
110
+ <div class="ec-result-inner">
111
+ <div>
112
+ <p class="ec-result-label">{t.labelTotalVolume}</p>
113
+ <div class="ec-result-total">
114
+ <span id="ec-total-volume" class="ec-total-number">0</span>
115
+ <span class="ec-total-unit">ml</span>
116
+ </div>
117
+ </div>
118
+ <div class="ec-parts-grid">
119
+ <div class="ec-part-card ec-part-a">
120
+ <p class="ec-part-label">{t.labelPartA}</p>
121
+ <p id="ec-part-a" class="ec-part-value">0 ml</p>
122
+ </div>
123
+ <div class="ec-part-card ec-part-b">
124
+ <p class="ec-part-label">{t.labelPartB}</p>
125
+ <p id="ec-part-b" class="ec-part-value">0 ml</p>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <div class="ec-viz-card">
132
+ <p class="ec-viz-label">{t.labelVisualization}</p>
133
+ <div class="ec-visual-stage">
134
+ <div id="ec-visual-shape" class="ec-visual-shape ec-shape-rect">
135
+ <div id="ec-liquid-fill" class="ec-liquid-fill">
136
+ <div class="ec-liquid-shimmer"></div>
137
+ <div class="ec-bubble ec-bubble-1"></div>
138
+ <div class="ec-bubble ec-bubble-2"></div>
139
+ </div>
140
+ </div>
141
+ <div id="ec-visual-label" class="ec-shape-label">{t.shapeRectLabel}</div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <script>
149
+ let ecShape = 'rect';
150
+ let ecCustomRatio = false;
151
+ let ecLengthEl: HTMLInputElement | null = null;
152
+ let ecWidthEl: HTMLInputElement | null = null;
153
+ let ecDepthEl: HTMLInputElement | null = null;
154
+ let ecDiameterEl: HTMLInputElement | null = null;
155
+ let ecSphereDiamEl: HTMLInputElement | null = null;
156
+ let ecRatioSlider: HTMLInputElement | null = null;
157
+ let ecCustomA: HTMLInputElement | null = null;
158
+ let ecCustomB: HTMLInputElement | null = null;
159
+ let ecWasteMargin: HTMLInputElement | null = null;
160
+ let ecTotalEl: HTMLElement | null = null;
161
+ let ecPartAEl: HTMLElement | null = null;
162
+ let ecPartBEl: HTMLElement | null = null;
163
+ let ecLiquidFill: HTMLElement | null = null;
164
+ let ecVisualShape: HTMLElement | null = null;
165
+ let ecVisualLabel: HTMLElement | null = null;
166
+ let ecRatioDisplay: HTMLElement | null = null;
167
+ let ecRoot: HTMLElement | null = null;
168
+
169
+ function parseInput(el: HTMLInputElement | null, fallback = 0): number {
170
+ return parseFloat(el?.value ?? '') || fallback;
171
+ }
172
+
173
+ function getVolume(): number {
174
+ if (ecShape === 'rect') {
175
+ return parseInput(ecLengthEl) * parseInput(ecWidthEl) * parseInput(ecDepthEl);
176
+ }
177
+ if (ecShape === 'cylinder') {
178
+ const r = parseInput(ecDiameterEl) / 2;
179
+ return Math.PI * r * r * parseInput(ecDepthEl);
180
+ }
181
+ const r = parseInput(ecSphereDiamEl) / 2;
182
+ return (4 / 3) * Math.PI * r * r * r;
183
+ }
184
+
185
+ function getRatio(): { rA: number; rB: number } {
186
+ if (ecCustomRatio) {
187
+ return { rA: parseInput(ecCustomA, 1), rB: parseInput(ecCustomB, 1) };
188
+ }
189
+ return { rA: parseInput(ecRatioSlider, 2), rB: 1 };
190
+ }
191
+
192
+ function animateValue(el: HTMLElement, end: number) {
193
+ const start = parseFloat(el.textContent ?? '0') || 0;
194
+ const startTs = performance.now();
195
+ const duration = 500;
196
+ const step = (ts: number) => {
197
+ const p = Math.min((ts - startTs) / duration, 1);
198
+ el.textContent = String(Math.floor(p * (end - start) + start));
199
+ if (p < 1) requestAnimationFrame(step);
200
+ };
201
+ requestAnimationFrame(step);
202
+ }
203
+
204
+ function updateUI(total: number, partA: number, partB: number) {
205
+ if (ecTotalEl) animateValue(ecTotalEl, total);
206
+ if (ecPartAEl) ecPartAEl.textContent = `${Math.round(partA)} ml`;
207
+ if (ecPartBEl) ecPartBEl.textContent = `${Math.round(partB)} ml`;
208
+ if (ecLiquidFill) ecLiquidFill.style.height = '85%';
209
+ }
210
+
211
+ function calculate() {
212
+ let volume = getVolume();
213
+ if (ecWasteMargin?.checked) volume *= 1.05;
214
+ const { rA, rB } = getRatio();
215
+ const total = rA + rB;
216
+ if (!total) return;
217
+ updateUI(volume, (volume / total) * rA, (volume / total) * rB);
218
+ }
219
+
220
+ function updateShapeBtns(root: HTMLElement, shape: string) {
221
+ root.querySelectorAll('[data-ec-shape]').forEach((btn) => {
222
+ const isActive = btn.getAttribute('data-ec-shape') === shape;
223
+ (btn as HTMLElement).dataset.ecActive = isActive ? 'true' : 'false';
224
+ });
225
+ }
226
+
227
+ function showInputs(root: HTMLElement, shape: string) {
228
+ root.querySelectorAll('.ec-rect-input').forEach((el) => el.classList.toggle('ec-hidden', shape !== 'rect'));
229
+ root.querySelectorAll('.ec-cylinder-input').forEach((el) => el.classList.toggle('ec-hidden', shape !== 'cylinder'));
230
+ root.querySelectorAll('.ec-sphere-input').forEach((el) => el.classList.toggle('ec-hidden', shape !== 'sphere'));
231
+ root.querySelectorAll('.ec-depth-input').forEach((el) => el.classList.toggle('ec-hidden', shape === 'sphere'));
232
+ }
233
+
234
+ function updateVisual(shape: string) {
235
+ if (!ecVisualShape || !ecVisualLabel || !ecRoot) return;
236
+ ecVisualShape.className = `ec-visual-shape ec-shape-${shape}`;
237
+ const key = `dataShape${shape.charAt(0).toUpperCase()}${shape.slice(1)}Label` as keyof typeof ecRoot.dataset;
238
+ ecVisualLabel.textContent = ecRoot.dataset[key] ?? shape;
239
+ }
240
+
241
+ function setShape(root: HTMLElement, shape: string) {
242
+ ecShape = shape;
243
+ updateShapeBtns(root, shape);
244
+ showInputs(root, shape);
245
+ updateVisual(shape);
246
+ calculate();
247
+ }
248
+
249
+ function attachShapeListeners(root: HTMLElement) {
250
+ root.querySelectorAll('[data-ec-shape]').forEach((btn) => {
251
+ btn.addEventListener('click', () => {
252
+ setShape(root, btn.getAttribute('data-ec-shape') ?? 'rect');
253
+ });
254
+ });
255
+ }
256
+
257
+ function attachCustomRatioListener(root: HTMLElement) {
258
+ const customCheck = root.querySelector<HTMLInputElement>('#ec-custom-check');
259
+ customCheck?.addEventListener('change', () => {
260
+ ecCustomRatio = customCheck.checked;
261
+ root.querySelector('#ec-custom-inputs')?.classList.toggle('ec-hidden', !ecCustomRatio);
262
+ const sliderWrapper = root.querySelector('.ec-slider-wrapper');
263
+ sliderWrapper?.classList.toggle('ec-disabled', ecCustomRatio);
264
+ if (ecRatioSlider) ecRatioSlider.disabled = ecCustomRatio;
265
+ calculate();
266
+ });
267
+ }
268
+
269
+ function attachRatioSliderListener(root: HTMLElement) {
270
+ root.querySelector<HTMLInputElement>('#ec-ratio-slider')?.addEventListener('input', (e) => {
271
+ const val = (e.target as HTMLInputElement).value;
272
+ if (ecRatioDisplay) ecRatioDisplay.textContent = `${val}:1`;
273
+ calculate();
274
+ });
275
+ }
276
+
277
+ function attachInputListeners(root: HTMLElement) {
278
+ const inputIds = ['#ec-length', '#ec-width', '#ec-depth', '#ec-diameter', '#ec-sphere-diameter', '#ec-ratio-a', '#ec-ratio-b', '#ec-waste-margin'];
279
+ inputIds.forEach((id) => {
280
+ root.querySelector(id)?.addEventListener('input', calculate);
281
+ });
282
+ }
283
+
284
+ function initEpoxyCalculator(root: HTMLElement) {
285
+ ecRoot = root;
286
+ ecLengthEl = root.querySelector('#ec-length');
287
+ ecWidthEl = root.querySelector('#ec-width');
288
+ ecDepthEl = root.querySelector('#ec-depth');
289
+ ecDiameterEl = root.querySelector('#ec-diameter');
290
+ ecSphereDiamEl = root.querySelector('#ec-sphere-diameter');
291
+ ecRatioSlider = root.querySelector('#ec-ratio-slider');
292
+ ecCustomA = root.querySelector('#ec-ratio-a');
293
+ ecCustomB = root.querySelector('#ec-ratio-b');
294
+ ecWasteMargin = root.querySelector('#ec-waste-margin');
295
+ ecTotalEl = root.querySelector('#ec-total-volume');
296
+ ecPartAEl = root.querySelector('#ec-part-a');
297
+ ecPartBEl = root.querySelector('#ec-part-b');
298
+ ecLiquidFill = root.querySelector('#ec-liquid-fill');
299
+ ecVisualShape = root.querySelector('#ec-visual-shape');
300
+ ecVisualLabel = root.querySelector('#ec-visual-label');
301
+ ecRatioDisplay = root.querySelector('#ec-ratio-display');
302
+
303
+ attachShapeListeners(root);
304
+ attachCustomRatioListener(root);
305
+ attachRatioSliderListener(root);
306
+ attachInputListeners(root);
307
+ calculate();
308
+ }
309
+
310
+ function init() {
311
+ document.querySelectorAll<HTMLElement>('.ec-root').forEach(initEpoxyCalculator);
312
+ }
313
+
314
+ document.addEventListener('astro:page-load', init);
315
+ init();
316
+ </script>
317
+
318
+ <style>
319
+ .ec-root {
320
+ width: 100%;
321
+ max-width: 72rem;
322
+ margin: 0 auto;
323
+ }
324
+
325
+ .ec-grid {
326
+ display: grid;
327
+ grid-template-columns: 1fr;
328
+ gap: 2rem;
329
+ }
330
+
331
+ @media (min-width: 1024px) {
332
+ .ec-grid {
333
+ grid-template-columns: 7fr 5fr;
334
+ }
335
+ }
336
+
337
+ .ec-panel-left {
338
+ background: rgba(255, 255, 255, 0.7);
339
+ backdrop-filter: blur(12px);
340
+ border: 1px solid rgba(255, 255, 255, 0.4);
341
+ border-radius: 1.5rem;
342
+ padding: 1.5rem;
343
+ box-shadow: 0 8px 32px rgba(120, 60, 0, 0.06);
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 1.5rem;
347
+ }
348
+
349
+ .ec-tabs {
350
+ display: flex;
351
+ gap: 0.375rem;
352
+ background: rgba(241, 245, 249, 0.5);
353
+ padding: 0.375rem;
354
+ border-radius: 1rem;
355
+ }
356
+
357
+ .ec-tab-btn {
358
+ flex: 1;
359
+ padding: 0.625rem 1rem;
360
+ border-radius: 0.75rem;
361
+ border: none;
362
+ background: transparent;
363
+ cursor: pointer;
364
+ font-size: 0.875rem;
365
+ font-weight: 600;
366
+ color: #64748b;
367
+ display: flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+ gap: 0.5rem;
371
+ transition: all 0.2s;
372
+ }
373
+
374
+ .ec-tab-btn[data-ec-active="true"] {
375
+ background: #1e293b;
376
+ color: #fff;
377
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
378
+ }
379
+
380
+ .ec-tab-icon {
381
+ font-size: 1.25rem;
382
+ }
383
+
384
+ .ec-inputs-grid {
385
+ display: grid;
386
+ grid-template-columns: 1fr 1fr;
387
+ gap: 1.5rem;
388
+ }
389
+
390
+ .ec-input-group {
391
+ display: flex;
392
+ flex-direction: column;
393
+ gap: 0.5rem;
394
+ }
395
+
396
+ .ec-label {
397
+ font-size: 0.7rem;
398
+ font-weight: 700;
399
+ text-transform: uppercase;
400
+ letter-spacing: 0.08em;
401
+ color: #94a3b8;
402
+ display: block;
403
+ }
404
+
405
+ .ec-input {
406
+ width: 100%;
407
+ background: #fff;
408
+ border: 1px solid #e2e8f0;
409
+ border-radius: 0.75rem;
410
+ padding: 0.75rem 1rem;
411
+ color: #334155;
412
+ font-size: 1rem;
413
+ outline: none;
414
+ transition: border-color 0.2s, box-shadow 0.2s;
415
+ box-sizing: border-box;
416
+ }
417
+
418
+ .ec-input:focus {
419
+ border-color: #fbbf24;
420
+ box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.15);
421
+ }
422
+
423
+ .ec-input-sm {
424
+ padding: 0.5rem 0.75rem;
425
+ font-size: 0.875rem;
426
+ }
427
+
428
+ .ec-hidden {
429
+ display: none;
430
+ }
431
+
432
+ .ec-divider {
433
+ height: 1px;
434
+ background: #f1f5f9;
435
+ }
436
+
437
+ .ec-ratio-section {
438
+ display: flex;
439
+ flex-direction: column;
440
+ gap: 0.75rem;
441
+ }
442
+
443
+ .ec-ratio-header {
444
+ display: flex;
445
+ justify-content: space-between;
446
+ align-items: center;
447
+ }
448
+
449
+ .ec-ratio-badge {
450
+ font-size: 0.75rem;
451
+ font-weight: 700;
452
+ color: #d97706;
453
+ background: #fef3c7;
454
+ padding: 0.2rem 0.6rem;
455
+ border-radius: 0.4rem;
456
+ }
457
+
458
+ .ec-slider-wrapper {
459
+ display: flex;
460
+ flex-direction: column;
461
+ gap: 0.5rem;
462
+ }
463
+
464
+ .ec-slider-wrapper.ec-disabled {
465
+ opacity: 0.4;
466
+ pointer-events: none;
467
+ }
468
+
469
+ .ec-slider {
470
+ width: 100%;
471
+ height: 8px;
472
+ background: #e2e8f0;
473
+ border-radius: 999px;
474
+ appearance: none;
475
+ cursor: pointer;
476
+ accent-color: #f59e0b;
477
+ outline: none;
478
+ }
479
+
480
+ .ec-slider::-webkit-slider-thumb {
481
+ appearance: none;
482
+ width: 18px;
483
+ height: 18px;
484
+ background: #f59e0b;
485
+ border-radius: 50%;
486
+ cursor: pointer;
487
+ border: 3px solid #fff;
488
+ box-shadow: 0 0 6px rgba(245, 158, 11, 0.4);
489
+ }
490
+
491
+ .ec-ratio-marks {
492
+ display: flex;
493
+ justify-content: space-between;
494
+ font-size: 0.625rem;
495
+ color: #94a3b8;
496
+ }
497
+
498
+ .ec-custom-check-row {
499
+ display: flex;
500
+ align-items: center;
501
+ gap: 0.5rem;
502
+ font-size: 0.875rem;
503
+ color: #475569;
504
+ cursor: pointer;
505
+ }
506
+
507
+ .ec-checkbox {
508
+ width: 16px;
509
+ height: 16px;
510
+ accent-color: #f59e0b;
511
+ cursor: pointer;
512
+ flex-shrink: 0;
513
+ }
514
+
515
+ .ec-custom-grid {
516
+ display: grid;
517
+ grid-template-columns: 1fr 1fr;
518
+ gap: 1rem;
519
+ }
520
+
521
+ .ec-waste-row {
522
+ display: flex;
523
+ align-items: center;
524
+ justify-content: space-between;
525
+ background: #f8fafc;
526
+ padding: 1rem;
527
+ border-radius: 0.75rem;
528
+ border: 1px solid #f1f5f9;
529
+ }
530
+
531
+ .ec-waste-info {
532
+ display: flex;
533
+ align-items: center;
534
+ gap: 0.75rem;
535
+ }
536
+
537
+ .ec-waste-icon {
538
+ padding: 0.5rem;
539
+ background: #fff;
540
+ border-radius: 0.5rem;
541
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
542
+ color: #f59e0b;
543
+ font-size: 1.25rem;
544
+ display: flex;
545
+ }
546
+
547
+ .ec-waste-title {
548
+ font-size: 0.875rem;
549
+ font-weight: 600;
550
+ color: #334155;
551
+ margin: 0;
552
+ }
553
+
554
+ .ec-waste-desc {
555
+ font-size: 0.75rem;
556
+ color: #94a3b8;
557
+ margin: 0;
558
+ }
559
+
560
+ .ec-toggle {
561
+ position: relative;
562
+ display: inline-flex;
563
+ align-items: center;
564
+ cursor: pointer;
565
+ flex-shrink: 0;
566
+ }
567
+
568
+ .ec-toggle-input {
569
+ position: absolute;
570
+ opacity: 0;
571
+ width: 0;
572
+ height: 0;
573
+ }
574
+
575
+ .ec-toggle-track {
576
+ width: 44px;
577
+ height: 24px;
578
+ background: #e2e8f0;
579
+ border-radius: 12px;
580
+ transition: background 0.2s;
581
+ position: relative;
582
+ }
583
+
584
+ .ec-toggle-track::after {
585
+ content: '';
586
+ position: absolute;
587
+ top: 2px;
588
+ left: 2px;
589
+ width: 20px;
590
+ height: 20px;
591
+ background: #fff;
592
+ border-radius: 50%;
593
+ transition: transform 0.2s;
594
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
595
+ }
596
+
597
+ .ec-toggle-input:checked ~ .ec-toggle-track {
598
+ background: #f59e0b;
599
+ }
600
+
601
+ .ec-toggle-input:checked ~ .ec-toggle-track::after {
602
+ transform: translateX(20px);
603
+ }
604
+
605
+ .ec-panel-right {
606
+ display: flex;
607
+ flex-direction: column;
608
+ gap: 1.5rem;
609
+ }
610
+
611
+ .ec-result-card {
612
+ background: linear-gradient(135deg, #f59e0b, #ea580c);
613
+ border-radius: 1.5rem;
614
+ padding: 0.25rem;
615
+ box-shadow: 0 12px 40px rgba(234, 88, 12, 0.25);
616
+ position: relative;
617
+ overflow: hidden;
618
+ color: #fff;
619
+ }
620
+
621
+ .ec-result-blob {
622
+ position: absolute;
623
+ top: 0;
624
+ right: 0;
625
+ width: 12rem;
626
+ height: 12rem;
627
+ background: rgba(255, 255, 255, 0.1);
628
+ border-radius: 50%;
629
+ filter: blur(40px);
630
+ transform: translate(30%, -30%);
631
+ pointer-events: none;
632
+ }
633
+
634
+ .ec-result-inner {
635
+ background: rgba(255, 255, 255, 0.1);
636
+ backdrop-filter: blur(4px);
637
+ border-radius: 1.25rem;
638
+ padding: 1.5rem;
639
+ display: flex;
640
+ flex-direction: column;
641
+ gap: 2rem;
642
+ position: relative;
643
+ z-index: 1;
644
+ }
645
+
646
+ .ec-result-label {
647
+ font-size: 0.75rem;
648
+ font-weight: 700;
649
+ text-transform: uppercase;
650
+ letter-spacing: 0.08em;
651
+ color: rgba(254, 243, 199, 0.9);
652
+ margin: 0 0 0.25rem;
653
+ }
654
+
655
+ .ec-result-total {
656
+ display: flex;
657
+ align-items: baseline;
658
+ gap: 0.5rem;
659
+ }
660
+
661
+ .ec-total-number {
662
+ font-size: 3rem;
663
+ font-weight: 700;
664
+ line-height: 1;
665
+ }
666
+
667
+ .ec-total-unit {
668
+ font-size: 1.25rem;
669
+ color: rgba(254, 243, 199, 0.8);
670
+ }
671
+
672
+ .ec-parts-grid {
673
+ display: grid;
674
+ grid-template-columns: 1fr 1fr;
675
+ gap: 1rem;
676
+ }
677
+
678
+ .ec-part-card {
679
+ border-radius: 0.75rem;
680
+ padding: 0.75rem;
681
+ backdrop-filter: blur(8px);
682
+ }
683
+
684
+ .ec-part-a {
685
+ background: rgba(0, 0, 0, 0.2);
686
+ }
687
+
688
+ .ec-part-b {
689
+ background: rgba(255, 255, 255, 0.2);
690
+ }
691
+
692
+ .ec-part-label {
693
+ font-size: 0.625rem;
694
+ font-weight: 700;
695
+ text-transform: uppercase;
696
+ letter-spacing: 0.06em;
697
+ color: rgba(254, 243, 199, 0.9);
698
+ margin: 0 0 0.25rem;
699
+ }
700
+
701
+ .ec-part-value {
702
+ font-size: 1.5rem;
703
+ font-weight: 700;
704
+ margin: 0;
705
+ }
706
+
707
+ .ec-viz-card {
708
+ background: #fff;
709
+ border-radius: 1.5rem;
710
+ padding: 1.5rem;
711
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
712
+ border: 1px solid #f1f5f9;
713
+ display: flex;
714
+ flex-direction: column;
715
+ align-items: center;
716
+ justify-content: center;
717
+ min-height: 280px;
718
+ position: relative;
719
+ overflow: hidden;
720
+ }
721
+
722
+ .ec-viz-label {
723
+ position: absolute;
724
+ top: 1.5rem;
725
+ left: 1.5rem;
726
+ font-size: 0.625rem;
727
+ font-weight: 700;
728
+ text-transform: uppercase;
729
+ letter-spacing: 0.1em;
730
+ color: #cbd5e1;
731
+ margin: 0;
732
+ }
733
+
734
+ .ec-visual-stage {
735
+ display: flex;
736
+ flex-direction: column;
737
+ align-items: center;
738
+ gap: 2.5rem;
739
+ perspective: 1000px;
740
+ }
741
+
742
+ .ec-visual-shape {
743
+ width: 8rem;
744
+ height: 8rem;
745
+ border: 4px solid rgba(30, 41, 59, 0.15);
746
+ background: #f8fafc;
747
+ overflow: hidden;
748
+ position: relative;
749
+ transition: all 0.5s ease;
750
+ }
751
+
752
+ .ec-shape-rect {
753
+ border-radius: 4px;
754
+ height: 5rem;
755
+ transform: rotateX(10deg) rotateY(10deg);
756
+ box-shadow: 10px 10px 30px rgba(0, 0, 0, 0.08);
757
+ }
758
+
759
+ .ec-shape-cylinder {
760
+ border-radius: 4px 4px 20px 20px;
761
+ width: 6rem;
762
+ transform: rotateX(5deg);
763
+ }
764
+
765
+ .ec-shape-sphere {
766
+ border-radius: 50%;
767
+ transform: scale(1.1);
768
+ box-shadow: inset -10px -10px 40px rgba(0, 0, 0, 0.08);
769
+ }
770
+
771
+ .ec-liquid-fill {
772
+ position: absolute;
773
+ bottom: 0;
774
+ left: 0;
775
+ width: 100%;
776
+ height: 0;
777
+ background: rgba(251, 191, 36, 0.8);
778
+ transition: height 0.7s ease-in-out;
779
+ box-shadow: 0 0 20px rgba(251, 191, 36, 0.4);
780
+ }
781
+
782
+ .ec-liquid-shimmer {
783
+ position: absolute;
784
+ top: 0;
785
+ left: 0;
786
+ width: 100%;
787
+ height: 8px;
788
+ background: rgba(255, 255, 255, 0.3);
789
+ animation: ec-pulse 2s ease-in-out infinite;
790
+ }
791
+
792
+ .ec-bubble {
793
+ position: absolute;
794
+ background: rgba(255, 255, 255, 0.4);
795
+ border-radius: 50%;
796
+ }
797
+
798
+ .ec-bubble-1 {
799
+ bottom: 8px;
800
+ left: 8px;
801
+ width: 8px;
802
+ height: 8px;
803
+ animation: ec-bounce 1s ease-in-out infinite;
804
+ }
805
+
806
+ .ec-bubble-2 {
807
+ bottom: 20px;
808
+ right: 16px;
809
+ width: 4px;
810
+ height: 4px;
811
+ animation: ec-pulse 1.5s ease-in-out infinite;
812
+ }
813
+
814
+ .ec-shape-label {
815
+ font-size: 0.75rem;
816
+ color: #94a3b8;
817
+ text-align: center;
818
+ }
819
+
820
+ @keyframes ec-pulse {
821
+ 0%, 100% { opacity: 1; }
822
+ 50% { opacity: 0.4; }
823
+ }
824
+
825
+ @keyframes ec-bounce {
826
+ 0%, 100% { transform: translateY(0); }
827
+ 50% { transform: translateY(-4px); }
828
+ }
829
+
830
+ :global(.theme-dark) .ec-panel-left {
831
+ background: rgba(15, 23, 42, 0.8);
832
+ border-color: rgba(51, 65, 85, 0.6);
833
+ }
834
+
835
+ :global(.theme-dark) .ec-tabs {
836
+ background: rgba(30, 41, 59, 0.6);
837
+ }
838
+
839
+ :global(.theme-dark) .ec-tab-btn {
840
+ color: #94a3b8;
841
+ }
842
+
843
+ :global(.theme-dark) .ec-tab-btn[data-ec-active="true"] {
844
+ background: #f59e0b;
845
+ color: #0f172a;
846
+ }
847
+
848
+ :global(.theme-dark) .ec-input {
849
+ background: #1e293b;
850
+ border-color: #334155;
851
+ color: #e2e8f0;
852
+ }
853
+
854
+ :global(.theme-dark) .ec-input:focus {
855
+ border-color: #f59e0b;
856
+ }
857
+
858
+ :global(.theme-dark) .ec-slider {
859
+ background: #334155;
860
+ }
861
+
862
+ :global(.theme-dark) .ec-custom-check-row {
863
+ color: #94a3b8;
864
+ }
865
+
866
+ :global(.theme-dark) .ec-waste-row {
867
+ background: rgba(30, 41, 59, 0.6);
868
+ border-color: rgba(51, 65, 85, 0.4);
869
+ }
870
+
871
+ :global(.theme-dark) .ec-waste-icon {
872
+ background: #1e293b;
873
+ }
874
+
875
+ :global(.theme-dark) .ec-waste-title {
876
+ color: #e2e8f0;
877
+ }
878
+
879
+ :global(.theme-dark) .ec-viz-card {
880
+ background: #1e293b;
881
+ border-color: #334155;
882
+ }
883
+
884
+ :global(.theme-dark) .ec-visual-shape {
885
+ background: #0f172a;
886
+ border-color: rgba(148, 163, 184, 0.15);
887
+ }
888
+ </style>