@jjlmoya/utils-home 1.30.0 → 1.31.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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +4 -1
  3. package/src/index.ts +1 -0
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/tileLayoutCalculator/bibliography.astro +14 -0
  7. package/src/tool/tileLayoutCalculator/bibliography.ts +10 -0
  8. package/src/tool/tileLayoutCalculator/component.astro +415 -0
  9. package/src/tool/tileLayoutCalculator/entry.ts +29 -0
  10. package/src/tool/tileLayoutCalculator/i18n/de.ts +208 -0
  11. package/src/tool/tileLayoutCalculator/i18n/en.ts +208 -0
  12. package/src/tool/tileLayoutCalculator/i18n/es.ts +208 -0
  13. package/src/tool/tileLayoutCalculator/i18n/fr.ts +208 -0
  14. package/src/tool/tileLayoutCalculator/i18n/id.ts +208 -0
  15. package/src/tool/tileLayoutCalculator/i18n/it.ts +208 -0
  16. package/src/tool/tileLayoutCalculator/i18n/ja.ts +208 -0
  17. package/src/tool/tileLayoutCalculator/i18n/ko.ts +208 -0
  18. package/src/tool/tileLayoutCalculator/i18n/nl.ts +208 -0
  19. package/src/tool/tileLayoutCalculator/i18n/pl.ts +208 -0
  20. package/src/tool/tileLayoutCalculator/i18n/pt.ts +208 -0
  21. package/src/tool/tileLayoutCalculator/i18n/ru.ts +208 -0
  22. package/src/tool/tileLayoutCalculator/i18n/sv.ts +208 -0
  23. package/src/tool/tileLayoutCalculator/i18n/tr.ts +208 -0
  24. package/src/tool/tileLayoutCalculator/i18n/zh.ts +208 -0
  25. package/src/tool/tileLayoutCalculator/index.ts +9 -0
  26. package/src/tool/tileLayoutCalculator/logic.ts +55 -0
  27. package/src/tool/tileLayoutCalculator/seo.astro +15 -0
  28. package/src/tool/tileLayoutCalculator/tile-layout-calculator.css +404 -0
  29. package/src/tool/tileLayoutCalculator/ui.ts +37 -0
  30. package/src/tools.ts +2 -0
@@ -0,0 +1,55 @@
1
+ export interface TileLayoutInput {
2
+ roomWidthM: number;
3
+ roomLengthM: number;
4
+ tileWidthM: number;
5
+ tileLengthM: number;
6
+ groutM: number;
7
+ wastePercent: number;
8
+ tilesPerBox: number;
9
+ pricePerBox: number;
10
+ }
11
+
12
+ export interface TileLayoutResult {
13
+ roomWidthM: number;
14
+ roomLengthM: number;
15
+ tileWidthM: number;
16
+ tileLengthM: number;
17
+ groutM: number;
18
+ roomArea: number;
19
+ cols: number;
20
+ rows: number;
21
+ exactTiles: number;
22
+ wasteTiles: number;
23
+ totalTiles: number;
24
+ boxes: number;
25
+ totalCost: number;
26
+ }
27
+
28
+ export function calculateTileLayout(i: TileLayoutInput): TileLayoutResult {
29
+ const cols = Math.ceil(i.roomWidthM / (i.tileWidthM + i.groutM));
30
+ const rows = Math.ceil(i.roomLengthM / (i.tileLengthM + i.groutM));
31
+ const exactTiles = cols * rows;
32
+ const wasteTiles = Math.ceil(exactTiles * i.wastePercent / 100);
33
+ const totalTiles = exactTiles + wasteTiles;
34
+ const boxes = Math.ceil(totalTiles / i.tilesPerBox);
35
+ const totalCost = boxes * i.pricePerBox;
36
+ return {
37
+ roomWidthM: i.roomWidthM,
38
+ roomLengthM: i.roomLengthM,
39
+ tileWidthM: i.tileWidthM,
40
+ tileLengthM: i.tileLengthM,
41
+ groutM: i.groutM,
42
+ roomArea: i.roomWidthM * i.roomLengthM,
43
+ cols,
44
+ rows,
45
+ exactTiles,
46
+ wasteTiles,
47
+ totalTiles,
48
+ boxes,
49
+ totalCost,
50
+ };
51
+ }
52
+
53
+ export function fmt0(n: number): string { return String(Math.round(n)); }
54
+ export function fmt1(n: number): string { return n.toFixed(1); }
55
+ export function fmt2(n: number): string { return n.toFixed(2); }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { tileLayoutCalculator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await tileLayoutCalculator.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,404 @@
1
+ .tile-wrapper {
2
+ --tile-p: #8b5cf6;
3
+ --tile-success: #22c55e;
4
+ --tile-warn: #ef4444;
5
+ --tile-success-glow: rgba(34, 197, 94, 0.35);
6
+ --tile-success-glow-0: rgba(34, 197, 94, 0);
7
+ --tile-warn-glow: rgba(239, 68, 68, 0.15);
8
+ --tile-stroke: rgba(140, 140, 170, 0.5);
9
+ --tile-bg-glow: rgba(230, 230, 240, 0.5);
10
+ --tile-rect-p: rgba(220, 220, 235, 0.95);
11
+ --tile-rect-s: rgba(195, 195, 215, 0.92);
12
+
13
+ width: 100%;
14
+ padding: 1rem 0;
15
+ }
16
+
17
+ .tile-card {
18
+ background: var(--bg-surface);
19
+ width: calc(100% - 24px);
20
+ max-width: 960px;
21
+ margin: 0 auto;
22
+ border-radius: 24px;
23
+ overflow: hidden;
24
+ display: flex;
25
+ flex-direction: column;
26
+ border: 1px solid var(--border-color);
27
+ color: var(--text-main);
28
+ position: relative;
29
+ }
30
+
31
+ @media (min-width: 768px) {
32
+ .tile-card {
33
+ flex-direction: row;
34
+ min-height: 560px;
35
+ }
36
+ }
37
+
38
+ .tile-left {
39
+ flex: 0 0 auto;
40
+ width: 100%;
41
+ padding: 32px;
42
+ border-bottom: 1px solid var(--border-color);
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 20px;
46
+ }
47
+
48
+ @media (min-width: 768px) {
49
+ .tile-left {
50
+ width: 380px;
51
+ border-bottom: none;
52
+ border-right: 1px solid var(--border-color);
53
+ }
54
+ }
55
+
56
+ .tile-right {
57
+ flex: 1;
58
+ background: var(--bg-page);
59
+ display: flex;
60
+ flex-direction: column;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: 28px;
64
+ padding: 40px 32px;
65
+ min-height: 360px;
66
+ }
67
+
68
+ .tile-section-title {
69
+ font-size: 0.75rem;
70
+ font-weight: 900;
71
+ text-transform: uppercase;
72
+ letter-spacing: 0.14em;
73
+ color: var(--tile-p);
74
+ margin: 0;
75
+ }
76
+
77
+ .tile-unit-toggle {
78
+ display: flex;
79
+ gap: 8px;
80
+ }
81
+
82
+ .tile-unit-btn {
83
+ padding: 6px 14px;
84
+ border-radius: 9999px;
85
+ border: 1px solid var(--border-color);
86
+ background: var(--bg-surface);
87
+ color: var(--text-muted);
88
+ font-size: 0.75rem;
89
+ font-weight: 700;
90
+ cursor: pointer;
91
+ transition: all 0.2s;
92
+ }
93
+
94
+ .tile-unit-btn:hover {
95
+ border-color: var(--tile-p);
96
+ color: var(--text-main);
97
+ }
98
+
99
+ .tile-unit-active {
100
+ background: var(--tile-p);
101
+ border-color: var(--tile-p);
102
+ color: #fff;
103
+ }
104
+
105
+ .tile-field {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 10px;
109
+ }
110
+
111
+ .tile-label {
112
+ font-size: 0.6875rem;
113
+ font-weight: 700;
114
+ text-transform: uppercase;
115
+ letter-spacing: 0.1em;
116
+ color: var(--text-muted);
117
+ }
118
+
119
+ .tile-number-row {
120
+ display: flex;
121
+ align-items: baseline;
122
+ gap: 8px;
123
+ }
124
+
125
+ .tile-number-input {
126
+ width: 100px;
127
+ font-size: 2.5rem;
128
+ font-weight: 900;
129
+ color: var(--text-main);
130
+ background: transparent;
131
+ border: none;
132
+ border-bottom: 2px solid var(--border-color);
133
+ padding: 4px 0;
134
+ outline: none;
135
+ transition: border-color 0.2s;
136
+ }
137
+
138
+ .tile-number-input:focus {
139
+ border-color: var(--tile-p);
140
+ }
141
+
142
+ .tile-price-input {
143
+ width: 90px;
144
+ font-size: 1.5rem;
145
+ font-weight: 900;
146
+ color: var(--text-main);
147
+ background: transparent;
148
+ border: none;
149
+ border-bottom: 2px solid var(--border-color);
150
+ padding: 4px 0;
151
+ outline: none;
152
+ transition: border-color 0.2s;
153
+ }
154
+
155
+ .tile-price-input:focus {
156
+ border-color: var(--tile-p);
157
+ }
158
+
159
+ .tile-number-unit {
160
+ font-size: 0.875rem;
161
+ color: var(--text-muted);
162
+ }
163
+
164
+ .tile-slider {
165
+ width: 100%;
166
+ height: 6px;
167
+ accent-color: var(--tile-p);
168
+ cursor: pointer;
169
+ border-radius: 9999px;
170
+ }
171
+
172
+ .tile-waste-desc {
173
+ font-size: 0.8125rem;
174
+ color: var(--text-muted);
175
+ margin: 0;
176
+ }
177
+
178
+ .tile-visual-title {
179
+ font-size: 0.625rem;
180
+ font-weight: 900;
181
+ text-transform: uppercase;
182
+ letter-spacing: 0.18em;
183
+ color: var(--tile-p);
184
+ }
185
+
186
+ .tile-pattern-toggle {
187
+ display: flex;
188
+ gap: 8px;
189
+ }
190
+
191
+ .tile-pattern-btn {
192
+ padding: 6px 14px;
193
+ border-radius: 9999px;
194
+ border: 1px solid var(--border-color);
195
+ background: var(--bg-surface);
196
+ color: var(--text-muted);
197
+ font-size: 0.75rem;
198
+ font-weight: 700;
199
+ cursor: pointer;
200
+ transition: all 0.2s;
201
+ }
202
+
203
+ .tile-pattern-btn:hover {
204
+ border-color: var(--tile-p);
205
+ color: var(--text-main);
206
+ }
207
+
208
+ .tile-pattern-active {
209
+ background: var(--tile-p);
210
+ border-color: var(--tile-p);
211
+ color: #fff;
212
+ }
213
+
214
+ .tile-svg-wrap {
215
+ width: 100%;
216
+ max-width: 420px;
217
+ aspect-ratio: 4 / 3;
218
+ background: var(--tile-bg-glow);
219
+ border-radius: 16px;
220
+ border: 1px solid var(--border-color);
221
+ position: relative;
222
+ overflow: hidden;
223
+ transition: border-color 0.3s, box-shadow 0.3s;
224
+ }
225
+
226
+ .tile-svg {
227
+ width: 100%;
228
+ height: 100%;
229
+ display: block;
230
+ overflow: visible;
231
+ }
232
+
233
+ .tile-rect-primary {
234
+ fill: var(--tile-rect-p);
235
+ }
236
+
237
+ .tile-rect-secondary {
238
+ fill: var(--tile-rect-s);
239
+ }
240
+
241
+ .tile-grid-bg {
242
+ stroke: var(--tile-stroke);
243
+ stroke-width: 0.15;
244
+ stroke-dasharray: 1 1;
245
+ opacity: 0.4;
246
+ }
247
+
248
+ .tile-dim-label {
249
+ position: absolute;
250
+ top: 10px;
251
+ right: 10px;
252
+ background: rgba(255, 255, 255, 0.85);
253
+ border: 1px solid var(--tile-stroke);
254
+ border-radius: 999px;
255
+ padding: 3px 12px;
256
+ font-size: 0.65rem;
257
+ font-weight: 700;
258
+ color: var(--tile-p);
259
+ pointer-events: none;
260
+ z-index: 2;
261
+ backdrop-filter: blur(4px);
262
+ letter-spacing: 0.02em;
263
+ }
264
+
265
+ .tile-result-badge {
266
+ font-size: 0.625rem;
267
+ font-weight: 900;
268
+ text-transform: uppercase;
269
+ letter-spacing: 0.18em;
270
+ color: var(--tile-p);
271
+ padding: 6px 14px;
272
+ border: 1px solid var(--tile-stroke);
273
+ border-radius: 9999px;
274
+ }
275
+
276
+ .tile-stats {
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 24px;
280
+ width: 100%;
281
+ max-width: 320px;
282
+ justify-content: center;
283
+ }
284
+
285
+ .tile-stat {
286
+ text-align: center;
287
+ }
288
+
289
+ .tile-stat-label {
290
+ font-size: 0.625rem;
291
+ font-weight: 900;
292
+ text-transform: uppercase;
293
+ letter-spacing: 0.12em;
294
+ color: var(--text-muted);
295
+ margin: 0 0 4px;
296
+ }
297
+
298
+ .tile-stat-value {
299
+ font-size: 1.25rem;
300
+ font-weight: 700;
301
+ color: var(--text-main);
302
+ margin: 0;
303
+ }
304
+
305
+ .tile-stat-divider {
306
+ width: 1px;
307
+ height: 36px;
308
+ background: var(--border-color);
309
+ flex-shrink: 0;
310
+ }
311
+
312
+ .tile-waste-row {
313
+ display: flex;
314
+ align-items: center;
315
+ gap: 10px;
316
+ padding: 10px 16px;
317
+ border-radius: 12px;
318
+ background: var(--tile-bg-glow);
319
+ border: 1px solid var(--tile-stroke);
320
+ width: 100%;
321
+ max-width: 320px;
322
+ }
323
+
324
+ .tile-waste-label {
325
+ font-size: 0.6875rem;
326
+ font-weight: 700;
327
+ text-transform: uppercase;
328
+ letter-spacing: 0.1em;
329
+ color: var(--text-muted);
330
+ margin: 0;
331
+ }
332
+
333
+ .tile-waste-value {
334
+ font-size: 1rem;
335
+ font-weight: 900;
336
+ color: var(--tile-p);
337
+ margin: 0;
338
+ }
339
+
340
+ @keyframes tile-pulse-success {
341
+ 0% {
342
+ transform: scale(1);
343
+ box-shadow: 0 0 0 0 var(--tile-success-glow);
344
+ }
345
+
346
+ 70% {
347
+ transform: scale(1.02);
348
+ box-shadow: 0 0 0 18px var(--tile-success-glow-0);
349
+ }
350
+
351
+ 100% {
352
+ transform: scale(1);
353
+ box-shadow: 0 0 0 0 var(--tile-success-glow-0);
354
+ }
355
+ }
356
+
357
+ @keyframes tile-shake {
358
+
359
+ 0%,
360
+ 100% {
361
+ transform: translateY(0);
362
+ }
363
+
364
+ 25% {
365
+ transform: translateY(5px);
366
+ }
367
+
368
+ 50% {
369
+ transform: translateY(-3px);
370
+ }
371
+
372
+ 75% {
373
+ transform: translateY(2px);
374
+ }
375
+ }
376
+
377
+ .tile-svg-wrap.tile-state-optimal {
378
+ border-color: var(--tile-success);
379
+ box-shadow: 0 0 30px var(--tile-success-glow);
380
+ animation: tile-pulse-success 1.4s ease infinite;
381
+ }
382
+
383
+ .tile-particle {
384
+ position: fixed;
385
+ pointer-events: none;
386
+ z-index: 9999;
387
+ font-size: 0.875rem;
388
+ font-weight: 900;
389
+ color: var(--tile-p);
390
+ text-shadow: 0 0 8px var(--tile-p);
391
+ animation: tile-particle-float 0.8s ease-out forwards;
392
+ }
393
+
394
+ @keyframes tile-particle-float {
395
+ 0% {
396
+ transform: translate(0, 0) scale(1);
397
+ opacity: 1;
398
+ }
399
+
400
+ 100% {
401
+ transform: translate(var(--tx), var(--ty)) scale(0);
402
+ opacity: 0;
403
+ }
404
+ }
@@ -0,0 +1,37 @@
1
+ export interface TileLayoutCalculatorUI extends Record<string, string> {
2
+ sectionTitle: string;
3
+ labelRoomWidth: string;
4
+ labelRoomLength: string;
5
+ labelTileWidth: string;
6
+ labelTileLength: string;
7
+ labelGrout: string;
8
+ labelWaste: string;
9
+ labelTilesPerBox: string;
10
+ labelPrice: string;
11
+ labelPattern: string;
12
+ unitMetricRoom: string;
13
+ unitImperialRoom: string;
14
+ unitMetricTile: string;
15
+ unitImperialTile: string;
16
+ unitGroutMetric: string;
17
+ unitGroutImperial: string;
18
+ unitPercent: string;
19
+ unitBoxes: string;
20
+ unitPrice: string;
21
+ resultBadge: string;
22
+ labelArea: string;
23
+ labelTiles: string;
24
+ labelBoxes: string;
25
+ labelCost: string;
26
+ labelWasteCount: string;
27
+ labelCuts: string;
28
+ currency: string;
29
+ btnMetric: string;
30
+ btnImperial: string;
31
+ btnPatternStraight: string;
32
+ btnPatternBrick: string;
33
+ btnPatternDiagonal: string;
34
+ badgeOptimal: string;
35
+ badgeWarning: string;
36
+ visualTitle: string;
37
+ }
package/src/tools.ts CHANGED
@@ -13,6 +13,7 @@ import { WALL_PAINTING_CALCULATOR_TOOL } from './tool/wallPaintingCalculator/ind
13
13
  import { VAMPIRE_DRAW_SIMULATOR_TOOL } from './tool/vampireDrawSimulator/index';
14
14
  import { DESK_ERGONOMICS_TOOL } from './tool/deskErgonomics/index';
15
15
  import { APPLIANCE_COST_CALCULATOR_TOOL } from './tool/applianceCostCalculator/index';
16
+ import { TILE_LAYOUT_CALCULATOR_TOOL } from './tool/tileLayoutCalculator/index';
16
17
 
17
18
  export const ALL_TOOLS: ToolDefinition[] = [
18
19
  QR_GENERATOR_TOOL,
@@ -28,5 +29,6 @@ export const ALL_TOOLS: ToolDefinition[] = [
28
29
  VAMPIRE_DRAW_SIMULATOR_TOOL,
29
30
  DESK_ERGONOMICS_TOOL,
30
31
  APPLIANCE_COST_CALCULATOR_TOOL,
32
+ TILE_LAYOUT_CALCULATOR_TOOL,
31
33
  ];
32
34