@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,552 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { FurnitureFitUI } from './ui';
4
+
5
+ interface Props {
6
+ ui?: Record<string, unknown>;
7
+ }
8
+ const { ui } = Astro.props;
9
+ const t = (ui ?? {}) as FurnitureFitUI;
10
+ ---
11
+
12
+ <div
13
+ class="ff-root"
14
+ data-ff-root
15
+ data-ff-yes={t.verdictYes}
16
+ data-ff-no={t.verdictNo}
17
+ data-ff-reason-direct={t.fitDirect}
18
+ data-ff-reason-diagonal3d={t.fitDiagonal3d}
19
+ data-ff-reason-diagonalplane={t.fitDiagonalPlane}
20
+ data-ff-reason-nope={t.fitNope}
21
+ >
22
+ <div class="ff-grid">
23
+ <div class="ff-col-left">
24
+ <div class="ff-card">
25
+ <h2 class="ff-card-title">
26
+ <Icon name="mdi:elevator-passenger" />
27
+ {t.conTitle}
28
+ </h2>
29
+ <div class="ff-inputs-grid">
30
+ <div class="ff-input-group">
31
+ <label class="ff-label">{t.labelWidth}</label>
32
+ <input type="number" data-ff-con-w data-ff-inp value="120" class="ff-input" />
33
+ </div>
34
+ <div class="ff-input-group">
35
+ <label class="ff-label">{t.labelHeight}</label>
36
+ <input type="number" data-ff-con-h data-ff-inp value="210" class="ff-input" />
37
+ </div>
38
+ <div class="ff-input-group">
39
+ <label class="ff-label">{t.labelDepth}</label>
40
+ <input type="number" data-ff-con-d data-ff-inp value="120" class="ff-input" />
41
+ </div>
42
+ </div>
43
+ <p class="ff-hint">{t.conHint}</p>
44
+ </div>
45
+
46
+ <div class="ff-card">
47
+ <h2 class="ff-card-title">
48
+ <Icon name="mdi:sofa" />
49
+ {t.objTitle}
50
+ </h2>
51
+ <div class="ff-inputs-grid">
52
+ <div class="ff-input-group">
53
+ <label class="ff-label">{t.labelLength}</label>
54
+ <input type="number" data-ff-obj-w data-ff-inp value="220" class="ff-input" />
55
+ </div>
56
+ <div class="ff-input-group">
57
+ <label class="ff-label">{t.labelHeight}</label>
58
+ <input type="number" data-ff-obj-h data-ff-inp value="90" class="ff-input" />
59
+ </div>
60
+ <div class="ff-input-group">
61
+ <label class="ff-label">{t.labelDepth}</label>
62
+ <input type="number" data-ff-obj-d data-ff-inp value="80" class="ff-input" />
63
+ </div>
64
+ </div>
65
+ <div class="ff-margin-row">
66
+ <div class="ff-margin-left">
67
+ <label class="ff-label">{t.labelMargin}</label>
68
+ <input type="range" data-ff-margin data-ff-inp min="0" max="10" value="2" class="ff-slider" />
69
+ </div>
70
+ <span class="ff-margin-display" data-ff-margin-display>2cm</span>
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="ff-col-right">
76
+ <div class="ff-visual-container">
77
+ <div class="ff-perspective">
78
+ <div class="ff-visual" data-ff-visual>
79
+ <div class="ff-space-box" data-ff-space-box></div>
80
+ <div class="ff-obj-box" data-ff-obj-box>{t.objLabel}</div>
81
+ </div>
82
+ </div>
83
+ <div class="ff-viz-label">
84
+ <Icon name="mdi:cube-outline" />
85
+ <span>{t.vizLabel}</span>
86
+ </div>
87
+ </div>
88
+
89
+ <div class="ff-verdict" data-ff-verdict>
90
+ <div class="ff-verdict-inner">
91
+ <div class="ff-verdict-icon-wrap">
92
+ <Icon name="mdi:check" />
93
+ </div>
94
+ <div>
95
+ <h3 class="ff-verdict-title" data-ff-verdict-title>{t.verdictDefault}</h3>
96
+ <p class="ff-verdict-reason" data-ff-verdict-reason>{t.verdictHint}</p>
97
+ </div>
98
+ </div>
99
+ <div class="ff-diag-info ff-hidden" data-ff-diag-info>
100
+ <span>{t.diagLabel}</span>
101
+ <span class="ff-diag-val" data-ff-diag-val>0cm</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <script>
109
+ import { calculateFit } from './engine';
110
+ import type { Dimensions, FitResult } from './engine';
111
+
112
+ interface FfDims {
113
+ obj: Dimensions;
114
+ con: Dimensions;
115
+ margin: number;
116
+ }
117
+
118
+ function ffNum(el: Element | null): number {
119
+ if (!(el instanceof HTMLInputElement)) return 0;
120
+ return parseFloat(el.value) || 0;
121
+ }
122
+
123
+ function ffAttr(root: Element, key: string): string {
124
+ return root.getAttribute(key) ?? '';
125
+ }
126
+
127
+ function ffReadDims(root: Element): FfDims {
128
+ return {
129
+ obj: { width: ffNum(root.querySelector('[data-ff-obj-w]')), height: ffNum(root.querySelector('[data-ff-obj-h]')), depth: ffNum(root.querySelector('[data-ff-obj-d]')) },
130
+ con: { width: ffNum(root.querySelector('[data-ff-con-w]')), height: ffNum(root.querySelector('[data-ff-con-h]')), depth: ffNum(root.querySelector('[data-ff-con-d]')) },
131
+ margin: ffNum(root.querySelector('[data-ff-margin]')),
132
+ };
133
+ }
134
+
135
+ function ffUpdateVerdict(root: Element, result: FitResult): void {
136
+ const card = root.querySelector('[data-ff-verdict]');
137
+ const title = root.querySelector('[data-ff-verdict-title]');
138
+ const reason = root.querySelector('[data-ff-verdict-reason]');
139
+ if (card) card.setAttribute('data-ff-fits', result.fits ? 'yes' : 'no');
140
+ if (title) title.textContent = result.fits ? ffAttr(root, 'data-ff-yes') : ffAttr(root, 'data-ff-no');
141
+ if (reason) reason.textContent = ffAttr(root, `data-ff-reason-${result.type}`);
142
+ }
143
+
144
+ function ffUpdateDiag(root: Element, result: FitResult): void {
145
+ const info = root.querySelector('[data-ff-diag-info]');
146
+ const val = root.querySelector('[data-ff-diag-val]');
147
+ const show = result.maxDiagonal > 0;
148
+ if (!info) return;
149
+ info.classList.toggle('ff-hidden', !show);
150
+ if (show && val) val.textContent = `${result.maxDiagonal.toFixed(1)}cm`;
151
+ }
152
+
153
+ function ffApplyBoxSizes(spaceBox: HTMLElement, objBox: HTMLElement, dims: FfDims, scale: number): void {
154
+ spaceBox.style.width = `${dims.con.width * scale}px`;
155
+ spaceBox.style.height = `${dims.con.height * scale}px`;
156
+ objBox.style.width = `${dims.obj.width * scale}px`;
157
+ objBox.style.height = `${dims.obj.height * scale}px`;
158
+ }
159
+
160
+ function ffApplyBoxStyle(objBox: HTMLElement, result: FitResult, conH: number, conW: number): void {
161
+ const isDiag = result.type === 'diagonal3d' || result.type === 'diagonalplane';
162
+ const angle = isDiag ? Math.atan2(conH, conW) * (180 / Math.PI) : 0;
163
+ objBox.style.transform = `translateX(-50%) translateY(-50%) rotate(-${angle}deg)`;
164
+ objBox.style.background = result.fits ? 'rgba(16, 185, 129, 0.4)' : 'rgba(244, 63, 94, 0.6)';
165
+ objBox.style.borderColor = result.fits ? 'rgba(16, 185, 129, 0.8)' : 'rgba(244, 63, 94, 0.8)';
166
+ }
167
+
168
+ function ffUpdateBoxes(root: Element, result: FitResult): void {
169
+ const dims = ffReadDims(root);
170
+ const max = Math.max(dims.con.width, dims.con.height, dims.con.depth, dims.obj.width, dims.obj.height, dims.obj.depth);
171
+ const scale = 200 / (max || 1);
172
+ const spaceBox = root.querySelector('[data-ff-space-box]') as HTMLElement | null;
173
+ const objBox = root.querySelector('[data-ff-obj-box]') as HTMLElement | null;
174
+ if (!spaceBox || !objBox) return;
175
+ ffApplyBoxSizes(spaceBox, objBox, dims, scale);
176
+ ffApplyBoxStyle(objBox, result, dims.con.height, dims.con.width);
177
+ }
178
+
179
+ function ffCalc(root: Element): void {
180
+ const dims = ffReadDims(root);
181
+ const display = root.querySelector('[data-ff-margin-display]');
182
+ if (display) display.textContent = `${dims.margin.toFixed(0)}cm`;
183
+ const result = calculateFit(dims.obj, dims.con, dims.margin);
184
+ ffUpdateVerdict(root, result);
185
+ ffUpdateDiag(root, result);
186
+ ffUpdateBoxes(root, result);
187
+ }
188
+
189
+ function ffAttach(root: Element): void {
190
+ root.querySelectorAll('[data-ff-inp]').forEach((el) => el.addEventListener('input', () => ffCalc(root)));
191
+ const vc = root.querySelector('[data-ff-visual]') as HTMLElement | null;
192
+ window.addEventListener('mousemove', (e) => {
193
+ if (!vc) return;
194
+ const x = (e.clientX / window.innerWidth - 0.5) * 40;
195
+ const y = (e.clientY / window.innerHeight - 0.5) * -40;
196
+ vc.style.transform = `rotateX(${12 + y}deg) rotateY(${-12 + x}deg)`;
197
+ });
198
+ }
199
+
200
+ function ffInit(root: Element): void {
201
+ ffAttach(root);
202
+ ffCalc(root);
203
+ }
204
+
205
+ document.querySelectorAll('[data-ff-root]').forEach(ffInit);
206
+ </script>
207
+
208
+ <style>
209
+ .ff-root {
210
+ max-width: 1000px;
211
+ margin: 0 auto;
212
+ }
213
+
214
+ .ff-grid {
215
+ display: grid;
216
+ grid-template-columns: 1fr 1fr;
217
+ gap: 3rem;
218
+ align-items: start;
219
+ }
220
+
221
+ .ff-col-left {
222
+ display: flex;
223
+ flex-direction: column;
224
+ gap: 2rem;
225
+ }
226
+
227
+ .ff-col-right {
228
+ display: flex;
229
+ flex-direction: column;
230
+ gap: 2rem;
231
+ }
232
+
233
+ .ff-card {
234
+ background: #fff;
235
+ border-radius: 1.5rem;
236
+ padding: 2rem;
237
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
238
+ border: 1px solid #f1f5f9;
239
+ }
240
+
241
+ :global(.theme-dark) .ff-card {
242
+ background: #0f172a;
243
+ border-color: #1e293b;
244
+ }
245
+
246
+ .ff-card-title {
247
+ font-size: 1.125rem;
248
+ font-weight: 700;
249
+ color: #1e293b;
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 0.5rem;
253
+ margin: 0 0 1.5rem;
254
+ }
255
+
256
+ :global(.theme-dark) .ff-card-title {
257
+ color: #fff;
258
+ }
259
+
260
+ .ff-card-title svg {
261
+ width: 1.5rem;
262
+ height: 1.5rem;
263
+ }
264
+
265
+ .ff-card-title svg:first-child {
266
+ color: #6366f1;
267
+ }
268
+
269
+ .ff-inputs-grid {
270
+ display: grid;
271
+ grid-template-columns: repeat(3, 1fr);
272
+ gap: 1rem;
273
+ margin-bottom: 1rem;
274
+ }
275
+
276
+ .ff-input-group {
277
+ display: flex;
278
+ flex-direction: column;
279
+ gap: 0.5rem;
280
+ }
281
+
282
+ .ff-label {
283
+ font-size: 0.625rem;
284
+ font-weight: 700;
285
+ text-transform: uppercase;
286
+ letter-spacing: 0.05em;
287
+ color: #94a3b8;
288
+ }
289
+
290
+ .ff-input {
291
+ width: 100%;
292
+ background: #f8fafc;
293
+ border: none;
294
+ border-radius: 0.75rem;
295
+ padding: 0.75rem 1rem;
296
+ font-size: 1rem;
297
+ font-weight: 700;
298
+ color: #1e293b;
299
+ outline: none;
300
+ transition: box-shadow 0.15s ease;
301
+ box-sizing: border-box;
302
+ }
303
+
304
+ .ff-input:focus {
305
+ box-shadow: 0 0 0 2px #6366f1;
306
+ }
307
+
308
+ :global(.theme-dark) .ff-input {
309
+ background: #1e293b;
310
+ color: #fff;
311
+ }
312
+
313
+ .ff-input::-webkit-outer-spin-button,
314
+ .ff-input::-webkit-inner-spin-button {
315
+ appearance: none;
316
+ }
317
+
318
+ .ff-input[type="number"] {
319
+ appearance: textfield;
320
+ }
321
+
322
+ .ff-hint {
323
+ font-size: 0.625rem;
324
+ color: #94a3b8;
325
+ font-style: italic;
326
+ margin: 0;
327
+ }
328
+
329
+ .ff-margin-row {
330
+ display: flex;
331
+ align-items: center;
332
+ gap: 1rem;
333
+ padding-top: 1rem;
334
+ border-top: 1px solid #f1f5f9;
335
+ margin-top: 0.5rem;
336
+ }
337
+
338
+ :global(.theme-dark) .ff-margin-row {
339
+ border-color: #1e293b;
340
+ }
341
+
342
+ .ff-margin-left {
343
+ flex: 1;
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 0.5rem;
347
+ }
348
+
349
+ .ff-slider {
350
+ width: 100%;
351
+ accent-color: #6366f1;
352
+ cursor: pointer;
353
+ }
354
+
355
+ .ff-margin-display {
356
+ font-size: 0.75rem;
357
+ font-weight: 700;
358
+ color: #64748b;
359
+ min-width: 2.5rem;
360
+ text-align: right;
361
+ }
362
+
363
+ .ff-visual-container {
364
+ aspect-ratio: 1;
365
+ background: #f8fafc;
366
+ border-radius: 2.5rem;
367
+ border: 1px solid #e2e8f0;
368
+ position: relative;
369
+ overflow: hidden;
370
+ }
371
+
372
+ :global(.theme-dark) .ff-visual-container {
373
+ background: #020617;
374
+ border-color: #1e293b;
375
+ }
376
+
377
+ .ff-perspective {
378
+ position: absolute;
379
+ inset: 0;
380
+ display: flex;
381
+ align-items: center;
382
+ justify-content: center;
383
+ padding: 3rem;
384
+ perspective: 1000px;
385
+ }
386
+
387
+ .ff-visual {
388
+ position: relative;
389
+ width: 100%;
390
+ height: 100%;
391
+ transform-style: preserve-3d;
392
+ transform: rotateX(12deg) rotateY(-12deg);
393
+ transition: transform 0.3s ease;
394
+ display: flex;
395
+ align-items: center;
396
+ justify-content: center;
397
+ }
398
+
399
+ .ff-space-box {
400
+ position: absolute;
401
+ top: 50%;
402
+ left: 50%;
403
+ transform: translateX(-50%) translateY(-50%);
404
+ border: 2px dashed #cbd5e1;
405
+ background: rgba(255, 255, 255, 0.05);
406
+ border-radius: 0.5rem;
407
+ transition:
408
+ width 0.5s ease,
409
+ height 0.5s ease;
410
+ width: 200px;
411
+ height: 200px;
412
+ }
413
+
414
+ :global(.theme-dark) .ff-space-box {
415
+ border-color: #334155;
416
+ }
417
+
418
+ .ff-obj-box {
419
+ position: absolute;
420
+ top: 50%;
421
+ left: 50%;
422
+ transform: translateX(-50%) translateY(-50%);
423
+ background: rgba(99, 102, 241, 0.5);
424
+ border: 1px solid rgba(99, 102, 241, 0.8);
425
+ border-radius: 0.5rem;
426
+ display: flex;
427
+ align-items: center;
428
+ justify-content: center;
429
+ font-size: 0.5625rem;
430
+ font-weight: 900;
431
+ color: rgba(255, 255, 255, 0.4);
432
+ text-transform: uppercase;
433
+ letter-spacing: 0.1em;
434
+ overflow: hidden;
435
+ transition:
436
+ width 0.5s ease,
437
+ height 0.5s ease,
438
+ background 0.5s ease,
439
+ border-color 0.5s ease;
440
+ width: 160px;
441
+ height: 80px;
442
+ }
443
+
444
+ .ff-viz-label {
445
+ position: absolute;
446
+ bottom: 1.5rem;
447
+ left: 1.5rem;
448
+ display: flex;
449
+ align-items: center;
450
+ gap: 0.5rem;
451
+ font-size: 0.5625rem;
452
+ font-weight: 900;
453
+ text-transform: uppercase;
454
+ letter-spacing: 0.1em;
455
+ color: #94a3b8;
456
+ }
457
+
458
+ .ff-viz-label svg {
459
+ width: 1.25rem;
460
+ height: 1.25rem;
461
+ }
462
+
463
+ .ff-verdict {
464
+ background: #f1f5f9;
465
+ border-left: 8px solid #94a3b8;
466
+ border-radius: 1.5rem;
467
+ padding: 2rem;
468
+ color: #1e293b;
469
+ transition: all 0.5s ease;
470
+ overflow: hidden;
471
+ position: relative;
472
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
473
+ }
474
+
475
+ :global(.theme-dark) .ff-verdict {
476
+ background: #1e293b;
477
+ border-color: #475569;
478
+ color: #fff;
479
+ }
480
+
481
+ .ff-verdict[data-ff-fits="yes"] {
482
+ background: #10b981;
483
+ border-color: #059669;
484
+ color: #fff;
485
+ box-shadow: 0 8px 32px rgba(16, 185, 129, 0.3);
486
+ }
487
+
488
+ .ff-verdict[data-ff-fits="no"] {
489
+ background: #f43f5e;
490
+ border-color: #e11d48;
491
+ color: #fff;
492
+ box-shadow: 0 8px 32px rgba(244, 63, 94, 0.3);
493
+ }
494
+
495
+ .ff-verdict-inner {
496
+ display: flex;
497
+ align-items: flex-start;
498
+ gap: 1rem;
499
+ }
500
+
501
+ .ff-verdict-icon-wrap {
502
+ padding: 1rem;
503
+ border-radius: 1rem;
504
+ background: rgba(255, 255, 255, 0.2);
505
+ flex-shrink: 0;
506
+ }
507
+
508
+ .ff-verdict-icon-wrap svg {
509
+ width: 2.5rem;
510
+ height: 2.5rem;
511
+ }
512
+
513
+ .ff-verdict-title {
514
+ font-size: 1.5rem;
515
+ font-weight: 900;
516
+ margin: 0 0 0.25rem;
517
+ line-height: 1.2;
518
+ }
519
+
520
+ .ff-verdict-reason {
521
+ font-size: 0.875rem;
522
+ opacity: 0.9;
523
+ line-height: 1.5;
524
+ margin: 0;
525
+ }
526
+
527
+ .ff-diag-info {
528
+ display: flex;
529
+ justify-content: space-between;
530
+ align-items: center;
531
+ font-size: 0.75rem;
532
+ opacity: 0.7;
533
+ margin-top: 1.5rem;
534
+ padding-top: 1.5rem;
535
+ border-top: 1px solid rgba(255, 255, 255, 0.15);
536
+ }
537
+
538
+ .ff-diag-val {
539
+ font-weight: 700;
540
+ }
541
+
542
+ .ff-hidden {
543
+ display: none;
544
+ }
545
+
546
+ @media (max-width: 768px) {
547
+ .ff-grid {
548
+ grid-template-columns: 1fr;
549
+ gap: 2rem;
550
+ }
551
+ }
552
+ </style>
@@ -0,0 +1,51 @@
1
+ export type FitType = 'direct' | 'diagonal3d' | 'diagonalplane' | 'nope';
2
+
3
+ export interface Dimensions {
4
+ width: number;
5
+ height: number;
6
+ depth: number;
7
+ }
8
+
9
+ export interface FitResult {
10
+ fits: boolean;
11
+ type: FitType;
12
+ clearance: number;
13
+ maxDiagonal: number;
14
+ }
15
+
16
+ function ffFitsDirect(obj: number[], con: number[]): boolean {
17
+ return obj[0] <= con[0] && obj[1] <= con[1] && obj[2] <= con[2];
18
+ }
19
+
20
+ function ffFits3D(objMax: number, objOther: number[], con3D: number, conOther: number[]): boolean {
21
+ return objMax <= con3D && objOther[0] <= conOther[0] && objOther[1] <= conOther[1];
22
+ }
23
+
24
+ function ffFitsPlane(objMax: number, maxPlane: number, obj0: number, con0: number): boolean {
25
+ return objMax <= maxPlane && obj0 <= con0;
26
+ }
27
+
28
+ export function calculateFit(object: Dimensions, container: Dimensions, margin: number): FitResult {
29
+ const adjW = container.width - margin;
30
+ const adjH = container.height - margin;
31
+ const adjD = container.depth - margin;
32
+ const objDims = [object.width, object.height, object.depth].sort((a, b) => a - b);
33
+ const conDims = [adjW, adjH, adjD].sort((a, b) => a - b);
34
+ if (ffFitsDirect(objDims, conDims)) {
35
+ const clearance = Math.min(conDims[0] - objDims[0], conDims[1] - objDims[1], conDims[2] - objDims[2]);
36
+ return { fits: true, type: 'direct', clearance, maxDiagonal: 0 };
37
+ }
38
+ const con3D = Math.sqrt(adjW ** 2 + adjH ** 2 + adjD ** 2);
39
+ const objMax = Math.max(...objDims);
40
+ const objOther = objDims.slice(0, 2);
41
+ const conOther = conDims.slice(0, 2);
42
+ if (ffFits3D(objMax, objOther, con3D, conOther)) {
43
+ return { fits: true, type: 'diagonal3d', clearance: con3D - objMax, maxDiagonal: con3D };
44
+ }
45
+ const planes = [Math.sqrt(adjW ** 2 + adjH ** 2), Math.sqrt(adjW ** 2 + adjD ** 2), Math.sqrt(adjH ** 2 + adjD ** 2)];
46
+ const maxPlane = Math.max(...planes);
47
+ if (ffFitsPlane(objMax, maxPlane, objOther[0], conOther[0])) {
48
+ return { fits: true, type: 'diagonalplane', clearance: maxPlane - objMax, maxDiagonal: maxPlane };
49
+ }
50
+ return { fits: false, type: 'nope', clearance: 0, maxDiagonal: con3D };
51
+ }