@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,605 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { KnownLocale } from '../../types';
4
+ import type { BalusterCalculatorUI } 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 BalusterCalculatorUI;
13
+ ---
14
+
15
+ <div class="bc-root">
16
+ <div class="bc-input-panel">
17
+ <div class="bc-panel-header">
18
+ <div class="bc-panel-icon">
19
+ <Icon name="mdi:tape-measure" class="bc-icon" />
20
+ </div>
21
+ <div>
22
+ <p class="bc-panel-title">{t.sectionTitle}</p>
23
+ <p class="bc-panel-desc">{t.sectionDesc}</p>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="bc-inputs-grid">
28
+ <div class="bc-input-group">
29
+ <label for="bc-length" class="bc-label">{t.labelRailLength}</label>
30
+ <div class="bc-input-wrapper">
31
+ <input type="number" id="bc-length" value="200" min="0" step="0.1" class="bc-input" />
32
+ <span class="bc-unit-label">cm</span>
33
+ </div>
34
+ </div>
35
+ <div class="bc-input-group">
36
+ <label for="bc-width" class="bc-label">{t.labelBalusterWidth}</label>
37
+ <div class="bc-input-wrapper">
38
+ <input type="number" id="bc-width" value="5" min="0" step="0.1" class="bc-input" />
39
+ <span class="bc-unit-label">cm</span>
40
+ </div>
41
+ </div>
42
+ <div class="bc-input-group">
43
+ <label for="bc-maxgap" class="bc-label">{t.labelMaxGap}</label>
44
+ <div class="bc-input-wrapper">
45
+ <input type="number" id="bc-maxgap" value="10" min="0" step="0.1" class="bc-input" />
46
+ <span class="bc-unit-label">cm</span>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="bc-unit-switcher">
52
+ <div class="bc-unit-toggle">
53
+ <button class="bc-unit-btn" data-bc-unit="cm" data-bc-active="true">cm</button>
54
+ <button class="bc-unit-btn" data-bc-unit="in" data-bc-active="false">in</button>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="bc-blueprint">
60
+ <div class="bc-blueprint-grid"></div>
61
+ <div class="bc-blueprint-inner">
62
+ <div class="bc-bp-header">
63
+ <div>
64
+ <p class="bc-bp-plan-title">{t.blueprintTitle}</p>
65
+ <p class="bc-bp-ref">{t.blueprintRef}</p>
66
+ </div>
67
+ <div class="bc-bp-count-block">
68
+ <p id="bc-count" class="bc-bp-count">0</p>
69
+ <p class="bc-bp-count-label">{t.labelRequired}</p>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="bc-bp-stats">
74
+ <div class="bc-stat-card">
75
+ <span class="bc-stat-label">{t.labelExactGap}</span>
76
+ <span id="bc-gap" class="bc-stat-value">0</span>
77
+ </div>
78
+ <div class="bc-stat-card">
79
+ <span class="bc-stat-label">{t.labelOnCenter}</span>
80
+ <span id="bc-oncenter" class="bc-stat-value">0</span>
81
+ </div>
82
+ <div class="bc-stat-card">
83
+ <span class="bc-stat-label">{t.labelTotalSpaces}</span>
84
+ <span id="bc-spaces" class="bc-stat-value">0</span>
85
+ </div>
86
+ <div class="bc-stat-card">
87
+ <span class="bc-stat-label">{t.labelCoveredLength}</span>
88
+ <span id="bc-covered" class="bc-stat-value">0</span>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="bc-visualizer">
93
+ <svg
94
+ id="bc-svg"
95
+ width="100%"
96
+ height="100%"
97
+ viewBox="0 0 1000 200"
98
+ preserveAspectRatio="xMidYMid meet"
99
+ class="bc-svg"
100
+ ></svg>
101
+ </div>
102
+
103
+ <p class="bc-disclaimer">{t.labelDisclaimer}</p>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <script>
109
+ let bcUnit = 'cm';
110
+ let bcLengthEl: HTMLInputElement | null = null;
111
+ let bcWidthEl: HTMLInputElement | null = null;
112
+ let bcMaxGapEl: HTMLInputElement | null = null;
113
+ let bcCountEl: HTMLElement | null = null;
114
+ let bcGapEl: HTMLElement | null = null;
115
+ let bcOnCenterEl: HTMLElement | null = null;
116
+ let bcSpacesEl: HTMLElement | null = null;
117
+ let bcCoveredEl: HTMLElement | null = null;
118
+ let bcSvgEl: Element | null = null;
119
+
120
+ function parseInput(el: HTMLInputElement | null, fallback = 0): number {
121
+ return parseFloat(el?.value ?? '') || fallback;
122
+ }
123
+
124
+ function setText(el: HTMLElement | null, text: string) {
125
+ if (el) el.textContent = text;
126
+ }
127
+
128
+ function createSvgEl(tag: string, attrs: Record<string, string>): Element {
129
+ const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
130
+ Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
131
+ return el;
132
+ }
133
+
134
+ function renderEmpty() {
135
+ setText(bcCountEl, '0');
136
+ setText(bcGapEl, '0');
137
+ setText(bcOnCenterEl, '0');
138
+ setText(bcSpacesEl, '0');
139
+ setText(bcCoveredEl, '0');
140
+ if (bcSvgEl) bcSvgEl.innerHTML = '';
141
+ }
142
+
143
+ type DrawCtx = { svg: Element; scale: number; pX: number; railY: number; railH: number; balH: number };
144
+
145
+ function drawRail(ctx: DrawCtx, L: number) {
146
+ ctx.svg.appendChild(createSvgEl('rect', {
147
+ x: String(ctx.pX), y: String(ctx.railY),
148
+ width: String(L * ctx.scale), height: String(ctx.railH),
149
+ fill: 'none', stroke: 'white', 'stroke-width': '2',
150
+ }));
151
+ }
152
+
153
+ function drawBaluster(ctx: DrawCtx, x: number, wp: number) {
154
+ const balY = ctx.railY + ctx.railH;
155
+ ctx.svg.appendChild(createSvgEl('rect', {
156
+ x: String(x), y: String(balY),
157
+ width: String(wp), height: String(ctx.balH),
158
+ fill: 'white', 'fill-opacity': '0.2', stroke: 'white', 'stroke-width': '1',
159
+ }));
160
+ }
161
+
162
+ function drawDashLine(ctx: DrawCtx, cx: number) {
163
+ const balY = ctx.railY + ctx.railH;
164
+ ctx.svg.appendChild(createSvgEl('line', {
165
+ x1: String(cx), y1: String(balY),
166
+ x2: String(cx), y2: String(balY + ctx.balH + 10),
167
+ stroke: 'white', 'stroke-width': '0.5', 'stroke-dasharray': '2,2', opacity: '0.5',
168
+ }));
169
+ }
170
+
171
+ function drawTruncatedText(ctx: DrawCtx, n: number) {
172
+ const el = createSvgEl('text', {
173
+ x: '500', y: String(ctx.railY + ctx.railH + ctx.balH / 2), fill: 'white', 'text-anchor': 'middle',
174
+ });
175
+ el.textContent = `... ${n - 40} ...`;
176
+ ctx.svg.appendChild(el);
177
+ }
178
+
179
+ function drawBalusters(ctx: DrawCtx, n: number, g: number, w: number) {
180
+ const isTruncated = n > 100;
181
+ const drawLimit = isTruncated ? 20 : n;
182
+ let currentX = ctx.pX + g * ctx.scale;
183
+ for (let i = 0; i < drawLimit; i++) {
184
+ drawBaluster(ctx, currentX, w * ctx.scale);
185
+ drawDashLine(ctx, currentX + (w * ctx.scale) / 2);
186
+ currentX += (w + g) * ctx.scale;
187
+ }
188
+ if (isTruncated) drawTruncatedText(ctx, n);
189
+ }
190
+
191
+ function drawArrow(ctx: DrawCtx, g: number, label: string) {
192
+ const balY = ctx.railY + ctx.railH;
193
+ const x1 = ctx.pX;
194
+ const x2 = ctx.pX + g * ctx.scale;
195
+ const y = balY + ctx.balH / 2;
196
+ if (x2 - x1 < 20) return;
197
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
198
+ group.appendChild(createSvgEl('line', {
199
+ x1: String(x1), y1: String(y), x2: String(x2), y2: String(y),
200
+ stroke: '#4ade80', 'stroke-width': '1',
201
+ }));
202
+ const textEl = createSvgEl('text', {
203
+ x: String((x1 + x2) / 2), y: String(y - 5),
204
+ fill: '#4ade80', 'text-anchor': 'middle', 'font-size': '10',
205
+ });
206
+ textEl.textContent = label;
207
+ group.appendChild(textEl);
208
+ ctx.svg.appendChild(group);
209
+ }
210
+
211
+ function drawBlueprint(n: number, g: number, w: number, L: number) {
212
+ if (!bcSvgEl) return;
213
+ bcSvgEl.innerHTML = '';
214
+ const pX = 50;
215
+ const ctx: DrawCtx = { svg: bcSvgEl, scale: (1000 - pX * 2) / L, pX, railY: 50, railH: 20, balH: 100 };
216
+ drawRail(ctx, L);
217
+ drawBalusters(ctx, n, g, w);
218
+ drawArrow(ctx, g, `Gap ${g.toFixed(1)}`);
219
+ }
220
+
221
+ function calculate() {
222
+ const L = parseInput(bcLengthEl);
223
+ const w = parseInput(bcWidthEl);
224
+ const maxGap = parseInput(bcMaxGapEl);
225
+ if (L <= 0 || maxGap <= 0) { renderEmpty(); return; }
226
+ let n = Math.ceil((L - maxGap) / (w + maxGap));
227
+ if (n < 0) n = 0;
228
+ let g = (L - n * w) / (n + 1);
229
+ if (g < 0) { g = 0; n = 0; }
230
+ const onCenter = g + w;
231
+ const fmt = (v: number) => v.toLocaleString('es-ES', { maximumFractionDigits: 2 });
232
+ setText(bcCountEl, String(n));
233
+ setText(bcGapEl, `${fmt(g)} ${bcUnit}`);
234
+ setText(bcOnCenterEl, `${fmt(onCenter)} ${bcUnit}`);
235
+ setText(bcSpacesEl, String(n + 1));
236
+ setText(bcCoveredEl, `${fmt(L)} ${bcUnit}`);
237
+ drawBlueprint(n, g, w, L);
238
+ }
239
+
240
+ function setUnit(root: HTMLElement, u: string) {
241
+ bcUnit = u;
242
+ root.querySelectorAll('[data-bc-unit]').forEach((btn) => {
243
+ (btn as HTMLElement).dataset.bcActive = btn.getAttribute('data-bc-unit') === u ? 'true' : 'false';
244
+ });
245
+ root.querySelectorAll('.bc-unit-label').forEach((el) => { el.textContent = u; });
246
+ calculate();
247
+ }
248
+
249
+ function attachInputListeners() {
250
+ bcLengthEl?.addEventListener('input', calculate);
251
+ bcWidthEl?.addEventListener('input', calculate);
252
+ bcMaxGapEl?.addEventListener('input', calculate);
253
+ }
254
+
255
+ function attachUnitListeners(root: HTMLElement) {
256
+ root.querySelectorAll('[data-bc-unit]').forEach((btn) => {
257
+ btn.addEventListener('click', () => setUnit(root, btn.getAttribute('data-bc-unit') ?? 'cm'));
258
+ });
259
+ }
260
+
261
+ function initBalusterCalculator(root: HTMLElement) {
262
+ bcLengthEl = root.querySelector('#bc-length');
263
+ bcWidthEl = root.querySelector('#bc-width');
264
+ bcMaxGapEl = root.querySelector('#bc-maxgap');
265
+ bcCountEl = root.querySelector('#bc-count');
266
+ bcGapEl = root.querySelector('#bc-gap');
267
+ bcOnCenterEl = root.querySelector('#bc-oncenter');
268
+ bcSpacesEl = root.querySelector('#bc-spaces');
269
+ bcCoveredEl = root.querySelector('#bc-covered');
270
+ bcSvgEl = root.querySelector('#bc-svg');
271
+ attachInputListeners();
272
+ attachUnitListeners(root);
273
+ calculate();
274
+ }
275
+
276
+ function init() {
277
+ document.querySelectorAll<HTMLElement>('.bc-root').forEach(initBalusterCalculator);
278
+ }
279
+
280
+ document.addEventListener('astro:page-load', init);
281
+ init();
282
+ </script>
283
+
284
+ <style>
285
+ .bc-root {
286
+ width: 100%;
287
+ max-width: 56rem;
288
+ margin: 0 auto;
289
+ padding: 1rem;
290
+ display: flex;
291
+ flex-direction: column;
292
+ gap: 2rem;
293
+ }
294
+
295
+ .bc-input-panel {
296
+ background: #fff;
297
+ border-radius: 1.5rem;
298
+ padding: 1.5rem;
299
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
300
+ border: 1px solid #f1f5f9;
301
+ }
302
+
303
+ .bc-panel-header {
304
+ display: flex;
305
+ align-items: center;
306
+ gap: 0.75rem;
307
+ margin-bottom: 1.5rem;
308
+ }
309
+
310
+ .bc-panel-icon {
311
+ padding: 0.75rem;
312
+ background: #dbeafe;
313
+ border-radius: 0.75rem;
314
+ color: #2563eb;
315
+ font-size: 1.5rem;
316
+ display: flex;
317
+ flex-shrink: 0;
318
+ }
319
+
320
+ .bc-icon {
321
+ width: 1.5rem;
322
+ height: 1.5rem;
323
+ }
324
+
325
+ .bc-panel-title {
326
+ font-size: 1.25rem;
327
+ font-weight: 700;
328
+ color: #1e293b;
329
+ margin: 0;
330
+ }
331
+
332
+ .bc-panel-desc {
333
+ font-size: 0.875rem;
334
+ color: #94a3b8;
335
+ margin: 0;
336
+ }
337
+
338
+ .bc-inputs-grid {
339
+ display: grid;
340
+ grid-template-columns: 1fr;
341
+ gap: 1.5rem;
342
+ }
343
+
344
+ @media (min-width: 768px) {
345
+ .bc-inputs-grid {
346
+ grid-template-columns: 1fr 1fr 1fr;
347
+ }
348
+ }
349
+
350
+ .bc-input-group {
351
+ display: flex;
352
+ flex-direction: column;
353
+ gap: 0.5rem;
354
+ }
355
+
356
+ .bc-label {
357
+ display: block;
358
+ font-size: 0.875rem;
359
+ font-weight: 500;
360
+ color: #374151;
361
+ }
362
+
363
+ .bc-input-wrapper {
364
+ position: relative;
365
+ }
366
+
367
+ .bc-input {
368
+ display: block;
369
+ width: 100%;
370
+ padding: 0.75rem 2.5rem 0.75rem 1rem;
371
+ border-radius: 0.75rem;
372
+ background: #f8fafc;
373
+ border: 1px solid transparent;
374
+ color: #1e293b;
375
+ font-size: 1rem;
376
+ outline: none;
377
+ transition: border-color 0.2s, box-shadow 0.2s;
378
+ box-sizing: border-box;
379
+ }
380
+
381
+ .bc-input:focus {
382
+ border-color: #3b82f6;
383
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
384
+ }
385
+
386
+ .bc-unit-label {
387
+ position: absolute;
388
+ right: 1rem;
389
+ top: 50%;
390
+ transform: translateY(-50%);
391
+ color: #94a3b8;
392
+ font-size: 0.875rem;
393
+ pointer-events: none;
394
+ }
395
+
396
+ .bc-unit-switcher {
397
+ display: flex;
398
+ justify-content: flex-end;
399
+ margin-top: 1.5rem;
400
+ }
401
+
402
+ .bc-unit-toggle {
403
+ display: inline-flex;
404
+ background: #f1f5f9;
405
+ padding: 0.25rem;
406
+ border-radius: 0.5rem;
407
+ gap: 0.25rem;
408
+ }
409
+
410
+ .bc-unit-btn {
411
+ padding: 0.375rem 1rem;
412
+ border-radius: 0.375rem;
413
+ border: none;
414
+ font-size: 0.875rem;
415
+ font-weight: 500;
416
+ cursor: pointer;
417
+ transition: all 0.2s;
418
+ color: #64748b;
419
+ background: transparent;
420
+ }
421
+
422
+ [data-bc-unit][data-bc-active='true'] {
423
+ background: #fff;
424
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
425
+ color: #1e293b;
426
+ }
427
+
428
+ .bc-blueprint {
429
+ background: #1e3a8a;
430
+ border-radius: 1.5rem;
431
+ padding: 0.25rem;
432
+ box-shadow: 0 12px 40px rgba(30, 58, 138, 0.3);
433
+ overflow: hidden;
434
+ border: 4px solid rgba(255, 255, 255, 0.1);
435
+ position: relative;
436
+ }
437
+
438
+ .bc-blueprint-grid {
439
+ position: absolute;
440
+ inset: 0;
441
+ opacity: 0.1;
442
+ background-image: linear-gradient(#fff 1px, transparent 1px), linear-gradient(90deg, #fff 1px, transparent 1px);
443
+ background-size: 20px 20px;
444
+ pointer-events: none;
445
+ }
446
+
447
+ .bc-blueprint-inner {
448
+ position: relative;
449
+ padding: 1.5rem;
450
+ color: #bfdbfe;
451
+ }
452
+
453
+ @media (min-width: 768px) {
454
+ .bc-blueprint-inner {
455
+ padding: 2.5rem;
456
+ }
457
+ }
458
+
459
+ .bc-bp-header {
460
+ display: flex;
461
+ justify-content: space-between;
462
+ align-items: flex-start;
463
+ margin-bottom: 2rem;
464
+ padding-bottom: 1rem;
465
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
466
+ }
467
+
468
+ .bc-bp-plan-title {
469
+ font-size: 1.25rem;
470
+ letter-spacing: 0.1em;
471
+ text-transform: uppercase;
472
+ color: #fff;
473
+ margin: 0 0 0.25rem;
474
+ }
475
+
476
+ .bc-bp-ref {
477
+ font-size: 0.75rem;
478
+ opacity: 0.6;
479
+ margin: 0;
480
+ }
481
+
482
+ .bc-bp-count-block {
483
+ text-align: right;
484
+ }
485
+
486
+ .bc-bp-count {
487
+ font-size: 1.875rem;
488
+ font-weight: 700;
489
+ color: #fff;
490
+ margin: 0 0 0.25rem;
491
+ line-height: 1;
492
+ }
493
+
494
+ .bc-bp-count-label {
495
+ font-size: 0.625rem;
496
+ opacity: 0.6;
497
+ text-transform: uppercase;
498
+ letter-spacing: 0.1em;
499
+ margin: 0;
500
+ }
501
+
502
+ .bc-bp-stats {
503
+ display: grid;
504
+ grid-template-columns: 1fr 1fr;
505
+ gap: 1rem;
506
+ margin-bottom: 2rem;
507
+ }
508
+
509
+ @media (min-width: 768px) {
510
+ .bc-bp-stats {
511
+ grid-template-columns: 1fr 1fr 1fr 1fr;
512
+ }
513
+ }
514
+
515
+ .bc-stat-card {
516
+ background: rgba(255, 255, 255, 0.05);
517
+ backdrop-filter: blur(4px);
518
+ padding: 0.75rem;
519
+ border-radius: 0.375rem;
520
+ border: 1px solid rgba(255, 255, 255, 0.1);
521
+ }
522
+
523
+ .bc-stat-label {
524
+ display: block;
525
+ font-size: 0.625rem;
526
+ opacity: 0.5;
527
+ text-transform: uppercase;
528
+ letter-spacing: 0.05em;
529
+ margin-bottom: 0.25rem;
530
+ }
531
+
532
+ .bc-stat-value {
533
+ font-size: 1.125rem;
534
+ font-weight: 700;
535
+ color: #fff;
536
+ }
537
+
538
+ .bc-visualizer {
539
+ width: 100%;
540
+ height: 12rem;
541
+ border: 1px dashed rgba(255, 255, 255, 0.3);
542
+ border-radius: 0.375rem;
543
+ background: rgba(23, 37, 84, 0.5);
544
+ overflow: hidden;
545
+ position: relative;
546
+ }
547
+
548
+ @media (min-width: 768px) {
549
+ .bc-visualizer {
550
+ height: 16rem;
551
+ }
552
+ }
553
+
554
+ .bc-svg {
555
+ width: 100%;
556
+ height: 100%;
557
+ }
558
+
559
+ .bc-disclaimer {
560
+ text-align: center;
561
+ font-size: 0.75rem;
562
+ opacity: 0.4;
563
+ margin-top: 1rem;
564
+ margin-bottom: 0;
565
+ }
566
+
567
+ :global(.theme-dark) .bc-input-panel {
568
+ background: #18181b;
569
+ border-color: rgba(51, 65, 85, 0.4);
570
+ }
571
+
572
+ :global(.theme-dark) .bc-panel-icon {
573
+ background: rgba(30, 58, 138, 0.3);
574
+ }
575
+
576
+ :global(.theme-dark) .bc-panel-title {
577
+ color: #f1f5f9;
578
+ }
579
+
580
+ :global(.theme-dark) .bc-label {
581
+ color: #94a3b8;
582
+ }
583
+
584
+ :global(.theme-dark) .bc-input {
585
+ background: #27272a;
586
+ color: #f1f5f9;
587
+ }
588
+
589
+ :global(.theme-dark) .bc-input:focus {
590
+ border-color: #3b82f6;
591
+ }
592
+
593
+ :global(.theme-dark) .bc-unit-toggle {
594
+ background: #27272a;
595
+ }
596
+
597
+ :global(.theme-dark) [data-bc-unit][data-bc-active='true'] {
598
+ background: #3f3f46;
599
+ color: #f1f5f9;
600
+ }
601
+
602
+ :global(.theme-dark) .bc-blueprint {
603
+ background: #0f172a;
604
+ }
605
+ </style>