@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,1089 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { ConcreteCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ ui?: Record<string, unknown>;
7
+ }
8
+ const { ui } = Astro.props;
9
+ const t = (ui ?? {}) as ConcreteCalculatorUI;
10
+ ---
11
+
12
+ <div
13
+ class="cc-root"
14
+ data-cc-root
15
+ data-cc-advice-h200={t.adviceH200}
16
+ data-cc-advice-m40={t.adviceM40}
17
+ data-cc-advice-m80={t.adviceM80}
18
+ data-cc-advice-lime={t.adviceLimeH200}
19
+ data-cc-label-cement={t.resLabelCement}
20
+ data-cc-label-lime={t.resLabelLime}
21
+ data-cc-binder-cement={t.binderCementLabel}
22
+ data-cc-binder-lime={t.binderLimeLabel}
23
+ >
24
+ <div class="cc-card">
25
+
26
+ <div class="cc-mix-selector">
27
+ <button class="cc-mix-btn cc-active" data-cc-mix="H200" title={t.mixH200Hint}>
28
+ <Icon name="mdi:mortar-pestle-plus" />
29
+ <span>{t.mixH200Label}</span>
30
+ </button>
31
+ <button class="cc-mix-btn" data-cc-mix="M40" title={t.mixM40Hint}>
32
+ <Icon name="mdi:wall" />
33
+ <span>{t.mixM40Label}</span>
34
+ </button>
35
+ <button class="cc-mix-btn" data-cc-mix="M80" title={t.mixM80Hint}>
36
+ <Icon name="mdi:floor-plan" />
37
+ <span>{t.mixM80Label}</span>
38
+ </button>
39
+ </div>
40
+
41
+ <div class="cc-volume-control">
42
+ <div class="cc-mode-selector">
43
+ <button class="cc-mode-btn cc-active" data-cc-mode="dims">
44
+ <Icon name="mdi:ruler" />
45
+ <span>{t.modeDimsLabel}</span>
46
+ </button>
47
+ <button class="cc-mode-btn" data-cc-mode="direct">
48
+ <Icon name="mdi:cube-outline" />
49
+ <span>{t.modeDirectLabel}</span>
50
+ </button>
51
+ </div>
52
+
53
+ <div class="cc-dims-panel cc-active" data-cc-mode-panel="dims">
54
+ <div class="cc-dim-inputs">
55
+ <div class="cc-dim-field">
56
+ <label>{t.labelLength}</label>
57
+ <div class="cc-dim-input-wrap">
58
+ <input type="number" data-cc-length data-cc-dim value="5" step="0.5" />
59
+ <span class="cc-dim-unit">m</span>
60
+ </div>
61
+ </div>
62
+ <div class="cc-dim-field">
63
+ <label>{t.labelWidth}</label>
64
+ <div class="cc-dim-input-wrap">
65
+ <input type="number" data-cc-width data-cc-dim value="3" step="0.5" />
66
+ <span class="cc-dim-unit">m</span>
67
+ </div>
68
+ </div>
69
+ <div class="cc-dim-field">
70
+ <label>{t.labelThickness}</label>
71
+ <div class="cc-dim-input-wrap">
72
+ <input type="number" data-cc-thickness data-cc-dim value="10" step="1" />
73
+ <span class="cc-dim-unit">cm</span>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <div class="cc-direct-panel" data-cc-mode-panel="direct">
80
+ <div class="cc-input-header">
81
+ <label>{t.labelVolume}</label>
82
+ </div>
83
+ <div class="cc-vol-input-wrap">
84
+ <input type="number" data-cc-volume value="1.5" step="0.1" min="0" />
85
+ <span class="cc-unit-tag">m³</span>
86
+ </div>
87
+ </div>
88
+
89
+ <div class="cc-binder-selector">
90
+ <div class="cc-binder-option cc-active" data-cc-binder="cement">
91
+ <div class="cc-binder-circle"><div class="cc-binder-inner"></div></div>
92
+ <span class="cc-binder-label">{t.binderCementLabel}</span>
93
+ </div>
94
+ <div class="cc-binder-option" data-cc-binder="lime">
95
+ <div class="cc-binder-circle"><div class="cc-binder-inner"></div></div>
96
+ <span class="cc-binder-label">{t.binderLimeLabel}</span>
97
+ </div>
98
+ </div>
99
+
100
+ <div class="cc-summary">
101
+ <span class="cc-summary-label">{t.summaryLabel}</span>
102
+ <div class="cc-summary-value-wrap">
103
+ <span class="cc-summary-value" data-cc-summary-vol>1.50</span>
104
+ <span class="cc-summary-unit">m³</span>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="cc-results-grid">
110
+ <div class="cc-result-card">
111
+ <Icon name="mdi:cube-outline" />
112
+ <div class="cc-result-info">
113
+ <span class="cc-result-value" data-cc-res-cement>0</span>
114
+ <span class="cc-result-unit">{t.resUnitSacks}</span>
115
+ </div>
116
+ <span class="cc-result-label" data-cc-res-binder-label>{t.resLabelCement}</span>
117
+ </div>
118
+ <div class="cc-result-card">
119
+ <Icon name="mdi:shovel" />
120
+ <div class="cc-result-info">
121
+ <span class="cc-result-value" data-cc-res-sand>0</span>
122
+ <span class="cc-result-unit">{t.resUnitShovels}</span>
123
+ </div>
124
+ <span class="cc-result-label">{t.resLabelSand}</span>
125
+ </div>
126
+ <div class="cc-result-card">
127
+ <Icon name="mdi:water-outline" />
128
+ <div class="cc-result-info">
129
+ <span class="cc-result-value" data-cc-res-water>0</span>
130
+ <span class="cc-result-unit">{t.resUnitLitres}</span>
131
+ </div>
132
+ <span class="cc-result-label">{t.resLabelWater}</span>
133
+ </div>
134
+ </div>
135
+
136
+ <div class="cc-proportions-banner">
137
+ <div class="cc-proportion-header">
138
+ <Icon name="mdi:information-outline" />
139
+ <span>{t.proportionsTitle}</span>
140
+ </div>
141
+ <div class="cc-proportion-bar">
142
+ <div class="cc-bar-segment cc-seg-cement" data-cc-prop-cement style="width: 15%;"></div>
143
+ <div class="cc-bar-segment cc-seg-sand" data-cc-prop-sand style="width: 40%;"></div>
144
+ <div class="cc-bar-segment cc-seg-gravel" data-cc-prop-gravel style="width: 45%;"></div>
145
+ </div>
146
+ <div class="cc-proportion-labels">
147
+ <div class="cc-prop-label">
148
+ <span class="cc-dot cc-seg-cement"></span>
149
+ <span data-cc-prop-binder-label>{t.binderCementLabel}</span>
150
+ </div>
151
+ <div class="cc-prop-label">
152
+ <span class="cc-dot cc-seg-sand"></span>
153
+ <span>{t.propLabelSand}</span>
154
+ </div>
155
+ <div class="cc-prop-label" data-cc-label-gravel>
156
+ <span class="cc-dot cc-seg-gravel"></span>
157
+ <span>{t.propLabelGravel}</span>
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <div class="cc-info-banner">
163
+ <Icon name="mdi:alert-circle-outline" />
164
+ <p data-cc-mix-advice></p>
165
+ </div>
166
+
167
+ <div class="cc-price-card">
168
+ <span class="cc-price-label">{t.priceTotalLabel}</span>
169
+ <div class="cc-price-value-wrap">
170
+ <span class="cc-price-value" data-cc-res-total-price>0</span>
171
+ <span class="cc-price-unit">€</span>
172
+ </div>
173
+ <div class="cc-price-inputs-grid">
174
+ <div class="cc-price-input-field">
175
+ <label>
176
+ {t.priceSackLabel}
177
+ <span data-cc-binder-type>{t.binderCementLabel}</span> (25kg)
178
+ </label>
179
+ <div class="cc-editable-price-wrap">
180
+ <input
181
+ type="number"
182
+ data-cc-price-binder
183
+ class="cc-editable-price"
184
+ value="5.50"
185
+ step="0.1"
186
+ />
187
+ <span class="cc-editable-unit">€</span>
188
+ </div>
189
+ </div>
190
+ <div class="cc-price-input-field">
191
+ <label>{t.priceSandLabel}</label>
192
+ <div class="cc-editable-price-wrap">
193
+ <input
194
+ type="number"
195
+ data-cc-price-sand
196
+ class="cc-editable-price"
197
+ value="35.00"
198
+ step="1"
199
+ />
200
+ <span class="cc-editable-unit">€</span>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ <p class="cc-price-disclaimer">{t.priceDisclaimer}</p>
205
+ </div>
206
+
207
+ </div>
208
+ </div>
209
+
210
+ <script>
211
+ interface CcDosage {
212
+ cementSacks: number;
213
+ sandPaladas: number;
214
+ gravelPaladas: number;
215
+ waterLitros: number;
216
+ ratio: string;
217
+ parts: { cement: number; sand: number; gravel: number };
218
+ }
219
+
220
+ interface CcState {
221
+ mix: string;
222
+ mode: string;
223
+ binder: string;
224
+ }
225
+
226
+ const CC_DOSAGES: Record<string, CcDosage> = {
227
+ H200: {
228
+ cementSacks: 14, sandPaladas: 180, gravelPaladas: 240, waterLitros: 180,
229
+ ratio: '1 : 2.5 : 3', parts: { cement: 15, sand: 40, gravel: 45 },
230
+ },
231
+ M40: {
232
+ cementSacks: 11, sandPaladas: 400, gravelPaladas: 0, waterLitros: 160,
233
+ ratio: '1 : 4', parts: { cement: 20, sand: 80, gravel: 0 },
234
+ },
235
+ M80: {
236
+ cementSacks: 16, sandPaladas: 320, gravelPaladas: 0, waterLitros: 190,
237
+ ratio: '1 : 3', parts: { cement: 25, sand: 75, gravel: 0 },
238
+ },
239
+ };
240
+
241
+ function ccNum(el: Element | null): number {
242
+ if (!(el instanceof HTMLInputElement)) return 0;
243
+ return parseFloat(el.value) || 0;
244
+ }
245
+
246
+ function ccAttr(root: Element, key: string): string {
247
+ return root.getAttribute(key) ?? '';
248
+ }
249
+
250
+ function ccSetText(el: Element | null, v: string): void {
251
+ if (el) el.textContent = v;
252
+ }
253
+
254
+ function ccSetWidth(el: Element | null, pct: number): void {
255
+ if (el instanceof HTMLElement) el.style.width = `${pct}%`;
256
+ }
257
+
258
+ function ccFromDims(root: Element): number {
259
+ const l = ccNum(root.querySelector('[data-cc-length]'));
260
+ const w = ccNum(root.querySelector('[data-cc-width]'));
261
+ const t = ccNum(root.querySelector('[data-cc-thickness]'));
262
+ return (l * w * t) / 100;
263
+ }
264
+
265
+ function ccVol(root: Element, state: CcState): number {
266
+ if (state.mode === 'dims') return ccFromDims(root);
267
+ return ccNum(root.querySelector('[data-cc-volume]'));
268
+ }
269
+
270
+ function ccSummary(root: Element, vol: number): void {
271
+ ccSetText(root.querySelector('[data-cc-summary-vol]'), vol.toFixed(2));
272
+ }
273
+
274
+ function ccResults(root: Element, vol: number, cfg: CcDosage, sacks: number): void {
275
+ const shovels = Math.ceil((cfg.sandPaladas + cfg.gravelPaladas) * vol);
276
+ const water = Math.ceil(cfg.waterLitros * vol);
277
+ ccSetText(root.querySelector('[data-cc-res-cement]'), sacks.toString());
278
+ ccSetText(root.querySelector('[data-cc-res-sand]'), shovels.toString());
279
+ ccSetText(root.querySelector('[data-cc-res-water]'), water.toString());
280
+ }
281
+
282
+ function ccBinderLabels(root: Element, state: CcState): void {
283
+ const isCement = state.binder === 'cement';
284
+ const resLabel = isCement ? ccAttr(root, 'data-cc-label-cement') : ccAttr(root, 'data-cc-label-lime');
285
+ const binderName = isCement ? ccAttr(root, 'data-cc-binder-cement') : ccAttr(root, 'data-cc-binder-lime');
286
+ ccSetText(root.querySelector('[data-cc-res-binder-label]'), resLabel);
287
+ ccSetText(root.querySelector('[data-cc-prop-binder-label]'), binderName);
288
+ root.querySelectorAll('[data-cc-binder-type]').forEach((el) => {
289
+ el.textContent = binderName;
290
+ });
291
+ }
292
+
293
+ function ccPrice(root: Element, vol: number, sacks: number): void {
294
+ const pb = ccNum(root.querySelector('[data-cc-price-binder]'));
295
+ const ps = ccNum(root.querySelector('[data-cc-price-sand]'));
296
+ ccSetText(root.querySelector('[data-cc-res-total-price]'), (sacks * pb + vol * ps).toFixed(2));
297
+ }
298
+
299
+ function ccProportions(root: Element, cfg: CcDosage): void {
300
+ ccSetWidth(root.querySelector('[data-cc-prop-cement]'), cfg.parts.cement);
301
+ ccSetWidth(root.querySelector('[data-cc-prop-sand]'), cfg.parts.sand);
302
+ ccSetWidth(root.querySelector('[data-cc-prop-gravel]'), cfg.parts.gravel);
303
+ const gravel = root.querySelector('[data-cc-label-gravel]') as HTMLElement | null;
304
+ if (gravel) gravel.style.display = cfg.parts.gravel > 0 ? 'flex' : 'none';
305
+ }
306
+
307
+ function ccAdvice(root: Element, cfg: CcDosage, state: CcState): void {
308
+ const advKey = `data-cc-advice-${state.mix.toLowerCase()}`;
309
+ let advice = ccAttr(root, advKey);
310
+ if (state.binder === 'lime' && state.mix === 'H200') {
311
+ advice = ccAttr(root, 'data-cc-advice-lime');
312
+ }
313
+ ccSetText(root.querySelector('[data-cc-mix-advice]'), `${advice} (${cfg.ratio})`);
314
+ }
315
+
316
+ function ccCalc(root: Element, state: CcState): void {
317
+ const vol = ccVol(root, state);
318
+ const cfg = CC_DOSAGES[state.mix] ?? CC_DOSAGES.H200;
319
+ const sacks = Math.ceil(cfg.cementSacks * vol);
320
+ ccSummary(root, vol);
321
+ ccResults(root, vol, cfg, sacks);
322
+ ccBinderLabels(root, state);
323
+ ccPrice(root, vol, sacks);
324
+ ccProportions(root, cfg);
325
+ ccAdvice(root, cfg, state);
326
+ }
327
+
328
+ function ccOnMix(root: Element, state: CcState, el: Element): void {
329
+ root.querySelectorAll('[data-cc-mix]').forEach((b) => b.classList.remove('cc-active'));
330
+ el.classList.add('cc-active');
331
+ state.mix = (el as HTMLElement).dataset.ccMix ?? 'H200';
332
+ ccCalc(root, state);
333
+ }
334
+
335
+ function ccOnMode(root: Element, state: CcState, el: Element): void {
336
+ root.querySelectorAll('[data-cc-mode]').forEach((b) => b.classList.remove('cc-active'));
337
+ el.classList.add('cc-active');
338
+ state.mode = (el as HTMLElement).dataset.ccMode ?? 'dims';
339
+ const isDims = state.mode === 'dims';
340
+ root.querySelector('[data-cc-mode-panel="dims"]')?.classList.toggle('cc-active', isDims);
341
+ root.querySelector('[data-cc-mode-panel="direct"]')?.classList.toggle('cc-active', !isDims);
342
+ ccCalc(root, state);
343
+ }
344
+
345
+ function ccOnBinder(root: Element, state: CcState, el: Element): void {
346
+ root.querySelectorAll('[data-cc-binder]').forEach((o) => o.classList.remove('cc-active'));
347
+ el.classList.add('cc-active');
348
+ state.binder = (el as HTMLElement).dataset.ccBinder ?? 'cement';
349
+ const pb = root.querySelector('[data-cc-price-binder]') as HTMLInputElement | null;
350
+ if (pb) pb.value = state.binder === 'lime' ? '8.50' : '5.50';
351
+ ccCalc(root, state);
352
+ }
353
+
354
+ function ccAttachBtns(root: Element, state: CcState): void {
355
+ root.querySelectorAll('[data-cc-mix]').forEach((btn) => {
356
+ btn.addEventListener('click', () => ccOnMix(root, state, btn));
357
+ });
358
+ root.querySelectorAll('[data-cc-mode]').forEach((btn) => {
359
+ btn.addEventListener('click', () => ccOnMode(root, state, btn));
360
+ });
361
+ root.querySelectorAll('[data-cc-binder]').forEach((opt) => {
362
+ opt.addEventListener('click', () => ccOnBinder(root, state, opt));
363
+ });
364
+ }
365
+
366
+ function ccAttachInputs(root: Element, state: CcState): void {
367
+ root.querySelectorAll('[data-cc-dim]').forEach((inp) => {
368
+ inp.addEventListener('input', () => ccCalc(root, state));
369
+ });
370
+ root.querySelector('[data-cc-volume]')?.addEventListener('input', () => ccCalc(root, state));
371
+ root.querySelector('[data-cc-price-binder]')?.addEventListener('input', () => ccCalc(root, state));
372
+ root.querySelector('[data-cc-price-sand]')?.addEventListener('input', () => ccCalc(root, state));
373
+ }
374
+
375
+ function ccInit(root: Element): void {
376
+ const state: CcState = { mix: 'H200', mode: 'dims', binder: 'cement' };
377
+ ccAttachBtns(root, state);
378
+ ccAttachInputs(root, state);
379
+ ccCalc(root, state);
380
+ }
381
+
382
+ document.querySelectorAll('[data-cc-root]').forEach(ccInit);
383
+ </script>
384
+
385
+ <style>
386
+ .cc-root {
387
+ max-width: 800px;
388
+ margin: 0 auto;
389
+ padding-bottom: 4rem;
390
+ }
391
+
392
+ .cc-card {
393
+ background: rgba(255, 255, 255, 0.7);
394
+ backdrop-filter: blur(16px);
395
+ -webkit-backdrop-filter: blur(16px);
396
+ border: 1px solid rgba(255, 255, 255, 0.4);
397
+ border-radius: 2rem;
398
+ padding: 2.5rem;
399
+ box-shadow: 0 25px 60px rgba(0, 0, 0, 0.05);
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: 2.5rem;
403
+ animation: cc-slide-in 0.4s ease;
404
+ }
405
+
406
+ @keyframes cc-slide-in {
407
+ from {
408
+ opacity: 0;
409
+ transform: translateY(1rem);
410
+ }
411
+ to {
412
+ opacity: 1;
413
+ transform: translateY(0);
414
+ }
415
+ }
416
+
417
+ :global(.theme-dark) .cc-card {
418
+ background: rgba(15, 23, 42, 0.7);
419
+ border: 1px solid rgba(255, 255, 255, 0.05);
420
+ }
421
+
422
+ .cc-mix-selector {
423
+ display: grid;
424
+ grid-template-columns: repeat(3, 1fr);
425
+ gap: 1rem;
426
+ }
427
+
428
+ .cc-mix-btn {
429
+ padding: 1rem;
430
+ border-radius: 1rem;
431
+ border: 2px solid transparent;
432
+ background: rgba(241, 245, 249, 0.5);
433
+ cursor: pointer;
434
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
435
+ display: flex;
436
+ flex-direction: column;
437
+ align-items: center;
438
+ gap: 0.5rem;
439
+ color: #64748b;
440
+ }
441
+
442
+ :global(.theme-dark) .cc-mix-btn {
443
+ background: rgba(30, 41, 59, 0.5);
444
+ }
445
+
446
+ .cc-mix-btn svg {
447
+ width: 2rem;
448
+ height: 2rem;
449
+ }
450
+
451
+ .cc-mix-btn span {
452
+ font-size: 0.875rem;
453
+ font-weight: 700;
454
+ }
455
+
456
+ .cc-mix-btn.cc-active {
457
+ background: #f0fdf4;
458
+ border-color: #22c55e;
459
+ color: #166534;
460
+ box-shadow: 0 10px 20px rgba(34, 197, 94, 0.1);
461
+ transform: translateY(-2px);
462
+ }
463
+
464
+ :global(.theme-dark) .cc-mix-btn.cc-active {
465
+ background: rgba(34, 197, 94, 0.1);
466
+ color: #4ade80;
467
+ }
468
+
469
+ .cc-volume-control {
470
+ display: flex;
471
+ flex-direction: column;
472
+ gap: 1.5rem;
473
+ }
474
+
475
+ .cc-mode-selector {
476
+ display: flex;
477
+ gap: 0.5rem;
478
+ background: rgba(241, 245, 249, 0.8);
479
+ padding: 0.4rem;
480
+ border-radius: 1rem;
481
+ align-self: flex-start;
482
+ }
483
+
484
+ :global(.theme-dark) .cc-mode-selector {
485
+ background: rgba(30, 41, 59, 0.8);
486
+ }
487
+
488
+ .cc-mode-btn {
489
+ padding: 0.6rem 1.25rem;
490
+ border-radius: 0.75rem;
491
+ border: none;
492
+ background: transparent;
493
+ color: #64748b;
494
+ font-size: 0.8125rem;
495
+ font-weight: 700;
496
+ cursor: pointer;
497
+ transition: all 0.2s ease;
498
+ display: flex;
499
+ align-items: center;
500
+ gap: 0.5rem;
501
+ }
502
+
503
+ .cc-mode-btn svg {
504
+ width: 1rem;
505
+ height: 1rem;
506
+ }
507
+
508
+ .cc-mode-btn.cc-active {
509
+ background: #fff;
510
+ color: #1e293b;
511
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
512
+ }
513
+
514
+ :global(.theme-dark) .cc-mode-btn.cc-active {
515
+ background: #1e293b;
516
+ color: #fff;
517
+ }
518
+
519
+ .cc-dims-panel,
520
+ .cc-direct-panel {
521
+ display: none;
522
+ }
523
+
524
+ .cc-dims-panel.cc-active {
525
+ display: flex;
526
+ background: rgba(241, 245, 249, 0.5);
527
+ padding: 1.5rem;
528
+ border-radius: 1.5rem;
529
+ flex-direction: column;
530
+ gap: 1rem;
531
+ border: 1px dashed #cbd5e1;
532
+ }
533
+
534
+ .cc-direct-panel.cc-active {
535
+ display: flex;
536
+ flex-direction: column;
537
+ }
538
+
539
+ :global(.theme-dark) .cc-dims-panel.cc-active {
540
+ background: rgba(30, 41, 59, 0.3);
541
+ border-color: #334155;
542
+ }
543
+
544
+ .cc-dim-inputs {
545
+ display: grid;
546
+ grid-template-columns: repeat(3, 1fr);
547
+ gap: 1rem;
548
+ }
549
+
550
+ .cc-dim-field {
551
+ display: flex;
552
+ flex-direction: column;
553
+ gap: 0.5rem;
554
+ }
555
+
556
+ .cc-dim-field label {
557
+ font-size: 0.7rem;
558
+ font-weight: 700;
559
+ color: #94a3b8;
560
+ }
561
+
562
+ .cc-dim-input-wrap {
563
+ position: relative;
564
+ display: flex;
565
+ align-items: center;
566
+ }
567
+
568
+ .cc-dim-input-wrap input {
569
+ width: 100%;
570
+ padding: 0.75rem;
571
+ padding-right: 2.5rem;
572
+ border-radius: 0.75rem;
573
+ border: 2px solid #e2e8f0;
574
+ background: #fff;
575
+ font-size: 1rem;
576
+ font-weight: 700;
577
+ color: #1e293b;
578
+ outline: none;
579
+ }
580
+
581
+ :global(.theme-dark) .cc-dim-input-wrap input {
582
+ background: #0f172a;
583
+ border-color: #1e293b;
584
+ color: #fff;
585
+ }
586
+
587
+ .cc-dim-unit {
588
+ position: absolute;
589
+ right: 0.75rem;
590
+ font-size: 0.7rem;
591
+ font-weight: 800;
592
+ color: #94a3b8;
593
+ }
594
+
595
+ .cc-input-header {
596
+ margin-bottom: 1rem;
597
+ }
598
+
599
+ .cc-input-header label {
600
+ font-size: 0.75rem;
601
+ font-weight: 800;
602
+ color: #94a3b8;
603
+ text-transform: uppercase;
604
+ letter-spacing: 0.1em;
605
+ }
606
+
607
+ .cc-vol-input-wrap {
608
+ position: relative;
609
+ display: flex;
610
+ align-items: center;
611
+ }
612
+
613
+ .cc-vol-input-wrap input {
614
+ width: 100%;
615
+ padding: 1rem 1.5rem;
616
+ padding-right: 4rem;
617
+ border-radius: 1rem;
618
+ border: 2px solid #e2e8f0;
619
+ background: #fff;
620
+ font-size: 1.5rem;
621
+ font-weight: 800;
622
+ color: #1e293b;
623
+ outline: none;
624
+ transition: all 0.2s ease;
625
+ }
626
+
627
+ :global(.theme-dark) .cc-vol-input-wrap input {
628
+ background: #0f172a;
629
+ border-color: #1e293b;
630
+ color: #fff;
631
+ }
632
+
633
+ .cc-vol-input-wrap input:focus {
634
+ border-color: #22c55e;
635
+ box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.1);
636
+ }
637
+
638
+ .cc-unit-tag {
639
+ position: absolute;
640
+ right: 1.5rem;
641
+ font-weight: 900;
642
+ color: #94a3b8;
643
+ font-size: 1.25rem;
644
+ }
645
+
646
+ .cc-binder-selector {
647
+ display: flex;
648
+ justify-content: center;
649
+ gap: 1rem;
650
+ margin-bottom: 0.5rem;
651
+ }
652
+
653
+ .cc-binder-option {
654
+ display: flex;
655
+ align-items: center;
656
+ gap: 0.75rem;
657
+ cursor: pointer;
658
+ padding: 0.6rem 1rem;
659
+ border-radius: 1rem;
660
+ background: rgba(241, 245, 249, 0.5);
661
+ border: 2px solid transparent;
662
+ transition: all 0.2s ease;
663
+ }
664
+
665
+ :global(.theme-dark) .cc-binder-option {
666
+ background: rgba(30, 41, 59, 0.5);
667
+ }
668
+
669
+ .cc-binder-option.cc-active {
670
+ background: #fff;
671
+ border-color: #10b981;
672
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
673
+ }
674
+
675
+ :global(.theme-dark) .cc-binder-option.cc-active {
676
+ background: #1e293b;
677
+ border-color: #10b981;
678
+ }
679
+
680
+ .cc-binder-circle {
681
+ width: 1rem;
682
+ height: 1rem;
683
+ border-radius: 50%;
684
+ border: 2px solid #cbd5e1;
685
+ display: flex;
686
+ align-items: center;
687
+ justify-content: center;
688
+ transition: all 0.2s ease;
689
+ }
690
+
691
+ .cc-binder-option.cc-active .cc-binder-circle {
692
+ border-color: #10b981;
693
+ background: #10b981;
694
+ }
695
+
696
+ .cc-binder-inner {
697
+ width: 0.4rem;
698
+ height: 0.4rem;
699
+ background: #fff;
700
+ border-radius: 50%;
701
+ transform: scale(0);
702
+ transition: transform 0.2s ease;
703
+ }
704
+
705
+ .cc-binder-option.cc-active .cc-binder-inner {
706
+ transform: scale(1);
707
+ }
708
+
709
+ .cc-binder-label {
710
+ font-size: 0.8125rem;
711
+ font-weight: 700;
712
+ color: #64748b;
713
+ }
714
+
715
+ .cc-binder-option.cc-active .cc-binder-label {
716
+ color: #1e293b;
717
+ }
718
+
719
+ :global(.theme-dark) .cc-binder-option.cc-active .cc-binder-label {
720
+ color: #fff;
721
+ }
722
+
723
+ .cc-summary {
724
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
725
+ padding: 2rem;
726
+ border-radius: 1.5rem;
727
+ display: flex;
728
+ flex-direction: column;
729
+ align-items: center;
730
+ justify-content: center;
731
+ color: #fff;
732
+ text-align: center;
733
+ box-shadow: 0 15px 30px rgba(16, 185, 129, 0.2);
734
+ margin-top: 0.5rem;
735
+ position: relative;
736
+ overflow: hidden;
737
+ }
738
+
739
+ .cc-summary::before {
740
+ content: '';
741
+ position: absolute;
742
+ top: -50%;
743
+ left: -50%;
744
+ width: 200%;
745
+ height: 200%;
746
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
747
+ pointer-events: none;
748
+ }
749
+
750
+ .cc-summary-label {
751
+ font-size: 0.75rem;
752
+ font-weight: 800;
753
+ text-transform: uppercase;
754
+ letter-spacing: 0.05em;
755
+ opacity: 0.9;
756
+ margin-bottom: 0.25rem;
757
+ }
758
+
759
+ .cc-summary-value-wrap {
760
+ display: flex;
761
+ align-items: baseline;
762
+ gap: 0.5rem;
763
+ }
764
+
765
+ .cc-summary-value {
766
+ font-size: 3rem;
767
+ font-weight: 900;
768
+ line-height: 1;
769
+ }
770
+
771
+ .cc-summary-unit {
772
+ font-size: 1.25rem;
773
+ font-weight: 700;
774
+ opacity: 0.8;
775
+ }
776
+
777
+ .cc-results-grid {
778
+ display: grid;
779
+ grid-template-columns: repeat(3, 1fr);
780
+ gap: 1.5rem;
781
+ }
782
+
783
+ .cc-result-card {
784
+ background: #fff;
785
+ padding: 1.5rem;
786
+ border-radius: 1.5rem;
787
+ border: 1px solid #e2e8f0;
788
+ display: flex;
789
+ flex-direction: column;
790
+ align-items: center;
791
+ gap: 0.75rem;
792
+ text-align: center;
793
+ transition: all 0.3s ease;
794
+ }
795
+
796
+ :global(.theme-dark) .cc-result-card {
797
+ background: #1e293b;
798
+ border-color: #334155;
799
+ }
800
+
801
+ .cc-result-card svg {
802
+ width: 2rem;
803
+ height: 2rem;
804
+ color: #22c55e;
805
+ }
806
+
807
+ .cc-result-info {
808
+ display: flex;
809
+ flex-direction: column;
810
+ }
811
+
812
+ .cc-result-value {
813
+ font-size: 2rem;
814
+ font-weight: 900;
815
+ color: #1e293b;
816
+ line-height: 1;
817
+ }
818
+
819
+ :global(.theme-dark) .cc-result-value {
820
+ color: #fff;
821
+ }
822
+
823
+ .cc-result-unit {
824
+ font-size: 0.75rem;
825
+ font-weight: 800;
826
+ color: #64748b;
827
+ text-transform: uppercase;
828
+ margin-top: 0.25rem;
829
+ }
830
+
831
+ .cc-result-label {
832
+ font-size: 0.875rem;
833
+ font-weight: 600;
834
+ color: #475569;
835
+ }
836
+
837
+ :global(.theme-dark) .cc-result-label {
838
+ color: #94a3b8;
839
+ }
840
+
841
+ .cc-proportions-banner {
842
+ background: #f8fafc;
843
+ border-radius: 1.25rem;
844
+ padding: 1.25rem;
845
+ display: flex;
846
+ flex-direction: column;
847
+ gap: 0.75rem;
848
+ }
849
+
850
+ :global(.theme-dark) .cc-proportions-banner {
851
+ background: rgba(30, 41, 59, 0.3);
852
+ }
853
+
854
+ .cc-proportion-header {
855
+ font-size: 0.75rem;
856
+ font-weight: 800;
857
+ color: #64748b;
858
+ text-transform: uppercase;
859
+ display: flex;
860
+ align-items: center;
861
+ gap: 0.5rem;
862
+ }
863
+
864
+ .cc-proportion-header svg {
865
+ width: 1rem;
866
+ height: 1rem;
867
+ }
868
+
869
+ .cc-proportion-bar {
870
+ height: 1.5rem;
871
+ background: #e2e8f0;
872
+ border-radius: 0.5rem;
873
+ display: flex;
874
+ overflow: hidden;
875
+ }
876
+
877
+ :global(.theme-dark) .cc-proportion-bar {
878
+ background: #1e293b;
879
+ }
880
+
881
+ .cc-bar-segment {
882
+ height: 100%;
883
+ transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
884
+ }
885
+
886
+ .cc-seg-cement {
887
+ background: #64748b;
888
+ }
889
+
890
+ .cc-seg-sand {
891
+ background: #fbbf24;
892
+ }
893
+
894
+ .cc-seg-gravel {
895
+ background: #94a3b8;
896
+ }
897
+
898
+ .cc-proportion-labels {
899
+ display: flex;
900
+ justify-content: center;
901
+ gap: 1.5rem;
902
+ }
903
+
904
+ .cc-prop-label {
905
+ display: flex;
906
+ align-items: center;
907
+ gap: 0.5rem;
908
+ font-size: 0.75rem;
909
+ font-weight: 600;
910
+ color: #475569;
911
+ }
912
+
913
+ :global(.theme-dark) .cc-prop-label {
914
+ color: #94a3b8;
915
+ }
916
+
917
+ .cc-dot {
918
+ width: 0.5rem;
919
+ height: 0.5rem;
920
+ border-radius: 50%;
921
+ }
922
+
923
+ .cc-info-banner {
924
+ display: flex;
925
+ gap: 1rem;
926
+ padding: 1.25rem;
927
+ background: #f0fdf4;
928
+ border-radius: 1.25rem;
929
+ color: #166534;
930
+ font-size: 0.875rem;
931
+ line-height: 1.5;
932
+ }
933
+
934
+ :global(.theme-dark) .cc-info-banner {
935
+ background: rgba(34, 197, 94, 0.1);
936
+ color: #4ade80;
937
+ }
938
+
939
+ .cc-info-banner svg {
940
+ width: 1.5rem;
941
+ height: 1.5rem;
942
+ flex-shrink: 0;
943
+ }
944
+
945
+ .cc-info-banner p {
946
+ margin: 0;
947
+ }
948
+
949
+ .cc-price-card {
950
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
951
+ padding: 2.5rem;
952
+ border-radius: 2rem;
953
+ display: flex;
954
+ flex-direction: column;
955
+ align-items: center;
956
+ justify-content: center;
957
+ color: #fff;
958
+ text-align: center;
959
+ box-shadow: 0 20px 40px rgba(59, 130, 246, 0.2);
960
+ margin-top: 1.5rem;
961
+ position: relative;
962
+ overflow: hidden;
963
+ }
964
+
965
+ .cc-price-card::before {
966
+ content: '';
967
+ position: absolute;
968
+ top: -50%;
969
+ left: -50%;
970
+ width: 200%;
971
+ height: 200%;
972
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%);
973
+ pointer-events: none;
974
+ }
975
+
976
+ .cc-price-label {
977
+ font-size: 0.875rem;
978
+ font-weight: 800;
979
+ text-transform: uppercase;
980
+ letter-spacing: 0.1em;
981
+ opacity: 0.9;
982
+ margin-bottom: 0.5rem;
983
+ }
984
+
985
+ .cc-price-value-wrap {
986
+ display: flex;
987
+ align-items: baseline;
988
+ gap: 0.25rem;
989
+ }
990
+
991
+ .cc-price-value {
992
+ font-size: 4rem;
993
+ font-weight: 900;
994
+ line-height: 1;
995
+ }
996
+
997
+ .cc-price-unit {
998
+ font-size: 1.5rem;
999
+ font-weight: 800;
1000
+ opacity: 0.9;
1001
+ }
1002
+
1003
+ .cc-price-inputs-grid {
1004
+ display: grid;
1005
+ grid-template-columns: repeat(2, 1fr);
1006
+ gap: 1.25rem;
1007
+ margin-top: 2rem;
1008
+ width: 100%;
1009
+ }
1010
+
1011
+ .cc-price-input-field {
1012
+ display: flex;
1013
+ flex-direction: column;
1014
+ gap: 0.5rem;
1015
+ text-align: left;
1016
+ }
1017
+
1018
+ .cc-price-input-field label {
1019
+ font-size: 0.7rem;
1020
+ font-weight: 800;
1021
+ text-transform: uppercase;
1022
+ opacity: 0.8;
1023
+ }
1024
+
1025
+ .cc-editable-price-wrap {
1026
+ position: relative;
1027
+ display: flex;
1028
+ align-items: center;
1029
+ }
1030
+
1031
+ .cc-editable-price {
1032
+ width: 100%;
1033
+ background: rgba(255, 255, 255, 0.15);
1034
+ border: 1px solid rgba(255, 255, 255, 0.3);
1035
+ border-radius: 0.75rem;
1036
+ padding: 0.6rem 0.75rem;
1037
+ padding-right: 1.5rem;
1038
+ color: #fff;
1039
+ font-weight: 800;
1040
+ font-size: 1rem;
1041
+ outline: none;
1042
+ transition: all 0.2s ease;
1043
+ }
1044
+
1045
+ .cc-editable-price:focus {
1046
+ background: rgba(255, 255, 255, 0.25);
1047
+ border-color: #fff;
1048
+ }
1049
+
1050
+ .cc-editable-unit {
1051
+ position: absolute;
1052
+ right: 0.75rem;
1053
+ font-size: 0.75rem;
1054
+ font-weight: 800;
1055
+ opacity: 0.7;
1056
+ }
1057
+
1058
+ .cc-price-disclaimer {
1059
+ margin-top: 1.5rem;
1060
+ font-size: 0.7rem;
1061
+ opacity: 0.6;
1062
+ font-style: italic;
1063
+ }
1064
+
1065
+ @media (max-width: 640px) {
1066
+ .cc-results-grid {
1067
+ grid-template-columns: 1fr;
1068
+ }
1069
+
1070
+ .cc-mix-selector {
1071
+ grid-template-columns: 1fr;
1072
+ }
1073
+
1074
+ .cc-card {
1075
+ padding: 1.5rem;
1076
+ }
1077
+
1078
+ .cc-price-inputs-grid {
1079
+ grid-template-columns: 1fr;
1080
+ }
1081
+ }
1082
+
1083
+ @media (max-width: 480px) {
1084
+ .cc-binder-selector {
1085
+ flex-direction: column;
1086
+ align-items: stretch;
1087
+ }
1088
+ }
1089
+ </style>