@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,766 @@
1
+ ---
2
+ import type { StairCalculatorUI } from './ui';
3
+ interface Props { ui?: StairCalculatorUI; }
4
+ const { ui } = Astro.props;
5
+ const t = (ui ?? {}) as StairCalculatorUI;
6
+ ---
7
+
8
+ <div class="sc-root" data-adjusted-note={t.adjustedNote} data-ui={JSON.stringify(t)}>
9
+ <div class="sc-card">
10
+ <aside class="sc-sidebar">
11
+ <div class="sc-section">
12
+ <h3>{t.labelGeometry}</h3>
13
+ <div class="sc-input-group">
14
+ <label for="sc-total-height">{t.labelTotalHeight} <small>(cm)</small></label>
15
+ <div class="sc-stepper">
16
+ <button type="button" class="sc-step-btn minus" data-input="sc-total-height">-</button>
17
+ <input type="number" id="sc-total-height" value="300" step="1" min="10">
18
+ <button type="button" class="sc-step-btn plus" data-input="sc-total-height">+</button>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="sc-input-group">
23
+ <label for="sc-total-length">{t.labelAvailableLength} <small>(cm)</small></label>
24
+ <div class="sc-stepper">
25
+ <input type="number" id="sc-total-length" value="442" step="1" min="0">
26
+ </div>
27
+ <small style="opacity:0.6">{t.labelAvailableLengthHint}</small>
28
+ </div>
29
+
30
+ <div class="sc-input-group">
31
+ <label for="sc-num-steps">{t.labelNumSteps}</label>
32
+ <div class="sc-stepper">
33
+ <button type="button" class="sc-step-btn minus" data-input="sc-num-steps">-</button>
34
+ <input type="number" id="sc-num-steps" value="17" step="1" min="1">
35
+ <button type="button" class="sc-step-btn plus" data-input="sc-num-steps">+</button>
36
+ </div>
37
+ <button type="button" id="sc-btn-optimize" class="sc-optimize-btn">{t.labelOptimize}</button>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="sc-section">
42
+ <h3>{t.labelWork}</h3>
43
+ <div class="sc-input-group">
44
+ <label for="sc-slab-thickness">{t.labelSlabThickness} <small>(cm)</small></label>
45
+ <div class="sc-stepper">
46
+ <input type="number" id="sc-slab-thickness" value="15" step="1" min="8">
47
+ </div>
48
+ </div>
49
+ <div class="sc-input-group">
50
+ <label for="sc-finish-thickness">{t.labelFinishThickness} <small>(cm)</small></label>
51
+ <div class="sc-stepper">
52
+ <input type="number" id="sc-finish-thickness" value="3" step="0.5" min="0">
53
+ </div>
54
+ </div>
55
+ <div class="sc-input-group">
56
+ <label for="sc-ceiling-height">{t.labelCeilingHeight} <small>(cm)</small></label>
57
+ <div class="sc-stepper">
58
+ <input type="number" id="sc-ceiling-height" value="0" step="1" min="0">
59
+ </div>
60
+ </div>
61
+ <div class="sc-input-group">
62
+ <label for="sc-stair-width">{t.labelStairWidth} <small>(cm)</small></label>
63
+ <input type="number" id="sc-stair-width" value="100" class="sc-select">
64
+ </div>
65
+ <div class="sc-input-group">
66
+ <label for="sc-dosif">{t.labelConcreteStrength}</label>
67
+ <select id="sc-dosif" class="sc-select">
68
+ <option value="150">{t.optionH150}</option>
69
+ <option value="200" selected>{t.optionH200}</option>
70
+ <option value="250">{t.optionH250}</option>
71
+ </select>
72
+ </div>
73
+ </div>
74
+
75
+ <div id="sc-blondel-status" class="sc-blondel-alert warning">
76
+ <span class="sc-blondel-title" id="sc-blondel-title">{t.msgCalculating}</span>
77
+ <span class="sc-blondel-msg" id="sc-blondel-text">{t.msgEnterMeasurements}</span>
78
+ </div>
79
+ </aside>
80
+
81
+ <main class="sc-main">
82
+ <div class="sc-results-header">
83
+ <div class="sc-stat-box">
84
+ <span class="sc-stat-label">{t.labelRiser}</span>
85
+ <span class="sc-stat-value" id="sc-res-riser">--</span>
86
+ <span class="sc-stat-unit">cm</span>
87
+ </div>
88
+ <div class="sc-stat-box">
89
+ <span class="sc-stat-label">{t.labelTread}</span>
90
+ <span class="sc-stat-value" id="sc-res-tread">--</span>
91
+ <span class="sc-stat-unit">cm</span>
92
+ </div>
93
+ <div class="sc-stat-box">
94
+ <span class="sc-stat-label">{t.labelSlope}</span>
95
+ <span class="sc-stat-value" id="sc-res-angle">--</span>
96
+ <span class="sc-stat-unit">º</span>
97
+ </div>
98
+ <div class="sc-stat-box">
99
+ <span class="sc-stat-label">{t.labelRealAdvance}</span>
100
+ <span class="sc-stat-value" id="sc-res-len">--</span>
101
+ <span class="sc-stat-unit">cm</span>
102
+ </div>
103
+ </div>
104
+
105
+ <div class="sc-visualizer-container">
106
+ <svg id="sc-stair-svg" viewBox="0 0 550 380" preserveAspectRatio="xMidYMid meet">
107
+ <defs>
108
+ <marker id="sc-arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
109
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
110
+ </marker>
111
+ </defs>
112
+ <path id="sc-stair-slab" d="" fill="rgba(79, 70, 229, 0.08)" stroke="#4f46e5" stroke-width="1.5" stroke-dasharray="4,2" />
113
+ <path id="sc-stair-path" d="" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round" />
114
+ <line id="sc-ceiling-line" x1="0" y1="0" x2="0" y2="0" stroke="#f43f5e" stroke-width="2" stroke-dasharray="8,4" />
115
+ <path id="sc-angle-arc" d="" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.6" marker-end="url(#sc-arrow)" />
116
+ <text id="sc-angle-text" x="0" y="0" font-size="12" font-weight="700" fill="currentColor"></text>
117
+ <text id="sc-warning-text" x="20" y="30" font-size="14" font-weight="800" fill="#f43f5e"></text>
118
+ </svg>
119
+ </div>
120
+
121
+ <div class="sc-bottom-grid">
122
+ <div class="sc-table-card">
123
+ <h3>{t.labelLayoutTable}</h3>
124
+ <div class="sc-table-wrapper">
125
+ <table id="sc-layout-table">
126
+ <thead>
127
+ <tr>
128
+ <th>{t.colStep}</th>
129
+ <th>{t.colConcreteHeight} <small>(cm)</small></th>
130
+ <th>{t.colFinishedHeight} <small>(cm)</small></th>
131
+ <th>{t.colAdvance} <small>(cm)</small></th>
132
+ </tr>
133
+ </thead>
134
+ <tbody></tbody>
135
+ </table>
136
+ </div>
137
+ </div>
138
+
139
+ <div class="sc-materials-card">
140
+ <h3>{t.labelMaterials}</h3>
141
+ <div class="sc-material-row">
142
+ <span>{t.labelTotalConcrete}</span>
143
+ <div><strong id="sc-mat-volume">0.00</strong> <span>{t.unitM3}</span></div>
144
+ </div>
145
+ <div class="sc-material-row">
146
+ <span>{t.labelCement}</span>
147
+ <strong id="sc-mat-cement">0</strong>
148
+ </div>
149
+ <div class="sc-material-row">
150
+ <span>{t.labelSand}</span>
151
+ <strong id="sc-mat-sand">0</strong> {t.unitLiters}
152
+ </div>
153
+ <div class="sc-actions-row">
154
+ <button id="sc-btn-share" class="sc-action-btn sc-btn-primary" style="grid-column: 1 / -1;">{t.btnShare}</button>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </main>
159
+ </div>
160
+ </div>
161
+
162
+ <script>
163
+ import { calculateStairs } from './engine';
164
+
165
+ const rootEl = document.querySelector('.sc-root') as HTMLElement;
166
+ const adjustedNote = rootEl?.getAttribute('data-adjusted-note') || 'Ajustado';
167
+
168
+ const inputs = {
169
+ h: document.getElementById('sc-total-height') as HTMLInputElement,
170
+ l: document.getElementById('sc-total-length') as HTMLInputElement,
171
+ n: document.getElementById('sc-num-steps') as HTMLInputElement,
172
+ slab: document.getElementById('sc-slab-thickness') as HTMLInputElement,
173
+ finish: document.getElementById('sc-finish-thickness') as HTMLInputElement,
174
+ ceiling: document.getElementById('sc-ceiling-height') as HTMLInputElement,
175
+ width: document.getElementById('sc-stair-width') as HTMLInputElement,
176
+ dosif: document.getElementById('sc-dosif') as HTMLSelectElement
177
+ };
178
+
179
+ const results = {
180
+ riser: document.getElementById('sc-res-riser') as HTMLElement,
181
+ tread: document.getElementById('sc-res-tread') as HTMLElement,
182
+ angle: document.getElementById('sc-res-angle') as HTMLElement,
183
+ len: document.getElementById('sc-res-len') as HTMLElement,
184
+ blondelTitle: document.getElementById('sc-blondel-title') as HTMLElement,
185
+ blondelText: document.getElementById('sc-blondel-text') as HTMLElement,
186
+ blondelBox: document.getElementById('sc-blondel-status') as HTMLElement,
187
+ tableBody: document.querySelector('#sc-layout-table tbody') as HTMLElement,
188
+ svgPath: document.getElementById('sc-stair-path') as unknown as SVGPathElement,
189
+ slabPath: document.getElementById('sc-stair-slab') as unknown as SVGPathElement,
190
+ ceilingLine: document.getElementById('sc-ceiling-line') as unknown as SVGLineElement,
191
+ angleArc: document.getElementById('sc-angle-arc') as unknown as SVGPathElement,
192
+ angleText: document.getElementById('sc-angle-text') as unknown as SVGTextElement,
193
+ warningText: document.getElementById('sc-warning-text') as unknown as SVGTextElement,
194
+ matVol: document.getElementById('sc-mat-volume') as HTMLElement,
195
+ matCement: document.getElementById('sc-mat-cement') as HTMLElement,
196
+ matSand: document.getElementById('sc-mat-sand') as HTMLElement
197
+ };
198
+
199
+ // eslint-disable-next-line max-lines-per-function, complexity
200
+ function updateCalculations() {
201
+ const H = parseFloat(inputs.h.value) || 0;
202
+ const L = parseFloat(inputs.l.value) || 0;
203
+ const N = parseInt(inputs.n.value) || 1;
204
+ const slabE = parseFloat(inputs.slab.value) || 15;
205
+ const finishE = parseFloat(inputs.finish.value) || 0;
206
+ const ceilingH = parseFloat(inputs.ceiling.value) || 0;
207
+ const W = parseFloat(inputs.width.value) || 100;
208
+ const cementFac = parseInt(inputs.dosif.value) || 200;
209
+
210
+ const calc = calculateStairs(H, L, N, slabE, finishE, ceilingH, W, cementFac);
211
+
212
+ const ui = JSON.parse(rootEl.getAttribute('data-ui') || '{}');
213
+ const getTranslation = (key: string, val?: number) => {
214
+ let text = ui[key] || key;
215
+ if (val !== undefined) text = text.replace('{val}', val.toFixed(1));
216
+ return text;
217
+ };
218
+
219
+ results.riser.textContent = calc.riser.toFixed(1);
220
+ results.tread.textContent = calc.tread.toFixed(1);
221
+ results.angle.textContent = calc.angle.toFixed(0);
222
+ results.len.textContent = calc.realLength.toFixed(0);
223
+
224
+ results.blondelTitle.textContent = getTranslation(calc.blondelTitleKey);
225
+ results.blondelText.textContent = getTranslation(calc.blondelMessageKey, calc.blondelMessageVal);
226
+ results.blondelBox.className = `sc-blondel-alert ${calc.blondelStatus}`;
227
+
228
+ let tableHTML = '';
229
+ calc.tableRows.forEach(row => {
230
+ const extra = row.step === 1 ? ` <small>(${adjustedNote})</small>` : '';
231
+ tableHTML += `<tr><td>${row.step}</td><td class="sc-col-hl">${row.concreteHeight.toFixed(1)}${extra}</td><td>${row.finishedHeight.toFixed(1)}</td><td>${row.horizontalDistance.toFixed(1)}</td></tr>`;
232
+ });
233
+ results.tableBody.innerHTML = tableHTML;
234
+
235
+ const svgW = 500, svgH = 320, margin = 50;
236
+ const angleRad = Math.atan(H / (calc.realLength || 1));
237
+ const scale = Math.min((svgH - margin * 2) / H, (svgW - margin * 2) / (calc.realLength || 1));
238
+ const x0 = margin, y0 = svgH - margin;
239
+
240
+ let d_stair = `M ${x0} ${y0}`;
241
+ for (let i = 0; i < N; i++) {
242
+ d_stair += ` v -${calc.riser * scale} h ${calc.tread * scale}`;
243
+ }
244
+ results.svgPath.setAttribute('d', d_stair);
245
+
246
+ const slabYOff = slabE * scale / Math.cos(angleRad);
247
+ const dSlab = d_stair + ` L ${x0 + (calc.realLength * scale) + (calc.tread * scale)} ${y0 - (H * scale) + slabYOff} L ${x0} ${y0 + slabYOff} Z`;
248
+ results.slabPath.setAttribute('d', dSlab);
249
+
250
+ if (ceilingH > 0) {
251
+ const cy = y0 - (ceilingH * scale);
252
+ results.ceilingLine.setAttribute('x1', String(x0));
253
+ results.ceilingLine.setAttribute('y1', String(cy));
254
+ results.ceilingLine.setAttribute('x2', String(x0 + calc.realLength * scale));
255
+ results.ceilingLine.setAttribute('y2', String(cy));
256
+ } else {
257
+ results.ceilingLine.setAttribute('x1', '0');
258
+ }
259
+ results.warningText.textContent = calc.spacingWarningKey ? getTranslation(calc.spacingWarningKey) : '';
260
+
261
+ const arcR = 40;
262
+ results.angleArc.setAttribute('d', `M ${x0 + arcR} ${y0} A ${arcR} ${arcR} 0 0 0 ${x0 + arcR * Math.cos(angleRad)} ${y0 - arcR * Math.sin(angleRad)}`);
263
+ results.angleText.setAttribute('x', String(x0 + arcR + 5));
264
+ results.angleText.setAttribute('y', String(y0 - 5));
265
+ results.angleText.textContent = `${calc.angle.toFixed(0)}°`;
266
+
267
+ results.matVol.textContent = calc.volume.toFixed(2);
268
+ results.matCement.textContent = calc.cement.toString();
269
+ results.matSand.textContent = calc.sand.toString();
270
+ }
271
+ document.querySelectorAll('.sc-step-btn').forEach(btn => {
272
+ btn.addEventListener('click', () => {
273
+ const inputId = btn.getAttribute('data-input');
274
+ if (!inputId) return;
275
+ const input = document.getElementById(inputId) as HTMLInputElement;
276
+ const step = parseFloat(input.getAttribute('step') || '1');
277
+ const val = parseFloat(input.value);
278
+ const min = parseFloat(input.getAttribute('min') || '0');
279
+ input.value = (btn.classList.contains('plus') ? (val + step) : Math.max(min, val - step)).toString();
280
+ updateCalculations();
281
+ });
282
+ });
283
+
284
+ document.getElementById('sc-btn-optimize')?.addEventListener('click', () => {
285
+ const H_val = parseFloat(inputs.h.value) || 0;
286
+ if (H_val > 0) {
287
+ inputs.n.value = Math.round(H_val / 18).toString();
288
+ updateCalculations();
289
+ }
290
+ });
291
+
292
+ Object.values(inputs).forEach(input => input?.addEventListener('input', updateCalculations));
293
+
294
+ document.getElementById('sc-btn-share')?.addEventListener('click', () => {
295
+ const ui = JSON.parse(rootEl.getAttribute('data-ui') || '{}');
296
+ const msg = (ui.shareMessage || '')
297
+ .replace('{h}', inputs.h.value)
298
+ .replace('{n}', inputs.n.value)
299
+ .replace('{ch}', results.riser.textContent || '0')
300
+ .replace('{vol}', results.matVol.textContent || '0');
301
+
302
+ if (navigator.share) {
303
+ navigator.share({ title: ui.shareTitle, text: msg }).catch(() => {});
304
+ } else {
305
+ alert(msg);
306
+ }
307
+ });
308
+
309
+ updateCalculations();
310
+ </script>
311
+
312
+ <style>
313
+ :root {
314
+ --sc-primary: #6366f1;
315
+ --sc-primary-dark: #4f46e5;
316
+ --sc-primary-light: #818cf8;
317
+ --sc-neutral: #1e293b;
318
+ --sc-neutral-light: #64748b;
319
+ --sc-border: #e2e8f0;
320
+ --sc-bg-light: #f8fafc;
321
+ --sc-bg-lighter: #f1f5f9;
322
+ --sc-bg: #fff;
323
+ }
324
+
325
+ :global(.theme-dark) {
326
+ --sc-primary: #818cf8;
327
+ --sc-primary-dark: #6366f1;
328
+ --sc-neutral: #f1f5f9;
329
+ --sc-neutral-light: #9ca3af;
330
+ --sc-border: rgba(255, 255, 255, 0.1);
331
+ --sc-bg-light: rgba(255, 255, 255, 0.04);
332
+ --sc-bg-lighter: rgba(255, 255, 255, 0.02);
333
+ --sc-bg: rgba(255, 255, 255, 0.01);
334
+ }
335
+
336
+ .sc-root {
337
+ width: 100%;
338
+ max-width: 1100px;
339
+ margin: 1.5rem auto;
340
+ padding: 0 1rem;
341
+ color: var(--sc-neutral);
342
+ }
343
+
344
+ .sc-card {
345
+ background: var(--sc-bg);
346
+ border-radius: 2rem;
347
+ overflow: hidden;
348
+ display: grid;
349
+ grid-template-columns: 350px 1fr;
350
+ box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.1);
351
+ border: 1px solid var(--sc-border);
352
+ }
353
+
354
+ .sc-sidebar {
355
+ background: var(--sc-bg-light);
356
+ padding: 1.5rem;
357
+ border-right: 1px solid var(--sc-border);
358
+ display: flex;
359
+ flex-direction: column;
360
+ gap: 1rem;
361
+ }
362
+
363
+ .sc-section h3 {
364
+ font-size: 0.75rem;
365
+ text-transform: uppercase;
366
+ letter-spacing: 0.05em;
367
+ color: var(--sc-primary);
368
+ margin-bottom: 1rem;
369
+ font-weight: 800;
370
+ }
371
+
372
+ .sc-main {
373
+ padding: 1.5rem 2rem;
374
+ display: flex;
375
+ flex-direction: column;
376
+ gap: 1.5rem;
377
+ background: var(--sc-bg);
378
+ }
379
+
380
+ .sc-results-header {
381
+ display: grid;
382
+ grid-template-columns: repeat(4, 1fr);
383
+ gap: 1rem;
384
+ }
385
+
386
+ .sc-stat-box {
387
+ background: rgba(255, 255, 255, 0.5);
388
+ padding: 0.75rem 0.5rem;
389
+ border-radius: 1.25rem;
390
+ text-align: center;
391
+ border: 1px solid var(--sc-border);
392
+ }
393
+
394
+ :global(.theme-dark) .sc-stat-box {
395
+ background: rgba(255, 255, 255, 0.04);
396
+ }
397
+
398
+ .sc-stat-label {
399
+ display: block;
400
+ font-size: 0.6rem;
401
+ text-transform: uppercase;
402
+ color: var(--sc-neutral-light);
403
+ margin-bottom: 0.25rem;
404
+ font-weight: 700;
405
+ }
406
+
407
+ .sc-stat-value {
408
+ font-size: 1.5rem;
409
+ font-weight: 800;
410
+ color: var(--sc-neutral);
411
+ }
412
+
413
+ .sc-stat-unit {
414
+ font-size: 0.75rem;
415
+ color: var(--sc-neutral-light);
416
+ }
417
+
418
+ .sc-visualizer-container {
419
+ background: var(--sc-bg-lighter);
420
+ border-radius: 1.5rem;
421
+ padding: 1.5rem;
422
+ position: relative;
423
+ min-height: 300px;
424
+ display: flex;
425
+ align-items: center;
426
+ justify-content: center;
427
+ border: 1px solid var(--sc-border);
428
+ overflow: hidden;
429
+ }
430
+
431
+ #sc-stair-svg {
432
+ width: 100%;
433
+ height: 100%;
434
+ max-height: 400px;
435
+ color: var(--sc-neutral);
436
+ }
437
+
438
+ .sc-input-grid {
439
+ display: grid;
440
+ grid-template-columns: 1fr 1fr;
441
+ gap: 0.75rem;
442
+ }
443
+
444
+ .sc-input-group {
445
+ margin-bottom: 0.75rem;
446
+ }
447
+
448
+ .sc-input-group label {
449
+ display: block;
450
+ font-size: 0.8rem;
451
+ font-weight: 700;
452
+ color: var(--sc-neutral-light);
453
+ margin-bottom: 0.4rem;
454
+ }
455
+
456
+ .sc-stepper {
457
+ display: flex;
458
+ align-items: center;
459
+ background: rgba(255, 255, 255, 0.5);
460
+ border-radius: 1rem;
461
+ border: 2px solid var(--sc-border);
462
+ transition: all 0.2s ease;
463
+ }
464
+
465
+ :global(.theme-dark) .sc-stepper {
466
+ background: rgba(255, 255, 255, 0.04);
467
+ }
468
+
469
+ .sc-stepper:focus-within {
470
+ border-color: var(--sc-primary);
471
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
472
+ }
473
+
474
+ .sc-step-btn {
475
+ width: 44px;
476
+ height: 44px;
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: center;
480
+ background: none;
481
+ border: none;
482
+ color: var(--sc-neutral-light);
483
+ font-size: 1.25rem;
484
+ cursor: pointer;
485
+ border-radius: 0.5rem;
486
+ }
487
+
488
+ .sc-step-btn:hover {
489
+ background: var(--sc-bg-lighter);
490
+ color: var(--sc-primary);
491
+ }
492
+
493
+ .sc-stepper input {
494
+ flex: 1;
495
+ background: none;
496
+ border: none;
497
+ color: var(--sc-neutral);
498
+ text-align: center;
499
+ font-size: 1.1rem;
500
+ font-weight: 700;
501
+ height: 44px;
502
+ padding: 0;
503
+ appearance: textfield;
504
+ }
505
+
506
+ .sc-stepper input::-webkit-outer-spin-button,
507
+ .sc-stepper input::-webkit-inner-spin-button {
508
+ -webkit-appearance: none;
509
+ margin: 0;
510
+ }
511
+
512
+ .sc-select {
513
+ width: 100%;
514
+ padding: 0.8rem;
515
+ border-radius: 1rem;
516
+ border: 2px solid var(--sc-border);
517
+ background: rgba(255, 255, 255, 0.5);
518
+ font-weight: 600;
519
+ font-size: 0.95rem;
520
+ color: var(--sc-neutral);
521
+ outline: none;
522
+ transition: all 0.2s ease;
523
+ }
524
+
525
+ :global(.theme-dark) .sc-select {
526
+ background: rgba(255, 255, 255, 0.04);
527
+ }
528
+
529
+ .sc-optimize-btn {
530
+ width: 100%;
531
+ padding: 0.75rem;
532
+ background: var(--sc-primary-dark);
533
+ color: white;
534
+ border: none;
535
+ border-radius: 0.75rem;
536
+ font-weight: 700;
537
+ cursor: pointer;
538
+ margin-top: 0.5rem;
539
+ font-size: 0.85rem;
540
+ }
541
+
542
+ .sc-blondel-alert {
543
+ padding: 1rem;
544
+ border-radius: 1rem;
545
+ font-weight: 700;
546
+ display: flex;
547
+ flex-direction: column;
548
+ gap: 0.4rem;
549
+ margin-top: auto;
550
+ }
551
+
552
+ .sc-blondel-title {
553
+ font-size: 0.65rem;
554
+ text-transform: uppercase;
555
+ opacity: 0.8;
556
+ }
557
+
558
+ .sc-blondel-msg {
559
+ font-size: 0.85rem;
560
+ }
561
+
562
+ .sc-blondel-alert.success {
563
+ background: #dcfce7;
564
+ color: #166534;
565
+ border: 1px solid #bbf7d0;
566
+ }
567
+
568
+ .sc-blondel-alert.warning {
569
+ background: #fef9c3;
570
+ color: #854d0e;
571
+ border: 1px solid #fef08a;
572
+ }
573
+
574
+ .sc-blondel-alert.error {
575
+ background: #fee2e2;
576
+ color: #991b1b;
577
+ border: 1px solid #fecaca;
578
+ }
579
+
580
+ .sc-bottom-grid {
581
+ display: grid;
582
+ grid-template-columns: 1fr 300px;
583
+ gap: 1.25rem;
584
+ }
585
+
586
+ .sc-table-card {
587
+ background: rgba(255, 255, 255, 0.5);
588
+ border-radius: 1.25rem;
589
+ padding: 1.25rem;
590
+ border: 1px solid var(--sc-border);
591
+ }
592
+
593
+ :global(.theme-dark) .sc-table-card {
594
+ background: rgba(255, 255, 255, 0.03);
595
+ }
596
+
597
+ .sc-table-card h3 {
598
+ font-size: 0.8rem;
599
+ text-transform: uppercase;
600
+ color: var(--sc-neutral-light);
601
+ font-weight: 800;
602
+ margin-bottom: 0.75rem;
603
+ }
604
+
605
+ .sc-table-wrapper {
606
+ max-height: 220px;
607
+ overflow-y: auto;
608
+ }
609
+
610
+ :global(.sc-table-wrapper table) {
611
+ width: 100%;
612
+ }
613
+
614
+ :global(.sc-table-wrapper th) {
615
+ text-align: left;
616
+ font-size: 0.55rem;
617
+ text-transform: uppercase;
618
+ color: var(--sc-neutral-light);
619
+ padding: 0.4rem 0.2rem;
620
+ background: transparent;
621
+ }
622
+
623
+ :global(.sc-table-wrapper td) {
624
+ padding: 0.6rem 0.2rem;
625
+ border-bottom: 1px solid var(--sc-border);
626
+ font-size: 0.8rem;
627
+ color: var(--sc-neutral);
628
+ }
629
+
630
+ .sc-col-hl {
631
+ color: var(--sc-primary);
632
+ font-weight: 700;
633
+ }
634
+
635
+ .sc-materials-card {
636
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(99, 102, 241, 0.05) 100%);
637
+ color: var(--sc-neutral);
638
+ border-radius: 1.25rem;
639
+ padding: 1.25rem;
640
+ display: flex;
641
+ flex-direction: column;
642
+ gap: 0.75rem;
643
+ border: 1px solid var(--sc-primary);
644
+ }
645
+
646
+ :global(.theme-dark) .sc-materials-card {
647
+ background: rgba(129, 140, 248, 0.08);
648
+ }
649
+
650
+ .sc-materials-card h3 {
651
+ font-size: 0.8rem;
652
+ text-transform: uppercase;
653
+ color: var(--sc-primary);
654
+ font-weight: 800;
655
+ margin-bottom: 0.5rem;
656
+ }
657
+
658
+ .sc-material-row {
659
+ display: flex;
660
+ justify-content: space-between;
661
+ align-items: baseline;
662
+ padding-bottom: 0.4rem;
663
+ border-bottom: 1px solid var(--sc-border);
664
+ }
665
+
666
+ .sc-material-row span {
667
+ font-size: 0.7rem;
668
+ color: var(--sc-neutral-light);
669
+ }
670
+
671
+ .sc-material-row strong {
672
+ font-size: 1.15rem;
673
+ color: var(--sc-primary);
674
+ }
675
+
676
+ .sc-actions-row {
677
+ display: grid;
678
+ grid-template-columns: 1fr 1fr;
679
+ gap: 0.5rem;
680
+ margin-top: auto;
681
+ }
682
+
683
+ .sc-action-btn {
684
+ padding: 0.75rem;
685
+ border-radius: 0.75rem;
686
+ font-weight: 800;
687
+ font-size: 0.8rem;
688
+ border: none;
689
+ cursor: pointer;
690
+ }
691
+
692
+ .sc-btn-primary {
693
+ background: var(--sc-primary-dark);
694
+ color: white;
695
+ }
696
+
697
+ .sc-btn-secondary {
698
+ background: rgba(255, 255, 255, 0.1);
699
+ color: white;
700
+ }
701
+
702
+ @media (max-width: 1000px) {
703
+ .sc-card {
704
+ grid-template-columns: 1fr;
705
+ border-radius: 1.5rem;
706
+ }
707
+
708
+ .sc-sidebar {
709
+ border-right: none;
710
+ border-bottom: 1px solid var(--sc-border);
711
+ padding: 1.25rem;
712
+ }
713
+
714
+ .sc-main {
715
+ padding: 1.25rem;
716
+ }
717
+ }
718
+
719
+ @media (max-width: 768px) {
720
+ .sc-root {
721
+ margin: 1rem auto;
722
+ padding: 0 0.75rem;
723
+ }
724
+
725
+ .sc-results-header {
726
+ grid-template-columns: repeat(2, 1fr);
727
+ gap: 0.5rem;
728
+ }
729
+
730
+ .sc-bottom-grid {
731
+ grid-template-columns: 1fr;
732
+ }
733
+
734
+ .sc-stat-value {
735
+ font-size: 1.25rem;
736
+ }
737
+
738
+ .sc-stat-box {
739
+ padding: 0.5rem;
740
+ }
741
+
742
+ .sc-visualizer-container {
743
+ min-height: 250px;
744
+ padding: 1rem;
745
+ }
746
+ }
747
+
748
+ @media (max-width: 480px) {
749
+ .sc-results-header {
750
+ gap: 0.4rem;
751
+ }
752
+
753
+ .sc-stat-box {
754
+ padding: 0.4rem;
755
+ border-radius: 1rem;
756
+ }
757
+
758
+ .sc-stat-value {
759
+ font-size: 1.1rem;
760
+ }
761
+
762
+ .sc-material-row strong {
763
+ font-size: 1rem;
764
+ }
765
+ }
766
+ </style>