@jjlmoya/utils-textiles 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 (134) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +76 -0
  3. package/src/category/i18n/es.ts +76 -0
  4. package/src/category/i18n/fr.ts +73 -0
  5. package/src/category/index.ts +17 -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 +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +253 -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/seo_length.test.ts +22 -0
  21. package/src/tests/title_separators.test.ts +40 -0
  22. package/src/tests/tool_validation.test.ts +17 -0
  23. package/src/tool/burnTest/bibliography.astro +34 -0
  24. package/src/tool/burnTest/component.astro +602 -0
  25. package/src/tool/burnTest/i18n/en.ts +225 -0
  26. package/src/tool/burnTest/i18n/es.ts +225 -0
  27. package/src/tool/burnTest/i18n/fr.ts +225 -0
  28. package/src/tool/burnTest/index.ts +34 -0
  29. package/src/tool/burnTest/logic.ts +3 -0
  30. package/src/tool/burnTest/seo.astro +40 -0
  31. package/src/tool/burnTest/ui.ts +36 -0
  32. package/src/tool/clothingSizeConverter/bibliography.astro +34 -0
  33. package/src/tool/clothingSizeConverter/component.astro +1226 -0
  34. package/src/tool/clothingSizeConverter/i18n/en.ts +356 -0
  35. package/src/tool/clothingSizeConverter/i18n/es.ts +356 -0
  36. package/src/tool/clothingSizeConverter/i18n/fr.ts +356 -0
  37. package/src/tool/clothingSizeConverter/index.ts +34 -0
  38. package/src/tool/clothingSizeConverter/logic.ts +30 -0
  39. package/src/tool/clothingSizeConverter/seo.astro +40 -0
  40. package/src/tool/clothingSizeConverter/ui.ts +70 -0
  41. package/src/tool/fabricProjectCalculator/bibliography.astro +19 -0
  42. package/src/tool/fabricProjectCalculator/component.astro +914 -0
  43. package/src/tool/fabricProjectCalculator/i18n/en.ts +250 -0
  44. package/src/tool/fabricProjectCalculator/i18n/es.ts +250 -0
  45. package/src/tool/fabricProjectCalculator/i18n/fr.ts +250 -0
  46. package/src/tool/fabricProjectCalculator/index.ts +34 -0
  47. package/src/tool/fabricProjectCalculator/logic.ts +89 -0
  48. package/src/tool/fabricProjectCalculator/seo.astro +40 -0
  49. package/src/tool/fabricProjectCalculator/ui.ts +57 -0
  50. package/src/tool/fabricTruth/bibliography.astro +40 -0
  51. package/src/tool/fabricTruth/component.astro +708 -0
  52. package/src/tool/fabricTruth/i18n/en.ts +282 -0
  53. package/src/tool/fabricTruth/i18n/es.ts +316 -0
  54. package/src/tool/fabricTruth/i18n/fr.ts +282 -0
  55. package/src/tool/fabricTruth/index.ts +34 -0
  56. package/src/tool/fabricTruth/logic.ts +91 -0
  57. package/src/tool/fabricTruth/seo.astro +40 -0
  58. package/src/tool/fabricTruth/ui.ts +28 -0
  59. package/src/tool/fiberPrep/bibliography.astro +33 -0
  60. package/src/tool/fiberPrep/component.astro +987 -0
  61. package/src/tool/fiberPrep/i18n/en.ts +225 -0
  62. package/src/tool/fiberPrep/i18n/es.ts +225 -0
  63. package/src/tool/fiberPrep/i18n/fr.ts +225 -0
  64. package/src/tool/fiberPrep/index.ts +34 -0
  65. package/src/tool/fiberPrep/logic.ts +36 -0
  66. package/src/tool/fiberPrep/seo.astro +40 -0
  67. package/src/tool/fiberPrep/ui.ts +38 -0
  68. package/src/tool/knittingGauge/bibliography.astro +14 -0
  69. package/src/tool/knittingGauge/component.astro +828 -0
  70. package/src/tool/knittingGauge/i18n/en.ts +234 -0
  71. package/src/tool/knittingGauge/i18n/es.ts +234 -0
  72. package/src/tool/knittingGauge/i18n/fr.ts +234 -0
  73. package/src/tool/knittingGauge/index.ts +34 -0
  74. package/src/tool/knittingGauge/logic.ts +28 -0
  75. package/src/tool/knittingGauge/seo.astro +40 -0
  76. package/src/tool/knittingGauge/ui.ts +41 -0
  77. package/src/tool/laundryGuide/bibliography.astro +33 -0
  78. package/src/tool/laundryGuide/component.astro +486 -0
  79. package/src/tool/laundryGuide/data-en.ts +166 -0
  80. package/src/tool/laundryGuide/data-es.ts +166 -0
  81. package/src/tool/laundryGuide/data-fr.ts +103 -0
  82. package/src/tool/laundryGuide/i18n/en.ts +275 -0
  83. package/src/tool/laundryGuide/i18n/es.ts +275 -0
  84. package/src/tool/laundryGuide/i18n/fr.ts +291 -0
  85. package/src/tool/laundryGuide/index.ts +29 -0
  86. package/src/tool/laundryGuide/logic.ts +19 -0
  87. package/src/tool/laundryGuide/seo.astro +40 -0
  88. package/src/tool/needleConverter/bibliography.astro +14 -0
  89. package/src/tool/needleConverter/component.astro +518 -0
  90. package/src/tool/needleConverter/i18n/en.ts +217 -0
  91. package/src/tool/needleConverter/i18n/es.ts +217 -0
  92. package/src/tool/needleConverter/i18n/fr.ts +217 -0
  93. package/src/tool/needleConverter/index.ts +34 -0
  94. package/src/tool/needleConverter/logic.ts +31 -0
  95. package/src/tool/needleConverter/seo.astro +15 -0
  96. package/src/tool/needleConverter/ui.ts +17 -0
  97. package/src/tool/sewingPatternScaler/bibliography.astro +14 -0
  98. package/src/tool/sewingPatternScaler/component.astro +550 -0
  99. package/src/tool/sewingPatternScaler/i18n/en.ts +196 -0
  100. package/src/tool/sewingPatternScaler/i18n/es.ts +231 -0
  101. package/src/tool/sewingPatternScaler/i18n/fr.ts +187 -0
  102. package/src/tool/sewingPatternScaler/index.ts +34 -0
  103. package/src/tool/sewingPatternScaler/logic.ts +35 -0
  104. package/src/tool/sewingPatternScaler/seo.astro +49 -0
  105. package/src/tool/sewingPatternScaler/ui.ts +29 -0
  106. package/src/tool/shoeSizeConverter/bibliography.astro +34 -0
  107. package/src/tool/shoeSizeConverter/component.astro +437 -0
  108. package/src/tool/shoeSizeConverter/i18n/en.ts +188 -0
  109. package/src/tool/shoeSizeConverter/i18n/es.ts +188 -0
  110. package/src/tool/shoeSizeConverter/i18n/fr.ts +188 -0
  111. package/src/tool/shoeSizeConverter/index.ts +34 -0
  112. package/src/tool/shoeSizeConverter/logic.ts +8 -0
  113. package/src/tool/shoeSizeConverter/seo.astro +40 -0
  114. package/src/tool/shoeSizeConverter/ui.ts +21 -0
  115. package/src/tool/stainChemistry/bibliography.astro +40 -0
  116. package/src/tool/stainChemistry/component.astro +717 -0
  117. package/src/tool/stainChemistry/i18n/en.ts +303 -0
  118. package/src/tool/stainChemistry/i18n/es.ts +300 -0
  119. package/src/tool/stainChemistry/i18n/fr.ts +310 -0
  120. package/src/tool/stainChemistry/index.ts +34 -0
  121. package/src/tool/stainChemistry/logic.ts +11 -0
  122. package/src/tool/stainChemistry/seo.astro +40 -0
  123. package/src/tool/stainChemistry/ui.ts +51 -0
  124. package/src/tool/yarnCalculator/bibliography.astro +19 -0
  125. package/src/tool/yarnCalculator/component.astro +792 -0
  126. package/src/tool/yarnCalculator/i18n/en.ts +310 -0
  127. package/src/tool/yarnCalculator/i18n/es.ts +310 -0
  128. package/src/tool/yarnCalculator/i18n/fr.ts +310 -0
  129. package/src/tool/yarnCalculator/index.ts +34 -0
  130. package/src/tool/yarnCalculator/logic.ts +84 -0
  131. package/src/tool/yarnCalculator/seo.astro +14 -0
  132. package/src/tool/yarnCalculator/ui.ts +42 -0
  133. package/src/tools.ts +16 -0
  134. package/src/types.ts +72 -0
@@ -0,0 +1,914 @@
1
+ ---
2
+ import type { FabricProjectCalculatorUI } from './ui';
3
+
4
+ interface Props {
5
+ ui?: Record<string, unknown>;
6
+ }
7
+
8
+ const { ui = {} } = Astro.props;
9
+ const calcUI = ui as FabricProjectCalculatorUI;
10
+ ---
11
+
12
+ <div class="fabric-calculator-card" id="fabric-calc-app">
13
+ <aside class="fabric-sidebar">
14
+ <div class="config-group">
15
+ <h3>{calcUI.sectionProject}</h3>
16
+ <div class="fabric-field">
17
+ <label>{calcUI.labelGarmentType}</label>
18
+ <select id="garment-type" class="fabric-select">
19
+ <option value="skirt">{calcUI.garmentSkirt}</option>
20
+ <option value="pants">{calcUI.garmentPants}</option>
21
+ <option value="dress">{calcUI.garmentDress}</option>
22
+ <option value="shirt">{calcUI.garmentShirt}</option>
23
+ <option value="coat">{calcUI.garmentCoat}</option>
24
+ <option value="tote">{calcUI.garmentTote}</option>
25
+ </select>
26
+ </div>
27
+ <div class="fabric-field">
28
+ <label>{calcUI.labelSize}</label>
29
+ <div class="preset-group" id="size-presets">
30
+ <button class="preset-pill" data-val="xs_core">{calcUI.sizeXS}</button>
31
+ <button class="preset-pill active" data-val="m_core">{calcUI.sizeM}</button>
32
+ <button class="preset-pill" data-val="xl_core">{calcUI.sizeXL}</button>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="config-group">
38
+ <h3>{calcUI.sectionMaterial}</h3>
39
+ <div class="fabric-field">
40
+ <label>{calcUI.labelFabricWidth}</label>
41
+ <select id="fabric-width" class="fabric-select">
42
+ <option value="90">{calcUI.width90}</option>
43
+ <option value="115">{calcUI.width115}</option>
44
+ <option value="140" selected>{calcUI.width140}</option>
45
+ <option value="150">{calcUI.width150}</option>
46
+ </select>
47
+ </div>
48
+ <div class="fabric-field">
49
+ <label>{calcUI.labelSeamAllowance}</label>
50
+ <div class="shop-stepper">
51
+ <button class="step-btn" id="sub-allow">−</button>
52
+ <input type="number" id="seam-allowance" class="fabric-input" value="1.5" step="0.5" min="0" max="5" />
53
+ <button class="step-btn" id="add-allow">+</button>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="btn-actions">
59
+ <button id="btn-clear-f" class="f-btn f-seco">{calcUI.btnClear}</button>
60
+ <button id="btn-share-f" class="f-btn f-prim">{calcUI.btnShare}</button>
61
+ </div>
62
+ </aside>
63
+
64
+ <main class="fabric-main">
65
+ <div id="warning-msg" class="warning-banner"></div>
66
+
67
+ <section class="magic-result-area">
68
+ <span class="result-lbl">{calcUI.resultLabel}</span>
69
+ <div class="val"><span id="main-meters">1.40</span><span class="unit">{calcUI.resultUnit}</span></div>
70
+ <div class="advice-tag" id="shop-advice"></div>
71
+ </section>
72
+
73
+ <div class="layout-container">
74
+ <div class="canvas-wrapper">
75
+ <div class="scheme-area">
76
+ <div class="ruler" id="ruler-col"></div>
77
+ <div id="fabric-board-box" class="fabric-board">
78
+ <div class="board-empty-state">{calcUI.boardEmpty}</div>
79
+ </div>
80
+ </div>
81
+ <div class="scheme-info" id="board-desc"></div>
82
+ </div>
83
+ </div>
84
+
85
+ <section class="merch-section">
86
+ <div class="merch-header">
87
+ <h3 class="merch-title">{calcUI.merchTitle}</h3>
88
+ <button id="copy-list-btn" class="copy-list-btn">{calcUI.btnCopyList}</button>
89
+ </div>
90
+ <ul id="check-list" class="merceria-checklist"></ul>
91
+ </section>
92
+ </main>
93
+ </div>
94
+
95
+ <script define:vars={{ ui }} is:inline>
96
+ window.__toolUI = ui;
97
+ </script>
98
+
99
+ <script>
100
+ import type { FabricProjectCalculatorUI } from './ui';
101
+ import type { PieceRow } from './logic';
102
+ import {
103
+ isSerialCut, computeMeters, shopAdviceMeters, getRulerStep,
104
+ PIECE_ROWS, CONSUMPTION_MATRIX, SIZE_MULTS, SIZE_MIN_WIDTHS,
105
+ } from './logic';
106
+
107
+ declare global {
108
+ interface Window {
109
+ __toolUI: FabricProjectCalculatorUI;
110
+ }
111
+ }
112
+
113
+ type PieceNameKey = keyof FabricProjectCalculatorUI['pieceNames'];
114
+
115
+ const PIECE_COLORS = [
116
+ 'rgba(196, 181, 253, 0.55)',
117
+ 'rgba(165, 180, 252, 0.55)',
118
+ 'rgba(139, 92, 246, 0.30)',
119
+ 'rgba(167, 139, 250, 0.45)',
120
+ 'rgba(129, 140, 248, 0.45)',
121
+ 'rgba(110, 231, 183, 0.40)',
122
+ ];
123
+
124
+ const PIECE_BORDERS = [
125
+ 'rgba(139, 92, 246, 0.50)',
126
+ 'rgba(99, 102, 241, 0.50)',
127
+ 'rgba(139, 92, 246, 0.70)',
128
+ 'rgba(124, 58, 237, 0.50)',
129
+ 'rgba(79, 70, 229, 0.50)',
130
+ 'rgba(16, 185, 129, 0.50)',
131
+ ];
132
+
133
+ let activeSize = 'm_core';
134
+
135
+ function getNum(id: string): number {
136
+ const el = document.getElementById(id) as HTMLInputElement | null;
137
+ return parseFloat(el?.value || '0') || 0;
138
+ }
139
+
140
+ function getStr(id: string): string {
141
+ const el = document.getElementById(id) as HTMLInputElement | null;
142
+ return el?.value || '';
143
+ }
144
+
145
+ function setText(id: string, text: string): void {
146
+ const el = document.getElementById(id);
147
+ if (el) el.textContent = text;
148
+ }
149
+
150
+ function flashBtn(id: string, flashText: string): void {
151
+ const btn = document.getElementById(id);
152
+ if (!btn) return;
153
+ const orig = btn.textContent;
154
+ btn.textContent = flashText;
155
+ setTimeout(() => { if (btn) btn.textContent = orig; }, 2000);
156
+ }
157
+
158
+ function buildPieceEl(nameKey: string, w: number, fold: boolean, col: number): HTMLDivElement {
159
+ const el = document.createElement('div');
160
+ el.className = `piece-block${fold ? ' piece-fold' : ''}`;
161
+ el.style.width = `${w * 100}%`;
162
+ el.style.background = PIECE_COLORS[col];
163
+ el.style.borderColor = PIECE_BORDERS[col];
164
+ const nameEl = document.createElement('span');
165
+ nameEl.className = 'piece-name';
166
+ nameEl.textContent = window.__toolUI.pieceNames[nameKey as PieceNameKey] || nameKey;
167
+ el.appendChild(nameEl);
168
+ if (fold) {
169
+ const foldEl = document.createElement('div');
170
+ foldEl.className = 'fold-indicator';
171
+ foldEl.textContent = window.__toolUI.foldLabel;
172
+ el.appendChild(foldEl);
173
+ }
174
+ return el;
175
+ }
176
+
177
+ function buildRowEl(row: PieceRow): HTMLDivElement {
178
+ const rowEl = document.createElement('div');
179
+ rowEl.className = 'piece-row';
180
+ rowEl.style.flex = `0 0 ${row.h * 100}%`;
181
+ row.pieces.forEach((spec) => {
182
+ rowEl.appendChild(buildPieceEl(spec.nameKey, spec.w, spec.fold ?? false, spec.col));
183
+ });
184
+ return rowEl;
185
+ }
186
+
187
+ function renderBoard(type: string, board: HTMLElement): void {
188
+ board.innerHTML = '';
189
+ const rows = PIECE_ROWS[type];
190
+ if (!rows?.length) {
191
+ const empty = document.createElement('div');
192
+ empty.className = 'board-empty-state';
193
+ empty.textContent = window.__toolUI.boardEmpty;
194
+ board.appendChild(empty);
195
+ return;
196
+ }
197
+ rows.forEach((row) => board.appendChild(buildRowEl(row)));
198
+ }
199
+
200
+ function renderRuler(totalMeters: number): void {
201
+ const ruler = document.getElementById('ruler-col');
202
+ if (!ruler) return;
203
+ ruler.innerHTML = '';
204
+ const step = getRulerStep(totalMeters);
205
+ let mark = 0;
206
+ while (mark <= totalMeters + 0.001) {
207
+ const markEl = document.createElement('div');
208
+ markEl.className = 'ruler-mark';
209
+ markEl.style.top = `${(mark / totalMeters) * 100}%`;
210
+ const label = document.createElement('span');
211
+ label.textContent = `${mark.toFixed(step < 0.5 ? 2 : 1)}m`;
212
+ markEl.appendChild(label);
213
+ ruler.appendChild(markEl);
214
+ mark = parseFloat((mark + step).toFixed(3));
215
+ }
216
+ }
217
+
218
+ function renderLayout(type: string, width: number, meters: number): void {
219
+ const board = document.getElementById('fabric-board-box');
220
+ if (!board) return;
221
+ renderBoard(type, board);
222
+ renderRuler(meters);
223
+ const desc = document.getElementById('board-desc');
224
+ if (desc) {
225
+ desc.textContent = window.__toolUI.boardDescFormat
226
+ .replace('{w}', String(width))
227
+ .replace('{m}', meters.toFixed(2));
228
+ }
229
+ }
230
+
231
+ function updateWarning(serial: boolean, width: string): void {
232
+ const el = document.getElementById('warning-msg');
233
+ if (!el) return;
234
+ el.style.display = serial ? 'block' : 'none';
235
+ if (serial) el.textContent = window.__toolUI.warningSerialFormat.replace('{w}', width);
236
+ }
237
+
238
+ function updateResult(meters: number): void {
239
+ setText('main-meters', meters.toFixed(2));
240
+ const store = shopAdviceMeters(meters);
241
+ const advice = document.getElementById('shop-advice');
242
+ if (advice) advice.textContent = window.__toolUI.shopAdviceFormat.replace('{m}', store.toFixed(2));
243
+ }
244
+
245
+ function updateChecklist(type: string): void {
246
+ const checks = document.getElementById('check-list');
247
+ if (!checks) return;
248
+ const u = window.__toolUI;
249
+ const extra = u.garmentMerceria[type as keyof typeof u.garmentMerceria] || '';
250
+ const extras = extra.split(',').map((s) => s.trim()).filter(Boolean);
251
+ const items = [u.checklistThread, ...extras];
252
+ checks.innerHTML = '';
253
+ items.forEach((text) => {
254
+ const li = document.createElement('li');
255
+ li.className = 'merch-item';
256
+ li.innerHTML = `<span class="merch-dot" aria-hidden="true">◆</span>${text}`;
257
+ checks.appendChild(li);
258
+ });
259
+ }
260
+
261
+ function updateUrl(): void {
262
+ const p = new URLSearchParams();
263
+ p.set('g', getStr('garment-type'));
264
+ p.set('s', activeSize);
265
+ p.set('w', getStr('fabric-width'));
266
+ p.set('a', getStr('seam-allowance'));
267
+ window.history.replaceState({}, '', `${window.location.pathname}?${p.toString()}`);
268
+ }
269
+
270
+ function calculate(): void {
271
+ const type = getStr('garment-type');
272
+ const width = getStr('fabric-width');
273
+ const allowCm = getNum('seam-allowance');
274
+ const widthInt = parseInt(width);
275
+ const base = CONSUMPTION_MATRIX[type]?.[width] || 1.5;
276
+ const mult = SIZE_MULTS[activeSize] ?? 1.0;
277
+ const minW = SIZE_MIN_WIDTHS[activeSize] ?? 55;
278
+ const serial = isSerialCut(minW, widthInt, type);
279
+ const meters = computeMeters(base, mult, serial, allowCm);
280
+ updateWarning(serial, width);
281
+ updateResult(meters);
282
+ renderLayout(type, widthInt, meters);
283
+ updateChecklist(type);
284
+ updateUrl();
285
+ }
286
+
287
+ function handleSizeClick(e: Event): void {
288
+ const btn = (e.target as HTMLElement).closest('.preset-pill');
289
+ if (!btn) return;
290
+ document.getElementById('size-presets')?.querySelectorAll('.preset-pill').forEach((b) => {
291
+ b.classList.remove('active');
292
+ });
293
+ btn.classList.add('active');
294
+ activeSize = btn.getAttribute('data-val') || 'm_core';
295
+ calculate();
296
+ }
297
+
298
+ function stepAllowance(delta: number): void {
299
+ const el = document.getElementById('seam-allowance') as HTMLInputElement | null;
300
+ if (!el) return;
301
+ const clamped = Math.min(5, Math.max(0, (parseFloat(el.value) || 1.5) + delta));
302
+ el.value = String(clamped);
303
+ calculate();
304
+ }
305
+
306
+ function handleClear(): void {
307
+ const garment = document.getElementById('garment-type') as HTMLSelectElement | null;
308
+ const width = document.getElementById('fabric-width') as HTMLSelectElement | null;
309
+ const allow = document.getElementById('seam-allowance') as HTMLInputElement | null;
310
+ if (garment) garment.value = 'skirt';
311
+ if (width) width.value = '140';
312
+ if (allow) allow.value = '1.5';
313
+ activeSize = 'm_core';
314
+ document.getElementById('size-presets')?.querySelectorAll('.preset-pill').forEach((b) => {
315
+ b.classList.toggle('active', b.getAttribute('data-val') === 'm_core');
316
+ });
317
+ calculate();
318
+ }
319
+
320
+ function handleCopyList(): void {
321
+ const checks = document.getElementById('check-list');
322
+ const lines = Array.from(checks?.querySelectorAll('.merch-item') || [])
323
+ .map((li) => '• ' + (li.textContent || '').replace('◆', '').trim());
324
+ navigator.clipboard.writeText(lines.join('\n'));
325
+ flashBtn('copy-list-btn', window.__toolUI.btnCopied);
326
+ }
327
+
328
+ function restoreEl(q: URLSearchParams, param: string, id: string, def: string): void {
329
+ if (!q.has(param)) return;
330
+ const el = document.getElementById(id) as HTMLInputElement | null;
331
+ if (el) el.value = q.get(param) || def;
332
+ }
333
+
334
+ function restoreInputs(q: URLSearchParams): void {
335
+ restoreEl(q, 'g', 'garment-type', 'skirt');
336
+ restoreEl(q, 'w', 'fabric-width', '140');
337
+ restoreEl(q, 'a', 'seam-allowance', '1.5');
338
+ }
339
+
340
+ function restoreSize(q: URLSearchParams): void {
341
+ if (!q.has('s')) return;
342
+ activeSize = q.get('s') || 'm_core';
343
+ document.getElementById('size-presets')?.querySelectorAll('.preset-pill').forEach((b) => {
344
+ b.classList.toggle('active', b.getAttribute('data-val') === activeSize);
345
+ });
346
+ }
347
+
348
+ function attachListeners(): void {
349
+ document.getElementById('size-presets')?.addEventListener('click', handleSizeClick);
350
+ document.getElementById('sub-allow')?.addEventListener('click', () => stepAllowance(-0.5));
351
+ document.getElementById('add-allow')?.addEventListener('click', () => stepAllowance(0.5));
352
+ document.getElementById('btn-clear-f')?.addEventListener('click', handleClear);
353
+ document.getElementById('btn-share-f')?.addEventListener('click', () => {
354
+ navigator.clipboard.writeText(window.location.href);
355
+ flashBtn('btn-share-f', window.__toolUI.btnCopied);
356
+ });
357
+ document.getElementById('copy-list-btn')?.addEventListener('click', handleCopyList);
358
+ ['garment-type', 'fabric-width', 'seam-allowance'].forEach((id) => {
359
+ document.getElementById(id)?.addEventListener('input', calculate);
360
+ });
361
+ }
362
+
363
+ function init(): void {
364
+ const q = new URLSearchParams(window.location.search);
365
+ restoreInputs(q);
366
+ restoreSize(q);
367
+ attachListeners();
368
+ calculate();
369
+ }
370
+
371
+ document.addEventListener('astro:page-load', init);
372
+ init();
373
+ </script>
374
+
375
+ <style>
376
+ .fabric-calculator-card {
377
+ --f-p: #4f46e5;
378
+ --f-a: #818cf8;
379
+ --f-side: rgba(79, 70, 229, 0.04);
380
+
381
+ display: grid;
382
+ grid-template-columns: 360px 1fr;
383
+ max-width: 1200px;
384
+ width: 95%;
385
+ margin: 2rem auto;
386
+ background: var(--bg-surface);
387
+ backdrop-filter: blur(25px);
388
+ border-radius: 32px;
389
+ overflow: hidden;
390
+ border: 1px solid var(--border-color);
391
+ box-shadow: 0 40px 100px rgba(0, 0, 0, 0.15);
392
+ color: var(--text-main);
393
+ }
394
+
395
+ .theme-dark .fabric-calculator-card {
396
+ --f-side: rgba(255, 255, 255, 0.03);
397
+ }
398
+
399
+ .fabric-sidebar {
400
+ background: var(--f-side);
401
+ padding: 2.5rem 2rem;
402
+ display: flex;
403
+ flex-direction: column;
404
+ gap: 2rem;
405
+ border-right: 1px solid var(--border-color);
406
+ }
407
+
408
+ .config-group {
409
+ display: flex;
410
+ flex-direction: column;
411
+ gap: 1.25rem;
412
+ }
413
+
414
+ .config-group h3 {
415
+ font-size: 0.7rem;
416
+ font-weight: 900;
417
+ text-transform: uppercase;
418
+ letter-spacing: 0.18rem;
419
+ color: var(--f-p);
420
+ margin: 0;
421
+ }
422
+
423
+ .fabric-field {
424
+ display: flex;
425
+ flex-direction: column;
426
+ gap: 0.6rem;
427
+ }
428
+
429
+ .fabric-field label {
430
+ font-size: 0.72rem;
431
+ font-weight: 800;
432
+ text-transform: uppercase;
433
+ letter-spacing: 0.06rem;
434
+ color: var(--text-muted);
435
+ }
436
+
437
+ .fabric-select {
438
+ width: 100%;
439
+ padding: 0.85rem 3rem 0.85rem 1.1rem;
440
+ border-radius: 14px;
441
+ border: 1.5px solid var(--border-color);
442
+ background: var(--bg-surface);
443
+ font-size: 0.875rem;
444
+ font-weight: 700;
445
+ color: inherit;
446
+ cursor: pointer;
447
+ appearance: none;
448
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%234F46E5' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
449
+ background-repeat: no-repeat;
450
+ background-position: right 1rem center;
451
+ background-size: 1.1rem;
452
+ transition: border-color 0.2s, box-shadow 0.2s;
453
+ outline: none;
454
+ }
455
+
456
+ .fabric-select:focus {
457
+ border-color: var(--f-p);
458
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
459
+ }
460
+
461
+ .shop-stepper {
462
+ display: grid;
463
+ grid-template-columns: 48px 1fr 48px;
464
+ align-items: center;
465
+ background: var(--bg-surface);
466
+ border-radius: 14px;
467
+ border: 1.5px solid var(--border-color);
468
+ overflow: hidden;
469
+ height: 50px;
470
+ max-width: 190px;
471
+ }
472
+
473
+ .step-btn {
474
+ height: 100%;
475
+ border: none;
476
+ background: transparent;
477
+ color: var(--f-p);
478
+ font-size: 1.6rem;
479
+ font-weight: 300;
480
+ cursor: pointer;
481
+ display: flex;
482
+ align-items: center;
483
+ justify-content: center;
484
+ transition: background 0.15s, color 0.15s;
485
+ flex-shrink: 0;
486
+ }
487
+
488
+ .step-btn:hover {
489
+ background: var(--f-p);
490
+ color: white;
491
+ }
492
+
493
+ .fabric-input {
494
+ width: 100%;
495
+ height: 100%;
496
+ border: none;
497
+ background: transparent;
498
+ text-align: center;
499
+ font-size: 1rem;
500
+ font-weight: 800;
501
+ color: inherit;
502
+ padding: 0;
503
+ outline: none;
504
+ box-shadow: none;
505
+ }
506
+
507
+ .preset-group {
508
+ display: grid;
509
+ grid-template-columns: repeat(3, 1fr);
510
+ gap: 0.5rem;
511
+ }
512
+
513
+ .preset-pill {
514
+ padding: 0.75rem 0;
515
+ border-radius: 12px;
516
+ border: 1.5px solid rgba(79, 70, 229, 0.25);
517
+ background: rgba(79, 70, 229, 0.06);
518
+ font-size: 0.72rem;
519
+ font-weight: 900;
520
+ color: var(--f-p);
521
+ cursor: pointer;
522
+ transition: all 0.2s;
523
+ text-align: center;
524
+ }
525
+
526
+ .theme-dark .preset-pill {
527
+ background: rgba(79, 70, 229, 0.08);
528
+ border-color: rgba(129, 140, 248, 0.3);
529
+ color: var(--f-a);
530
+ }
531
+
532
+ .preset-pill:hover {
533
+ background: rgba(79, 70, 229, 0.12);
534
+ border-color: var(--f-p);
535
+ }
536
+
537
+ .preset-pill.active {
538
+ background: var(--f-p);
539
+ color: white;
540
+ border-color: var(--f-p);
541
+ box-shadow: 0 6px 16px rgba(79, 70, 229, 0.35);
542
+ }
543
+
544
+ .btn-actions {
545
+ display: grid;
546
+ grid-template-columns: 1fr 1fr;
547
+ gap: 1rem;
548
+ margin-top: auto;
549
+ padding-top: 1rem;
550
+ }
551
+
552
+ .f-btn {
553
+ height: 50px;
554
+ border-radius: 14px;
555
+ font-size: 0.85rem;
556
+ font-weight: 800;
557
+ cursor: pointer;
558
+ border: none;
559
+ transition: all 0.2s;
560
+ }
561
+
562
+ .f-seco {
563
+ background: var(--bg-muted);
564
+ color: var(--text-muted);
565
+ border: 1.5px solid var(--border-color);
566
+ }
567
+
568
+ .f-seco:hover {
569
+ filter: brightness(0.95);
570
+ }
571
+
572
+ .f-prim {
573
+ background: var(--f-p);
574
+ color: white;
575
+ }
576
+
577
+ .f-prim:hover {
578
+ filter: brightness(1.1);
579
+ }
580
+
581
+ .fabric-main {
582
+ padding: 3rem;
583
+ display: flex;
584
+ flex-direction: column;
585
+ gap: 2.5rem;
586
+ min-width: 0;
587
+ }
588
+
589
+ .magic-result-area {
590
+ background: linear-gradient(145deg, rgba(79, 70, 229, 0.07), rgba(129, 140, 248, 0.03));
591
+ padding: 2.5rem 2rem;
592
+ border-radius: 28px;
593
+ border: 1px solid rgba(79, 70, 229, 0.12);
594
+ }
595
+
596
+ .result-lbl {
597
+ display: block;
598
+ font-size: 0.68rem;
599
+ font-weight: 900;
600
+ text-transform: uppercase;
601
+ letter-spacing: 0.12rem;
602
+ color: var(--text-muted);
603
+ margin-bottom: 0.4rem;
604
+ }
605
+
606
+ .magic-result-area .val {
607
+ font-size: 6rem;
608
+ font-weight: 950;
609
+ letter-spacing: -5px;
610
+ color: var(--f-p);
611
+ line-height: 0.85;
612
+ }
613
+
614
+ .magic-result-area .unit {
615
+ font-size: 1.75rem;
616
+ font-weight: 900;
617
+ opacity: 0.4;
618
+ margin-left: 0.5rem;
619
+ }
620
+
621
+ .advice-tag {
622
+ display: inline-flex;
623
+ align-items: center;
624
+ margin-top: 1.5rem;
625
+ padding: 0.85rem 2rem;
626
+ background: rgba(79, 70, 229, 0.1);
627
+ border: 1.5px solid rgba(79, 70, 229, 0.22);
628
+ border-radius: 50px;
629
+ font-size: 1rem;
630
+ font-weight: 800;
631
+ color: var(--f-p);
632
+ }
633
+
634
+ .theme-dark .advice-tag {
635
+ background: rgba(129, 140, 248, 0.12);
636
+ border-color: rgba(129, 140, 248, 0.25);
637
+ color: var(--f-a);
638
+ }
639
+
640
+ .layout-container {
641
+ min-width: 0;
642
+ }
643
+
644
+ .canvas-wrapper {
645
+ background: var(--bg-muted);
646
+ padding: 24px 24px 14px 12px;
647
+ border-radius: 28px;
648
+ }
649
+
650
+ .scheme-area {
651
+ display: flex;
652
+ align-items: stretch;
653
+ gap: 10px;
654
+ }
655
+
656
+ .ruler {
657
+ width: 34px;
658
+ flex-shrink: 0;
659
+ position: relative;
660
+ }
661
+
662
+ :global(.ruler-mark) {
663
+ position: absolute;
664
+ left: 0;
665
+ right: 0;
666
+ display: flex;
667
+ align-items: flex-start;
668
+ transform: translateY(-50%);
669
+ }
670
+
671
+ :global(.ruler-mark::before) {
672
+ content: '';
673
+ width: 8px;
674
+ height: 1.5px;
675
+ background: var(--text-muted);
676
+ flex-shrink: 0;
677
+ margin-top: 0.42em;
678
+ margin-right: 4px;
679
+ }
680
+
681
+ :global(.ruler-mark span) {
682
+ font-size: 0.58rem;
683
+ font-weight: 700;
684
+ color: var(--text-muted);
685
+ white-space: nowrap;
686
+ }
687
+
688
+ .fabric-board {
689
+ flex: 1;
690
+ height: 300px;
691
+ background: var(--bg-surface);
692
+ border: 2.5px solid var(--f-p);
693
+ padding: 6px 6px 28px;
694
+ box-shadow: 0 16px 36px rgba(0, 0, 0, 0.09);
695
+ display: flex;
696
+ flex-direction: column;
697
+ gap: 4px;
698
+ position: relative;
699
+ overflow: hidden;
700
+ border-radius: 4px;
701
+ }
702
+
703
+ :global(.piece-row) {
704
+ display: flex;
705
+ flex-direction: row;
706
+ gap: 4px;
707
+ flex-shrink: 0;
708
+ align-items: stretch;
709
+ }
710
+
711
+ :global(.piece-block) {
712
+ border-radius: 4px;
713
+ border: 1.5px solid;
714
+ display: flex;
715
+ flex-direction: column;
716
+ align-items: flex-start;
717
+ justify-content: center;
718
+ padding: 4px 8px;
719
+ overflow: hidden;
720
+ min-width: 0;
721
+ transition: filter 0.15s;
722
+ }
723
+
724
+ :global(.piece-block:hover) {
725
+ filter: brightness(1.08);
726
+ }
727
+
728
+ :global(.piece-name) {
729
+ font-size: 0.62rem;
730
+ font-weight: 800;
731
+ color: var(--text-main);
732
+ line-height: 1.2;
733
+ white-space: nowrap;
734
+ overflow: hidden;
735
+ text-overflow: ellipsis;
736
+ width: 100%;
737
+ }
738
+
739
+ :global(.piece-fold) {
740
+ border-left: 3px dashed rgba(79, 70, 229, 0.65);
741
+ }
742
+
743
+ :global(.fold-indicator) {
744
+ font-size: 0.48rem;
745
+ font-weight: 800;
746
+ color: rgba(79, 70, 229, 0.75);
747
+ text-transform: uppercase;
748
+ letter-spacing: 0.04em;
749
+ margin-top: 2px;
750
+ }
751
+
752
+ :global(.board-empty-state) {
753
+ flex: 1;
754
+ display: flex;
755
+ align-items: center;
756
+ justify-content: center;
757
+ font-size: 0.8rem;
758
+ font-weight: 600;
759
+ color: var(--text-muted);
760
+ text-align: center;
761
+ padding: 2rem;
762
+ }
763
+
764
+ .scheme-info {
765
+ font-size: 0.68rem;
766
+ font-weight: 700;
767
+ color: var(--text-muted);
768
+ margin-top: 0.6rem;
769
+ text-align: right;
770
+ }
771
+
772
+ .merch-section {
773
+ min-width: 0;
774
+ }
775
+
776
+ .merch-header {
777
+ display: flex;
778
+ align-items: center;
779
+ justify-content: space-between;
780
+ margin-bottom: 1rem;
781
+ padding-bottom: 0.5rem;
782
+ border-bottom: 2px solid var(--border-color);
783
+ }
784
+
785
+ .merch-title {
786
+ font-size: 0.7rem;
787
+ font-weight: 900;
788
+ text-transform: uppercase;
789
+ letter-spacing: 0.18rem;
790
+ color: var(--f-p);
791
+ margin: 0;
792
+ }
793
+
794
+ .copy-list-btn {
795
+ font-size: 0.7rem;
796
+ font-weight: 800;
797
+ color: var(--f-p);
798
+ background: rgba(79, 70, 229, 0.08);
799
+ border: 1px solid rgba(79, 70, 229, 0.2);
800
+ border-radius: 8px;
801
+ padding: 0.35rem 0.85rem;
802
+ cursor: pointer;
803
+ transition: all 0.2s;
804
+ }
805
+
806
+ .copy-list-btn:hover {
807
+ background: rgba(79, 70, 229, 0.15);
808
+ }
809
+
810
+ .merceria-checklist {
811
+ list-style: none;
812
+ margin: 0;
813
+ padding: 0;
814
+ display: flex;
815
+ flex-direction: column;
816
+ gap: 0.6rem;
817
+ }
818
+
819
+ :global(.merch-item) {
820
+ display: flex;
821
+ align-items: center;
822
+ gap: 0.6rem;
823
+ font-size: 0.875rem;
824
+ font-weight: 600;
825
+ color: var(--text-main);
826
+ }
827
+
828
+ :global(.merch-dot) {
829
+ color: var(--f-p);
830
+ font-size: 0.5rem;
831
+ flex-shrink: 0;
832
+ opacity: 0.65;
833
+ }
834
+
835
+ .warning-banner {
836
+ display: none;
837
+ background: #fff7ed;
838
+ color: #c2410c;
839
+ border: 1.5px solid #fed7aa;
840
+ padding: 1rem 1.25rem;
841
+ border-radius: 16px;
842
+ font-size: 0.85rem;
843
+ font-weight: 600;
844
+ }
845
+
846
+ @media (max-width: 1100px) {
847
+ .fabric-calculator-card {
848
+ grid-template-columns: 1fr;
849
+ width: 100%;
850
+ border-radius: 1rem;
851
+ margin: 0;
852
+ }
853
+
854
+ .fabric-sidebar {
855
+ border-right: none;
856
+ border-bottom: 1px solid var(--border-color);
857
+ padding: 1.75rem 1.5rem;
858
+ }
859
+
860
+ .btn-actions {
861
+ margin-top: 1rem;
862
+ }
863
+
864
+ .fabric-main {
865
+ padding: 2rem 1.5rem;
866
+ gap: 2rem;
867
+ }
868
+
869
+ .magic-result-area .val {
870
+ font-size: 4.5rem;
871
+ letter-spacing: -3px;
872
+ }
873
+ }
874
+
875
+ @media (max-width: 600px) {
876
+ .fabric-sidebar {
877
+ padding: 1.25rem;
878
+ }
879
+
880
+ .fabric-main {
881
+ padding: 1.25rem;
882
+ gap: 1.5rem;
883
+ }
884
+
885
+ .magic-result-area {
886
+ padding: 1.5rem 1.25rem;
887
+ border-radius: 1rem;
888
+ }
889
+
890
+ .magic-result-area .val {
891
+ font-size: 3rem;
892
+ letter-spacing: -2px;
893
+ }
894
+
895
+ .magic-result-area .unit {
896
+ font-size: 1.2rem;
897
+ }
898
+
899
+ .advice-tag {
900
+ font-size: 0.875rem;
901
+ padding: 0.7rem 1.25rem;
902
+ }
903
+
904
+ .canvas-wrapper {
905
+ padding: 14px 14px 10px 8px;
906
+ border-radius: 16px;
907
+ }
908
+
909
+ .fabric-board {
910
+ height: 220px;
911
+ padding-bottom: 20px;
912
+ }
913
+ }
914
+ </style>