@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,741 @@
1
+ ---
2
+ import type { PassepartoutCalculatorUI } from './ui';
3
+
4
+ interface Props {
5
+ ui?: Record<string, unknown>;
6
+ }
7
+ const { ui } = Astro.props;
8
+ const t = (ui ?? {}) as PassepartoutCalculatorUI;
9
+ ---
10
+
11
+ <div class="pp-root" data-pp-root>
12
+ <div class="pp-grid">
13
+ <div class="pp-inputs">
14
+ <div class="pp-section-header">
15
+ <h2 class="pp-section-title">{t.sectionTitle}</h2>
16
+ <p class="pp-section-desc">{t.sectionDesc}</p>
17
+ </div>
18
+
19
+ <div class="pp-field-group">
20
+ <div class="pp-group-label pp-label-frame">
21
+ <svg viewBox="0 0 24 24" class="pp-group-icon" aria-hidden="true">
22
+ <path fill="currentColor" d="M3 3h18v18H3V3m2 2v14h14V5H5Z"/>
23
+ </svg>
24
+ <span>{t.frameSizeTitle}</span>
25
+ </div>
26
+ <div class="pp-row">
27
+ <div class="pp-field">
28
+ <label class="pp-label">{t.labelWidth}</label>
29
+ <input type="number" data-pp-input data-pp-frame-w class="pp-input" value="40" min="0" step="0.1" />
30
+ </div>
31
+ <div class="pp-field">
32
+ <label class="pp-label">{t.labelHeight}</label>
33
+ <input type="number" data-pp-input data-pp-frame-h class="pp-input" value="50" min="0" step="0.1" />
34
+ </div>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="pp-field-group">
39
+ <div class="pp-group-label pp-label-art">
40
+ <svg viewBox="0 0 24 24" class="pp-group-icon" aria-hidden="true">
41
+ <path fill="currentColor" d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m0 2v14h14V5H5m3 3h8v2H8V8m0 4h8v2H8v-2"/>
42
+ </svg>
43
+ <span>{t.artSizeTitle}</span>
44
+ </div>
45
+ <div class="pp-row">
46
+ <div class="pp-field">
47
+ <label class="pp-label">{t.labelWidth}</label>
48
+ <input type="number" data-pp-input data-pp-art-w class="pp-input" value="28" min="0" step="0.1" />
49
+ </div>
50
+ <div class="pp-field">
51
+ <label class="pp-label">{t.labelHeight}</label>
52
+ <input type="number" data-pp-input data-pp-art-h class="pp-input" value="38" min="0" step="0.1" />
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <div class="pp-toggle-row">
58
+ <label class="pp-toggle-label">
59
+ <div class="pp-toggle-text">
60
+ <span class="pp-toggle-name">{t.labelBottomWeighting}</span>
61
+ <span class="pp-toggle-desc">{t.descBottomWeighting}</span>
62
+ </div>
63
+ <input type="checkbox" data-pp-input data-pp-weighted class="pp-checkbox" checked />
64
+ <span class="pp-toggle-track" aria-hidden="true"></span>
65
+ </label>
66
+ </div>
67
+
68
+ <div class="pp-offset-ctrl" data-pp-offset-ctrl>
69
+ <label class="pp-label">{t.labelOffset}</label>
70
+ <div class="pp-offset-row">
71
+ <input type="range" data-pp-input data-pp-offset-range class="pp-range" min="1" max="20" value="5" />
72
+ <span data-pp-offset-val class="pp-offset-val">5%</span>
73
+ </div>
74
+ </div>
75
+
76
+ <div class="pp-error" data-pp-error>
77
+ <svg viewBox="0 0 24 24" class="pp-error-icon" aria-hidden="true">
78
+ <path fill="currentColor" d="M13 13H11V7H13M13 17H11V15H13M12 2A10 10 0 0 0 2 12A10 10 0 0 0 12 22A10 10 0 0 0 22 12A10 10 0 0 0 12 2Z"/>
79
+ </svg>
80
+ <span>{t.errorMsg}</span>
81
+ </div>
82
+ </div>
83
+
84
+ <div class="pp-preview">
85
+ <div class="pp-dots-bg" aria-hidden="true"></div>
86
+ <div class="pp-frame-wrapper">
87
+ <div class="pp-border-label pp-border-top">
88
+ <span class="pp-border-tag">{t.labelTop}</span>
89
+ <span data-pp-top class="pp-border-val">6.00</span>
90
+ <span class="pp-border-unit">cm</span>
91
+ </div>
92
+ <div class="pp-border-label pp-border-bottom">
93
+ <span class="pp-border-unit">cm</span>
94
+ <span data-pp-bottom class="pp-border-val">6.00</span>
95
+ <span class="pp-border-tag">{t.labelBottom}</span>
96
+ </div>
97
+ <div class="pp-border-label pp-border-left">
98
+ <span class="pp-border-tag">{t.labelLeft}</span>
99
+ <span data-pp-left class="pp-border-val">6.00</span>
100
+ <span class="pp-border-unit">cm</span>
101
+ </div>
102
+ <div class="pp-border-label pp-border-right">
103
+ <span class="pp-border-tag">{t.labelRight}</span>
104
+ <span data-pp-right class="pp-border-val">6.00</span>
105
+ <span class="pp-border-unit">cm</span>
106
+ </div>
107
+ <div class="pp-frame" data-pp-frame style="aspect-ratio: 40/50;">
108
+ <div class="pp-mat"></div>
109
+ <div class="pp-art" data-pp-art>
110
+ <span class="pp-art-placeholder">{t.artPlaceholder}</span>
111
+ <div class="pp-art-dim pp-art-dim-w">
112
+ <div class="pp-dim-line"></div>
113
+ <span data-pp-art-w-display class="pp-dim-label">28cm</span>
114
+ </div>
115
+ <div class="pp-art-dim pp-art-dim-h">
116
+ <div class="pp-dim-line pp-dim-line-v"></div>
117
+ <span data-pp-art-h-display class="pp-dim-label pp-dim-label-v">38cm</span>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ <style>
127
+ .pp-root {
128
+ width: 100%;
129
+ max-width: 1100px;
130
+ margin: 0 auto;
131
+ background: #fff;
132
+ border: 1px solid #e2e8f0;
133
+ border-radius: 1.5rem;
134
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
135
+ overflow: hidden;
136
+ }
137
+
138
+ :global(.theme-dark) .pp-root {
139
+ background: #1e1e2e;
140
+ border-color: #2d2d3f;
141
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
142
+ }
143
+
144
+ .pp-grid {
145
+ display: grid;
146
+ grid-template-columns: 1fr;
147
+ }
148
+
149
+ @media (min-width: 1024px) {
150
+ .pp-grid {
151
+ grid-template-columns: 5fr 8fr;
152
+ }
153
+ }
154
+
155
+ .pp-inputs {
156
+ padding: 2rem;
157
+ background: #f8fafc;
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 1.5rem;
161
+ border-right: 1px solid #e2e8f0;
162
+ }
163
+
164
+ :global(.theme-dark) .pp-inputs {
165
+ background: #16161e;
166
+ border-right-color: #2d2d3f;
167
+ }
168
+
169
+ .pp-section-header {
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 0.25rem;
173
+ }
174
+
175
+ .pp-section-title {
176
+ font-size: 1.25rem;
177
+ font-weight: 700;
178
+ color: #0f172a;
179
+ display: flex;
180
+ align-items: center;
181
+ gap: 0.5rem;
182
+ }
183
+
184
+ :global(.theme-dark) .pp-section-title {
185
+ color: #e2e8f0;
186
+ }
187
+
188
+ .pp-section-desc {
189
+ font-size: 0.875rem;
190
+ color: #64748b;
191
+ }
192
+
193
+ :global(.theme-dark) .pp-section-desc {
194
+ color: #94a3b8;
195
+ }
196
+
197
+ .pp-field-group {
198
+ display: flex;
199
+ flex-direction: column;
200
+ gap: 0.75rem;
201
+ }
202
+
203
+ .pp-group-label {
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 0.5rem;
207
+ font-size: 0.875rem;
208
+ font-weight: 600;
209
+ }
210
+
211
+ .pp-label-frame {
212
+ color: #4f46e5;
213
+ }
214
+
215
+ :global(.theme-dark) .pp-label-frame {
216
+ color: #818cf8;
217
+ }
218
+
219
+ .pp-label-art {
220
+ color: #db2777;
221
+ }
222
+
223
+ :global(.theme-dark) .pp-label-art {
224
+ color: #f472b6;
225
+ }
226
+
227
+ .pp-group-icon {
228
+ width: 1.25rem;
229
+ height: 1.25rem;
230
+ }
231
+
232
+ .pp-row {
233
+ display: grid;
234
+ grid-template-columns: 1fr 1fr;
235
+ gap: 1rem;
236
+ }
237
+
238
+ .pp-field {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: 0.25rem;
242
+ }
243
+
244
+ .pp-label {
245
+ font-size: 0.75rem;
246
+ color: #64748b;
247
+ margin-left: 0.25rem;
248
+ }
249
+
250
+ :global(.theme-dark) .pp-label {
251
+ color: #94a3b8;
252
+ }
253
+
254
+ .pp-input {
255
+ width: 100%;
256
+ background: #fff;
257
+ border: 1px solid #cbd5e1;
258
+ border-radius: 0.75rem;
259
+ padding: 0.75rem 1rem;
260
+ color: #0f172a;
261
+ font-size: 1.125rem;
262
+ transition: border-color 0.2s, box-shadow 0.2s;
263
+ box-sizing: border-box;
264
+ }
265
+
266
+ .pp-input:focus {
267
+ outline: none;
268
+ border-color: #4f46e5;
269
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
270
+ }
271
+
272
+ :global(.theme-dark) .pp-input {
273
+ background: #1e1e2e;
274
+ border-color: #3d3d50;
275
+ color: #e2e8f0;
276
+ }
277
+
278
+ :global(.theme-dark) .pp-input:focus {
279
+ border-color: #818cf8;
280
+ box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.15);
281
+ }
282
+
283
+ .pp-toggle-row {
284
+ padding-top: 1rem;
285
+ border-top: 1px solid #e2e8f0;
286
+ }
287
+
288
+ :global(.theme-dark) .pp-toggle-row {
289
+ border-top-color: #2d2d3f;
290
+ }
291
+
292
+ .pp-toggle-label {
293
+ display: flex;
294
+ align-items: center;
295
+ justify-content: space-between;
296
+ cursor: pointer;
297
+ gap: 1rem;
298
+ }
299
+
300
+ .pp-toggle-text {
301
+ display: flex;
302
+ flex-direction: column;
303
+ gap: 0.125rem;
304
+ }
305
+
306
+ .pp-toggle-name {
307
+ font-size: 0.9375rem;
308
+ font-weight: 600;
309
+ color: #0f172a;
310
+ }
311
+
312
+ :global(.theme-dark) .pp-toggle-name {
313
+ color: #e2e8f0;
314
+ }
315
+
316
+ .pp-toggle-desc {
317
+ font-size: 0.75rem;
318
+ color: #64748b;
319
+ }
320
+
321
+ :global(.theme-dark) .pp-toggle-desc {
322
+ color: #94a3b8;
323
+ }
324
+
325
+ .pp-checkbox {
326
+ width: 3rem;
327
+ height: 1.75rem;
328
+ appearance: none;
329
+ background: #cbd5e1;
330
+ border-radius: 9999px;
331
+ position: relative;
332
+ cursor: pointer;
333
+ transition: background 0.3s;
334
+ flex-shrink: 0;
335
+ }
336
+
337
+ .pp-checkbox:checked {
338
+ background: #4f46e5;
339
+ }
340
+
341
+ .pp-checkbox::after {
342
+ content: '';
343
+ position: absolute;
344
+ width: 1.375rem;
345
+ height: 1.375rem;
346
+ background: #fff;
347
+ border-radius: 50%;
348
+ top: 0.1875rem;
349
+ left: 0.1875rem;
350
+ transition: left 0.3s;
351
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
352
+ }
353
+
354
+ .pp-checkbox:checked::after {
355
+ left: 1.4375rem;
356
+ }
357
+
358
+ .pp-toggle-track {
359
+ display: none;
360
+ }
361
+
362
+ .pp-offset-ctrl {
363
+ display: none;
364
+ padding-top: 0.5rem;
365
+ border-top: 1px solid #e2e8f0;
366
+ }
367
+
368
+ :global(.theme-dark) .pp-offset-ctrl {
369
+ border-top-color: #2d2d3f;
370
+ }
371
+
372
+ .pp-offset-row {
373
+ display: flex;
374
+ align-items: center;
375
+ gap: 1rem;
376
+ margin-top: 0.5rem;
377
+ }
378
+
379
+ .pp-range {
380
+ flex: 1;
381
+ height: 0.5rem;
382
+ accent-color: #4f46e5;
383
+ cursor: pointer;
384
+ }
385
+
386
+ .pp-offset-val {
387
+ font-size: 0.875rem;
388
+ font-weight: 600;
389
+ color: #0f172a;
390
+ min-width: 2.5rem;
391
+ text-align: right;
392
+ }
393
+
394
+ :global(.theme-dark) .pp-offset-val {
395
+ color: #e2e8f0;
396
+ }
397
+
398
+ .pp-error {
399
+ display: none;
400
+ padding: 1rem;
401
+ background: #fef2f2;
402
+ border: 1px solid #fecaca;
403
+ border-radius: 0.75rem;
404
+ color: #dc2626;
405
+ font-size: 0.875rem;
406
+ align-items: flex-start;
407
+ gap: 0.75rem;
408
+ }
409
+
410
+ :global(.theme-dark) .pp-error {
411
+ background: #2d1515;
412
+ border-color: #7f1d1d;
413
+ color: #fca5a5;
414
+ }
415
+
416
+ .pp-error-icon {
417
+ width: 1.25rem;
418
+ height: 1.25rem;
419
+ flex-shrink: 0;
420
+ margin-top: 0.125rem;
421
+ }
422
+
423
+ .pp-preview {
424
+ background: #f1f5f9;
425
+ padding: 3rem;
426
+ position: relative;
427
+ display: flex;
428
+ align-items: center;
429
+ justify-content: center;
430
+ min-height: 500px;
431
+ }
432
+
433
+ :global(.theme-dark) .pp-preview {
434
+ background: #0f172a;
435
+ }
436
+
437
+ .pp-dots-bg {
438
+ position: absolute;
439
+ inset: 0;
440
+ background-image: radial-gradient(circle at 1px 1px, rgba(0, 0, 0, 0.15) 1px, transparent 0);
441
+ background-size: 20px 20px;
442
+ pointer-events: none;
443
+ opacity: 0.5;
444
+ }
445
+
446
+ :global(.theme-dark) .pp-dots-bg {
447
+ background-image: radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.1) 1px, transparent 0);
448
+ }
449
+
450
+ .pp-frame-wrapper {
451
+ position: relative;
452
+ height: 100%;
453
+ max-height: 60vh;
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: center;
457
+ }
458
+
459
+ .pp-frame {
460
+ position: relative;
461
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
462
+ background: #fff;
463
+ height: 100%;
464
+ max-height: 60vh;
465
+ transition: all 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
466
+ }
467
+
468
+ :global(.theme-dark) .pp-frame {
469
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
470
+ }
471
+
472
+ .pp-mat {
473
+ position: absolute;
474
+ inset: 0;
475
+ background: #e2e2e2;
476
+ }
477
+
478
+ :global(.theme-dark) .pp-mat {
479
+ background: #334155;
480
+ }
481
+
482
+ .pp-art {
483
+ position: absolute;
484
+ background: #fff;
485
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
486
+ overflow: hidden;
487
+ display: flex;
488
+ align-items: center;
489
+ justify-content: center;
490
+ transition: all 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
491
+ }
492
+
493
+ :global(.theme-dark) .pp-art {
494
+ background: #1e293b;
495
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
496
+ }
497
+
498
+ .pp-art-placeholder {
499
+ font-style: italic;
500
+ color: #94a3b8;
501
+ font-size: clamp(0.75rem, 2vw, 1.25rem);
502
+ user-select: none;
503
+ position: relative;
504
+ z-index: 1;
505
+ }
506
+
507
+ :global(.theme-dark) .pp-art-placeholder {
508
+ color: #475569;
509
+ }
510
+
511
+ .pp-art-dim {
512
+ position: absolute;
513
+ pointer-events: none;
514
+ opacity: 0.5;
515
+ }
516
+
517
+ .pp-art-dim-w {
518
+ bottom: 0.5rem;
519
+ left: 0;
520
+ right: 0;
521
+ display: flex;
522
+ align-items: center;
523
+ justify-content: center;
524
+ flex-direction: column;
525
+ gap: 0.125rem;
526
+ }
527
+
528
+ .pp-art-dim-h {
529
+ top: 0;
530
+ bottom: 0;
531
+ right: 0.5rem;
532
+ display: flex;
533
+ align-items: center;
534
+ justify-content: center;
535
+ flex-direction: column;
536
+ gap: 0.125rem;
537
+ }
538
+
539
+ .pp-dim-line {
540
+ height: 1px;
541
+ width: 60%;
542
+ background: #0f172a;
543
+ }
544
+
545
+ :global(.theme-dark) .pp-dim-line {
546
+ background: #e2e8f0;
547
+ }
548
+
549
+ .pp-dim-line-v {
550
+ width: 1px;
551
+ height: 60%;
552
+ }
553
+
554
+ .pp-dim-label {
555
+ font-size: 0.5rem;
556
+ color: #0f172a;
557
+ }
558
+
559
+ :global(.theme-dark) .pp-dim-label {
560
+ color: #e2e8f0;
561
+ }
562
+
563
+ .pp-dim-label-v {
564
+ writing-mode: vertical-rl;
565
+ transform: rotate(180deg);
566
+ }
567
+
568
+ .pp-border-label {
569
+ position: absolute;
570
+ display: flex;
571
+ flex-direction: column;
572
+ align-items: center;
573
+ gap: 0.125rem;
574
+ color: #4f46e5;
575
+ z-index: 10;
576
+ }
577
+
578
+ :global(.theme-dark) .pp-border-label {
579
+ color: #818cf8;
580
+ }
581
+
582
+ .pp-border-top {
583
+ top: 0;
584
+ left: 50%;
585
+ transform: translate(-50%, -100%);
586
+ padding-bottom: 0.5rem;
587
+ }
588
+
589
+ .pp-border-bottom {
590
+ bottom: 0;
591
+ left: 50%;
592
+ transform: translate(-50%, 100%);
593
+ padding-top: 0.5rem;
594
+ }
595
+
596
+ .pp-border-left {
597
+ left: 0;
598
+ top: 50%;
599
+ transform: translate(-100%, -50%);
600
+ padding-right: 1rem;
601
+ color: #db2777;
602
+ }
603
+
604
+ :global(.theme-dark) .pp-border-left {
605
+ color: #f472b6;
606
+ }
607
+
608
+ .pp-border-right {
609
+ right: 0;
610
+ top: 50%;
611
+ transform: translate(100%, -50%);
612
+ padding-left: 1rem;
613
+ color: #db2777;
614
+ }
615
+
616
+ :global(.theme-dark) .pp-border-right {
617
+ color: #f472b6;
618
+ }
619
+
620
+ .pp-border-val {
621
+ font-size: 1.25rem;
622
+ font-weight: 700;
623
+ line-height: 1;
624
+ }
625
+
626
+ .pp-border-tag {
627
+ font-size: 0.65rem;
628
+ text-transform: uppercase;
629
+ letter-spacing: 0.1em;
630
+ opacity: 0.7;
631
+ }
632
+
633
+ .pp-border-unit {
634
+ font-size: 0.6rem;
635
+ opacity: 0.5;
636
+ }
637
+
638
+ @media (max-width: 1023px) {
639
+ .pp-preview {
640
+ padding: 4rem 3rem;
641
+ }
642
+ }
643
+ </style>
644
+
645
+ <script>
646
+ type PpSizes = { fw: number; fh: number; aw: number; ah: number };
647
+ type PpMargins = { top: number; bottom: number; left: number; right: number };
648
+
649
+ function ppInput(root: Element, sel: string): number {
650
+ return parseFloat(root.querySelector<HTMLInputElement>(sel)?.value ?? '') || 0;
651
+ }
652
+
653
+ function ppParse(root: Element): PpSizes {
654
+ return {
655
+ fw: ppInput(root, '[data-pp-frame-w]'),
656
+ fh: ppInput(root, '[data-pp-frame-h]'),
657
+ aw: ppInput(root, '[data-pp-art-w]'),
658
+ ah: ppInput(root, '[data-pp-art-h]'),
659
+ };
660
+ }
661
+
662
+ function ppWeighted(root: Element): { weighted: boolean; offset: number } {
663
+ const cb = root.querySelector<HTMLInputElement>('[data-pp-weighted]');
664
+ const range = root.querySelector<HTMLInputElement>('[data-pp-offset-range]');
665
+ return {
666
+ weighted: cb?.checked ?? true,
667
+ offset: parseFloat(range?.value ?? '5') / 100,
668
+ };
669
+ }
670
+
671
+ function ppMargins(s: PpSizes, weighted: boolean, offset: number): PpMargins {
672
+ const wDiff = s.fw - s.aw;
673
+ const hDiff = s.fh - s.ah;
674
+ const side = wDiff / 2;
675
+ if (!weighted) return { top: hDiff / 2, bottom: hDiff / 2, left: side, right: side };
676
+ const shift = s.fh * offset;
677
+ return { top: hDiff / 2 - shift, bottom: hDiff / 2 + shift, left: side, right: side };
678
+ }
679
+
680
+ function ppUpdateLabels(root: Element, m: PpMargins): void {
681
+ const set = (sel: string, v: number) => {
682
+ const el = root.querySelector(sel);
683
+ if (el) el.textContent = v.toFixed(2);
684
+ };
685
+ set('[data-pp-top]', m.top);
686
+ set('[data-pp-bottom]', m.bottom);
687
+ set('[data-pp-left]', m.left);
688
+ set('[data-pp-right]', m.right);
689
+ }
690
+
691
+ function ppUpdateVisual(root: Element, s: PpSizes, m: PpMargins): void {
692
+ const frame = root.querySelector<HTMLElement>('[data-pp-frame]');
693
+ const art = root.querySelector<HTMLElement>('[data-pp-art]');
694
+ const wEl = root.querySelector('[data-pp-art-w-display]');
695
+ const hEl = root.querySelector('[data-pp-art-h-display]');
696
+ if (!frame || !art) return;
697
+ frame.style.aspectRatio = `${s.fw}/${s.fh}`;
698
+ art.style.width = `${(s.aw / s.fw) * 100}%`;
699
+ art.style.height = `${(s.ah / s.fh) * 100}%`;
700
+ art.style.top = `${(m.top / s.fh) * 100}%`;
701
+ art.style.left = `${(m.left / s.fw) * 100}%`;
702
+ if (wEl) wEl.textContent = `${s.aw}cm`;
703
+ if (hEl) hEl.textContent = `${s.ah}cm`;
704
+ }
705
+
706
+ function ppUpdateOffset(root: Element, weighted: boolean, offset: number): void {
707
+ const ctrl = root.querySelector<HTMLElement>('[data-pp-offset-ctrl]');
708
+ const val = root.querySelector('[data-pp-offset-val]');
709
+ if (ctrl) ctrl.style.display = weighted ? 'block' : 'none';
710
+ if (val) val.textContent = `${Math.round(offset * 100)}%`;
711
+ }
712
+
713
+ function ppCalc(root: Element): void {
714
+ const s = ppParse(root);
715
+ const { weighted, offset } = ppWeighted(root);
716
+ const error = root.querySelector<HTMLElement>('[data-pp-error]');
717
+ if (s.aw >= s.fw || s.ah >= s.fh || s.fw <= 0 || s.fh <= 0) {
718
+ if (error) error.style.display = 'flex';
719
+ return;
720
+ }
721
+ if (error) error.style.display = 'none';
722
+ const m = ppMargins(s, weighted, offset);
723
+ ppUpdateLabels(root, m);
724
+ ppUpdateVisual(root, s, m);
725
+ ppUpdateOffset(root, weighted, offset);
726
+ }
727
+
728
+ function ppAttach(root: Element): void {
729
+ root.querySelectorAll<HTMLInputElement>('[data-pp-input]').forEach((el) => {
730
+ el.addEventListener('input', () => ppCalc(root));
731
+ el.addEventListener('change', () => ppCalc(root));
732
+ });
733
+ }
734
+
735
+ function ppInit(root: Element): void {
736
+ ppAttach(root);
737
+ ppCalc(root);
738
+ }
739
+
740
+ document.querySelectorAll('[data-pp-root]').forEach(ppInit);
741
+ </script>