@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,828 @@
1
+ ---
2
+ import type { KnittingGaugeUI } from './ui';
3
+
4
+ interface Props {
5
+ ui?: Record<string, unknown>;
6
+ }
7
+
8
+ const { ui = {} } = Astro.props;
9
+ const gaugeUI = ui as unknown as KnittingGaugeUI;
10
+ ---
11
+
12
+ <div class="knitting-gauge-card" id="knitting-gauge-app">
13
+ <aside class="gauge-sidebar">
14
+ <div class="config-section">
15
+ <h3>{gaugeUI.sectionOriginalGauge}</h3>
16
+ <div class="input-grid">
17
+ <div class="gauge-field">
18
+ <label>{gaugeUI.labelStitches} <span class="label-sub">({gaugeUI.labelWidth})</span></label>
19
+ <div class="stepper-container">
20
+ <button class="stepper-btn" data-step="-0.5" data-for="pattern-sts">−</button>
21
+ <input type="number" id="pattern-sts" class="gauge-input" value="20" step="0.5" />
22
+ <button class="stepper-btn" data-step="0.5" data-for="pattern-sts">+</button>
23
+ </div>
24
+ </div>
25
+ <div class="gauge-field">
26
+ <label>{gaugeUI.labelRows} <span class="label-sub">({gaugeUI.labelLength})</span></label>
27
+ <div class="stepper-container">
28
+ <button class="stepper-btn" data-step="-0.5" data-for="pattern-rows">−</button>
29
+ <input type="number" id="pattern-rows" class="gauge-input" value="28" step="0.5" />
30
+ <button class="stepper-btn" data-step="0.5" data-for="pattern-rows">+</button>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ <div class="gauge-field">
35
+ <label>{gaugeUI.labelUnit}</label>
36
+ <select id="gauge-unit" class="gauge-select">
37
+ <option value="10">{gaugeUI.unitEU}</option>
38
+ <option value="10.16">{gaugeUI.unitUS}</option>
39
+ </select>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="config-section">
44
+ <h3>{gaugeUI.sectionMyGauge}</h3>
45
+ <div class="input-grid">
46
+ <div class="gauge-field">
47
+ <label>{gaugeUI.labelStitches} <span class="label-sub">({gaugeUI.labelWidth})</span></label>
48
+ <div class="stepper-container">
49
+ <button class="stepper-btn" data-step="-0.5" data-for="my-sts">−</button>
50
+ <input type="number" id="my-sts" class="gauge-input" value="22" step="0.5" />
51
+ <button class="stepper-btn" data-step="0.5" data-for="my-sts">+</button>
52
+ </div>
53
+ </div>
54
+ <div class="gauge-field">
55
+ <label>{gaugeUI.labelRows} <span class="label-sub">({gaugeUI.labelLength})</span></label>
56
+ <div class="stepper-container">
57
+ <button class="stepper-btn" data-step="-0.5" data-for="my-rows">−</button>
58
+ <input type="number" id="my-rows" class="gauge-input" value="30" step="0.5" />
59
+ <button class="stepper-btn" data-step="0.5" data-for="my-rows">+</button>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ <div class="input-grid">
64
+ <div class="gauge-field">
65
+ <label>{gaugeUI.labelNeedleMm}</label>
66
+ <input type="number" id="my-needle" class="gauge-input gauge-input-bordered" value="4.0" step="0.25" />
67
+ </div>
68
+ <div class="gauge-field">
69
+ <label>{gaugeUI.labelWeight} <span class="label-sub">{gaugeUI.labelWeightOptional}</span></label>
70
+ <input type="number" id="sample-weight" class="gauge-input gauge-input-bordered" placeholder="5" />
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="config-section">
76
+ <h3>{gaugeUI.sectionProject}</h3>
77
+ <div class="input-grid">
78
+ <div class="gauge-field">
79
+ <label>{gaugeUI.labelPatternSts}</label>
80
+ <input type="number" id="target-sts" class="gauge-input gauge-input-bordered" value="100" />
81
+ </div>
82
+ <div class="gauge-field">
83
+ <label>{gaugeUI.labelPatternRows}</label>
84
+ <input type="number" id="target-rows" class="gauge-input gauge-input-bordered" value="140" />
85
+ </div>
86
+ </div>
87
+ <div class="gauge-field">
88
+ <label>{gaugeUI.labelMultiples} <span class="label-sub">{gaugeUI.labelMultiplesExample}</span></label>
89
+ <input
90
+ type="text"
91
+ id="pattern-multiples"
92
+ class="gauge-input gauge-input-bordered"
93
+ placeholder={gaugeUI.labelMultiplesPlaceholder}
94
+ />
95
+ </div>
96
+ </div>
97
+
98
+ <div class="btn-group">
99
+ <button id="btn-clear" class="ui-btn ui-btn-secondary">{gaugeUI.btnClear}</button>
100
+ <button id="btn-share" class="ui-btn ui-btn-primary">{gaugeUI.btnShare}</button>
101
+ </div>
102
+ </aside>
103
+
104
+ <main class="gauge-main">
105
+ <div class="top-display">
106
+ <div class="visualizer-container">
107
+ <div id="gauge-visual-swatch" class="knit-swatch"></div>
108
+ </div>
109
+ <div class="main-stat">
110
+ <h2>{gaugeUI.resSuggestedSts}</h2>
111
+ <div class="val" id="res-total-sts">110</div>
112
+ <div class="val-detail" id="res-multiples-note"></div>
113
+ <div id="res-danger-info" class="alert-card alert-card-err"></div>
114
+ </div>
115
+ </div>
116
+
117
+ <div class="metrics-grid">
118
+ <div class="metric-item">
119
+ <span class="sl">
120
+ {gaugeUI.resNewRows}
121
+ <i class="info-icon" data-tooltip={gaugeUI.resNewRowsTooltip}>i</i>
122
+ </span>
123
+ <div class="sv" id="res-total-rows">150</div>
124
+ </div>
125
+ <div class="metric-item">
126
+ <span class="sl">
127
+ {gaugeUI.resScaleFactor}
128
+ <i class="info-icon" data-tooltip={gaugeUI.resScaleFactorTooltip}>i</i>
129
+ </span>
130
+ <div class="sv" id="res-scale-factor">+10%</div>
131
+ </div>
132
+ <div class="metric-item">
133
+ <span class="sl">
134
+ {gaugeUI.resSuggestion}
135
+ <i class="info-icon" data-tooltip={gaugeUI.resSuggestionTooltip}>i</i>
136
+ </span>
137
+ <div class="sv sv-suggestion" id="res-size-suggestion"></div>
138
+ </div>
139
+ </div>
140
+
141
+ <div id="yarn-estimate-box" class="yarn-calc-extra">
142
+ <p class="yarn-title" id="yarn-title">{gaugeUI.yarnTitle}</p>
143
+ <p id="yarn-result"></p>
144
+ </div>
145
+
146
+ <div id="res-multiples-info" class="alert-card alert-card-warn"></div>
147
+ </main>
148
+ </div>
149
+
150
+ <script define:vars={{ ui }} is:inline>
151
+ window.__knittingGaugeUI = ui;
152
+ </script>
153
+
154
+ <script>
155
+ import type { KnittingGaugeUI } from './ui';
156
+
157
+ declare global {
158
+ interface Window {
159
+ __knittingGaugeUI: KnittingGaugeUI;
160
+ }
161
+ }
162
+
163
+ const INPUT_IDS = [
164
+ 'pattern-sts', 'pattern-rows', 'my-sts', 'my-rows',
165
+ 'target-sts', 'target-rows', 'pattern-multiples',
166
+ 'gauge-unit', 'my-needle', 'sample-weight',
167
+ ] as const;
168
+
169
+ let previousUnit = '10';
170
+
171
+ function getNum(id: string): number {
172
+ const el = document.getElementById(id) as HTMLInputElement | null;
173
+ return parseFloat(el?.value || '0') || 0;
174
+ }
175
+
176
+ function getStr(id: string): string {
177
+ const el = document.getElementById(id) as HTMLInputElement | null;
178
+ return el?.value || '';
179
+ }
180
+
181
+ function setText(id: string, text: string): void {
182
+ const el = document.getElementById(id);
183
+ if (el) el.textContent = text;
184
+ }
185
+
186
+ function setHTML(id: string, html: string): void {
187
+ const el = document.getElementById(id);
188
+ if (el) el.innerHTML = html;
189
+ }
190
+
191
+ function showEl(id: string, visible: boolean): void {
192
+ const el = document.getElementById(id);
193
+ if (el) el.style.display = visible ? 'block' : 'none';
194
+ }
195
+
196
+ function parseMultiplesStr(str: string): { base: number; offset: number } {
197
+ if (!str) return { base: 0, offset: 0 };
198
+ const match = str.replace(/\s+/g, '').match(/(\d+)(?:\+(\d+))?/);
199
+ if (!match) return { base: 0, offset: 0 };
200
+ return { base: parseInt(match[1] ?? '0') || 0, offset: parseInt(match[2] ?? '0') || 0 };
201
+ }
202
+
203
+ function adjustForMultiples(raw: number, base: number, offset: number): number {
204
+ const low = Math.floor((raw - offset) / base) * base + offset;
205
+ const high = Math.ceil((raw - offset) / base) * base + offset;
206
+ return Math.abs(raw - low) <= Math.abs(raw - high) ? low : high;
207
+ }
208
+
209
+ function buildSuggestionHTML(scaleDiff: number, needle: number): string {
210
+ const u = window.__knittingGaugeUI;
211
+ if (Math.abs(scaleDiff) < 2) {
212
+ return `<span style="color:#10b981">${u.suggestionIdeal}</span>`;
213
+ }
214
+ const step = Math.ceil(Math.abs(scaleDiff) / 5) * 0.25;
215
+ if (scaleDiff > 0) {
216
+ const adj = Math.max(2, needle - step);
217
+ const detail = u.suggestionDownDetail.replace('{n}', String(adj));
218
+ return `${u.suggestionDown}<br><small style="opacity:0.7;font-weight:400">${detail}</small>`;
219
+ }
220
+ const adj = needle + step;
221
+ const detail = u.suggestionUpDetail.replace('{n}', String(adj));
222
+ return `${u.suggestionUp}<br><small style="opacity:0.7;font-weight:400">${detail}</small>`;
223
+ }
224
+
225
+ function computeStitches(patternSts: number, mySts: number, targetSts: number): number {
226
+ const raw = Math.round(targetSts * mySts / patternSts);
227
+ const { base, offset } = parseMultiplesStr(getStr('pattern-multiples'));
228
+ if (base <= 0) {
229
+ showEl('res-multiples-note', false);
230
+ return raw;
231
+ }
232
+ const adj = adjustForMultiples(raw, base, offset);
233
+ const label = offset > 0 ? `${base}+${offset}` : String(base);
234
+ const note = window.__knittingGaugeUI.multiplesNoteFormat
235
+ .replace('{n}', String(raw))
236
+ .replace('{m}', label);
237
+ setText('res-multiples-note', note);
238
+ showEl('res-multiples-note', true);
239
+ return adj;
240
+ }
241
+
242
+ function updateSwatch(patternSts: number, mySts: number): void {
243
+ const swatch = document.getElementById('gauge-visual-swatch');
244
+ if (!swatch) return;
245
+ const scale = Math.min(Math.max(patternSts / mySts, 0.5), 1.3);
246
+ const density = 12 * mySts / patternSts;
247
+ swatch.style.transform = `scale(${scale})`;
248
+ swatch.style.backgroundSize = `${density}px ${density}px`;
249
+ }
250
+
251
+ function updateYarnEstimate(weight: number, totalScale: number): void {
252
+ if (weight <= 0) {
253
+ showEl('yarn-estimate-box', false);
254
+ return;
255
+ }
256
+ const grams = Math.ceil(weight * totalScale);
257
+ const safety = Math.ceil(grams * 0.1);
258
+ const u = window.__knittingGaugeUI;
259
+ const result = u.yarnResultFormat.replace('{g}', String(grams));
260
+ const safetyStr = u.yarnSafetyFormat.replace('{s}', String(safety));
261
+ setHTML('yarn-result', `${result} <small>${safetyStr}</small>`);
262
+ showEl('yarn-estimate-box', true);
263
+ }
264
+
265
+ function updateUrl(): void {
266
+ const params = new URLSearchParams();
267
+ INPUT_IDS.forEach((id) => {
268
+ const el = document.getElementById(id) as HTMLInputElement | null;
269
+ if (el?.value) params.set(id, el.value);
270
+ });
271
+ window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`);
272
+ }
273
+
274
+ function calculate(): void {
275
+ const patternSts = getNum('pattern-sts');
276
+ const patternRows = getNum('pattern-rows');
277
+ const mySts = getNum('my-sts');
278
+ const myRows = getNum('my-rows');
279
+ const targetSts = getNum('target-sts');
280
+ const targetRows = getNum('target-rows');
281
+ const weight = getNum('sample-weight');
282
+ const needle = getNum('my-needle');
283
+ if (!patternSts || !mySts) return;
284
+ const finalSts = computeStitches(patternSts, mySts, targetSts);
285
+ setText('res-total-sts', String(finalSts));
286
+ setText('res-total-rows', String(Math.round(targetRows * myRows / patternRows)));
287
+ const scaleDiff = (patternSts / mySts - 1) * 100;
288
+ const sign = scaleDiff >= 0 ? '+' : '';
289
+ setText('res-scale-factor', `${sign}${scaleDiff.toFixed(1)}%`);
290
+ setHTML('res-size-suggestion', buildSuggestionHTML(scaleDiff, needle));
291
+ showEl('res-danger-info', Math.abs(scaleDiff) > 25);
292
+ if (Math.abs(scaleDiff) > 25) setText('res-danger-info', window.__knittingGaugeUI.alertDanger);
293
+ updateSwatch(patternSts, mySts);
294
+ updateYarnEstimate(weight, (targetSts / patternSts) * (targetRows / patternRows));
295
+ updateUrl();
296
+ }
297
+
298
+ function handleUnitConversion(): void {
299
+ const currentUnit = getStr('gauge-unit');
300
+ if (currentUnit === previousUnit) return;
301
+ const ratio = parseFloat(currentUnit) / parseFloat(previousUnit);
302
+ ['pattern-sts', 'pattern-rows', 'my-sts', 'my-rows'].forEach((id) => {
303
+ const el = document.getElementById(id) as HTMLInputElement | null;
304
+ if (el?.value) el.value = (parseFloat(el.value) * ratio).toFixed(1);
305
+ });
306
+ previousUnit = currentUnit;
307
+ calculate();
308
+ }
309
+
310
+ function handleClear(): void {
311
+ INPUT_IDS.forEach((id) => {
312
+ if (id === 'gauge-unit') return;
313
+ const el = document.getElementById(id) as HTMLInputElement | null;
314
+ if (!el) return;
315
+ el.value = (el.id.includes('sts') || el.id.includes('rows')) ? '20' : '';
316
+ });
317
+ calculate();
318
+ }
319
+
320
+ function handleShare(): void {
321
+ navigator.clipboard.writeText(window.location.href);
322
+ const btn = document.getElementById('btn-share');
323
+ if (!btn) return;
324
+ const orig = btn.textContent;
325
+ btn.textContent = window.__knittingGaugeUI.btnCopied;
326
+ setTimeout(() => { if (btn) btn.textContent = orig; }, 2000);
327
+ }
328
+
329
+ function restoreFromUrl(): void {
330
+ const params = new URLSearchParams(window.location.search);
331
+ params.forEach((val, key) => {
332
+ const el = document.getElementById(key) as HTMLInputElement | null;
333
+ if (el) el.value = val;
334
+ });
335
+ previousUnit = getStr('gauge-unit') || '10';
336
+ }
337
+
338
+ function attachStepperListeners(): void {
339
+ document.querySelectorAll('.stepper-btn').forEach((btn) => {
340
+ btn.addEventListener('click', (ev) => {
341
+ const target = ev.currentTarget as HTMLButtonElement;
342
+ const forId = target.getAttribute('data-for') || '';
343
+ const step = parseFloat(target.getAttribute('data-step') || '1');
344
+ const input = document.getElementById(forId) as HTMLInputElement | null;
345
+ if (input) {
346
+ input.value = String(parseFloat(input.value || '0') + step);
347
+ calculate();
348
+ }
349
+ });
350
+ });
351
+ }
352
+
353
+ function init(): void {
354
+ restoreFromUrl();
355
+ attachStepperListeners();
356
+ const unitSel = document.getElementById('gauge-unit');
357
+ if (unitSel) unitSel.addEventListener('change', handleUnitConversion);
358
+ INPUT_IDS.forEach((id) => {
359
+ const el = document.getElementById(id);
360
+ if (el) el.addEventListener('input', calculate);
361
+ });
362
+ document.getElementById('btn-clear')?.addEventListener('click', handleClear);
363
+ document.getElementById('btn-share')?.addEventListener('click', handleShare);
364
+ calculate();
365
+ }
366
+
367
+ document.addEventListener('astro:page-load', init);
368
+ init();
369
+ </script>
370
+
371
+ <style>
372
+ .knitting-gauge-card {
373
+ --gauge-primary: #b06161;
374
+ --gauge-secondary: #ead7bb;
375
+ --gauge-accent: #d37676;
376
+ --gauge-bg: rgba(255, 255, 255, 0.75);
377
+ --gauge-sidebar-bg: rgba(255, 255, 255, 0.45);
378
+ --gauge-item-bg: rgba(255, 255, 255, 0.45);
379
+ --gauge-border: rgba(0, 0, 0, 0.05);
380
+ --gauge-input-bg: rgba(255, 255, 255, 0.6);
381
+ --gauge-radius: 24px;
382
+ --gauge-shadow: 0 15px 40px rgba(0, 0, 0, 0.08);
383
+
384
+ display: grid;
385
+ grid-template-columns: 320px 1fr;
386
+ max-width: 1150px;
387
+ width: 95%;
388
+ margin: 2rem auto;
389
+ background: var(--gauge-bg);
390
+ backdrop-filter: blur(15px);
391
+ border-radius: var(--gauge-radius);
392
+ overflow: hidden;
393
+ border: 1px solid rgba(255, 255, 255, 0.3);
394
+ box-shadow: var(--gauge-shadow);
395
+ color: #444;
396
+ }
397
+
398
+ :global(.theme-dark) .knitting-gauge-card {
399
+ --gauge-primary: #ff7e7e;
400
+ --gauge-secondary: #2d2d30;
401
+ --gauge-accent: #ffa4a4;
402
+ --gauge-bg: rgba(13, 13, 15, 0.95);
403
+ --gauge-sidebar-bg: rgba(0, 0, 0, 0.3);
404
+ --gauge-item-bg: rgba(255, 255, 255, 0.03);
405
+ --gauge-border: rgba(255, 255, 255, 0.1);
406
+ --gauge-input-bg: rgba(255, 255, 255, 0.05);
407
+ --gauge-shadow: 0 25px 60px rgba(0, 0, 0, 0.6);
408
+
409
+ color: #f3f4f6;
410
+ border-color: var(--gauge-border);
411
+ }
412
+
413
+ .gauge-sidebar {
414
+ background: var(--gauge-sidebar-bg);
415
+ padding: 1.5rem;
416
+ display: flex;
417
+ flex-direction: column;
418
+ gap: 1.5rem;
419
+ border-right: 1px solid var(--gauge-border);
420
+ max-height: 85vh;
421
+ overflow-y: auto;
422
+ }
423
+
424
+ .config-section {
425
+ display: flex;
426
+ flex-direction: column;
427
+ gap: 0.8rem;
428
+ }
429
+
430
+ .config-section h3 {
431
+ font-size: 0.7rem;
432
+ font-weight: 800;
433
+ text-transform: uppercase;
434
+ letter-spacing: 0.1rem;
435
+ color: var(--gauge-primary);
436
+ margin: 0;
437
+ display: flex;
438
+ align-items: center;
439
+ gap: 0.5rem;
440
+ }
441
+
442
+ .input-grid {
443
+ display: grid;
444
+ grid-template-columns: 1fr 1fr;
445
+ gap: 0.75rem;
446
+ }
447
+
448
+ .gauge-field {
449
+ display: flex;
450
+ flex-direction: column;
451
+ gap: 0.3rem;
452
+ position: relative;
453
+ }
454
+
455
+ .gauge-field label {
456
+ font-size: 0.6rem;
457
+ font-weight: 700;
458
+ text-transform: uppercase;
459
+ opacity: 0.6;
460
+ display: flex;
461
+ justify-content: space-between;
462
+ }
463
+
464
+ .label-sub {
465
+ font-weight: 400;
466
+ opacity: 0.8;
467
+ text-transform: none;
468
+ }
469
+
470
+ .stepper-container {
471
+ display: flex;
472
+ align-items: center;
473
+ background: var(--gauge-input-bg);
474
+ border: 1px solid var(--gauge-border);
475
+ border-radius: 8px;
476
+ overflow: hidden;
477
+ }
478
+
479
+ .stepper-container:focus-within {
480
+ border-color: var(--gauge-primary);
481
+ box-shadow: 0 0 0 2px rgba(176, 97, 97, 0.2);
482
+ }
483
+
484
+ .stepper-btn {
485
+ width: 32px;
486
+ height: 32px;
487
+ border: none;
488
+ background: rgba(0, 0, 0, 0.05);
489
+ cursor: pointer;
490
+ font-size: 1rem;
491
+ display: flex;
492
+ align-items: center;
493
+ justify-content: center;
494
+ transition: background 0.2s;
495
+ color: inherit;
496
+ }
497
+
498
+ :global(.theme-dark) .stepper-btn {
499
+ background: rgba(255, 255, 255, 0.04);
500
+ }
501
+
502
+ .stepper-btn:hover {
503
+ background: var(--gauge-secondary);
504
+ }
505
+
506
+ .gauge-input {
507
+ flex: 1;
508
+ width: 100%;
509
+ border: none;
510
+ background: transparent;
511
+ padding: 0.4rem 0.5rem;
512
+ font-size: 0.9rem;
513
+ text-align: center;
514
+ font-weight: 600;
515
+ color: inherit;
516
+ }
517
+
518
+ .gauge-input:focus {
519
+ outline: none;
520
+ }
521
+
522
+ .gauge-input-bordered {
523
+ border: 1px solid var(--gauge-border);
524
+ border-radius: 8px;
525
+ background: var(--gauge-input-bg);
526
+ }
527
+
528
+ .gauge-select {
529
+ width: 100%;
530
+ padding: 0.5rem;
531
+ border-radius: 8px;
532
+ border: 1px solid var(--gauge-border);
533
+ background: var(--gauge-input-bg);
534
+ font-size: 0.8rem;
535
+ color: inherit;
536
+ }
537
+
538
+ .gauge-main {
539
+ padding: 2.5rem;
540
+ display: flex;
541
+ flex-direction: column;
542
+ gap: 2rem;
543
+ justify-content: center;
544
+ }
545
+
546
+ .top-display {
547
+ display: grid;
548
+ grid-template-columns: auto 1fr;
549
+ gap: 3rem;
550
+ align-items: center;
551
+ }
552
+
553
+ .visualizer-container {
554
+ position: relative;
555
+ width: 180px;
556
+ height: 180px;
557
+ background: var(--gauge-sidebar-bg);
558
+ border-radius: 24px;
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ border: 1px dashed var(--gauge-border);
563
+ overflow: hidden;
564
+ }
565
+
566
+ .knit-swatch {
567
+ width: 130px;
568
+ height: 130px;
569
+ background-color: var(--gauge-primary);
570
+ background-image: radial-gradient(circle at 50% 50%, var(--gauge-accent) 1px, transparent 1px);
571
+ background-size: 12px 12px;
572
+ border-radius: 12px;
573
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
574
+ transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
575
+ position: relative;
576
+ }
577
+
578
+ .main-stat h2 {
579
+ font-size: 0.75rem;
580
+ font-weight: 800;
581
+ text-transform: uppercase;
582
+ opacity: 0.6;
583
+ margin: 0 0 0.5rem;
584
+ }
585
+
586
+ .main-stat .val {
587
+ font-size: 4rem;
588
+ font-weight: 950;
589
+ color: var(--gauge-primary);
590
+ line-height: 1;
591
+ }
592
+
593
+ .val-detail {
594
+ font-size: 0.8rem;
595
+ font-weight: 600;
596
+ opacity: 0.8;
597
+ }
598
+
599
+ .metrics-grid {
600
+ display: grid;
601
+ grid-template-columns: repeat(3, 1fr);
602
+ gap: 1rem;
603
+ }
604
+
605
+ .metric-item {
606
+ padding: 1.2rem;
607
+ background: var(--gauge-item-bg);
608
+ border: 1px solid var(--gauge-border);
609
+ border-radius: 18px;
610
+ display: flex;
611
+ flex-direction: column;
612
+ gap: 0.3rem;
613
+ position: relative;
614
+ }
615
+
616
+ .metric-item .sv {
617
+ font-size: 1.5rem;
618
+ font-weight: 800;
619
+ color: var(--gauge-accent);
620
+ }
621
+
622
+ .sv-suggestion {
623
+ font-size: 1rem;
624
+ line-height: 1.2;
625
+ }
626
+
627
+ .metric-item .sl {
628
+ font-size: 0.65rem;
629
+ font-weight: 700;
630
+ text-transform: uppercase;
631
+ opacity: 0.5;
632
+ display: flex;
633
+ align-items: center;
634
+ gap: 0.3rem;
635
+ }
636
+
637
+ .info-icon {
638
+ font-size: 0.75rem;
639
+ font-style: normal;
640
+ cursor: help;
641
+ opacity: 0.5;
642
+ background: rgba(0, 0, 0, 0.08);
643
+ border-radius: 50%;
644
+ width: 14px;
645
+ height: 14px;
646
+ display: inline-flex;
647
+ align-items: center;
648
+ justify-content: center;
649
+ }
650
+
651
+ :global(.theme-dark) .info-icon {
652
+ background: rgba(255, 255, 255, 0.12);
653
+ }
654
+
655
+ .info-icon:hover {
656
+ opacity: 1;
657
+ }
658
+
659
+ [data-tooltip] {
660
+ position: relative;
661
+ }
662
+
663
+ [data-tooltip]::before {
664
+ content: attr(data-tooltip);
665
+ position: absolute;
666
+ bottom: 125%;
667
+ left: 50%;
668
+ transform: translateX(-50%);
669
+ background: #333;
670
+ color: white;
671
+ padding: 0.5rem 0.8rem;
672
+ border-radius: 6px;
673
+ font-size: 0.7rem;
674
+ white-space: normal;
675
+ width: 200px;
676
+ text-align: center;
677
+ visibility: hidden;
678
+ opacity: 0;
679
+ transition: opacity 0.3s;
680
+ z-index: 100;
681
+ pointer-events: none;
682
+ }
683
+
684
+ :global(.theme-dark) [data-tooltip]::before {
685
+ background: #1f2937;
686
+ border: 1px solid rgba(255, 255, 255, 0.1);
687
+ }
688
+
689
+ [data-tooltip]:hover::before {
690
+ visibility: visible;
691
+ opacity: 1;
692
+ }
693
+
694
+ .yarn-calc-extra {
695
+ padding: 1rem;
696
+ background: rgba(176, 97, 97, 0.05);
697
+ border-radius: 12px;
698
+ font-size: 0.8rem;
699
+ border: 1px solid rgba(176, 97, 97, 0.1);
700
+ display: none;
701
+ }
702
+
703
+ :global(.theme-dark) .yarn-calc-extra {
704
+ background: rgba(255, 126, 126, 0.08);
705
+ border-color: rgba(255, 126, 126, 0.15);
706
+ }
707
+
708
+ .yarn-title {
709
+ margin: 0;
710
+ font-weight: 700;
711
+ color: var(--gauge-primary);
712
+ }
713
+
714
+ .alert-card {
715
+ padding: 1rem;
716
+ border-radius: 12px;
717
+ font-size: 0.82rem;
718
+ margin-top: 0.5rem;
719
+ display: none;
720
+ animation: fade-in 0.4s ease;
721
+ }
722
+
723
+ .alert-card-warn {
724
+ background: #fffbeb;
725
+ color: #92400e;
726
+ border: 1px solid #fde68a;
727
+ }
728
+
729
+ :global(.theme-dark) .alert-card-warn {
730
+ background: rgba(245, 158, 11, 0.15);
731
+ color: #fbbf24;
732
+ border-color: rgba(245, 158, 11, 0.3);
733
+ }
734
+
735
+ .alert-card-err {
736
+ background: #fef2f2;
737
+ color: #991b1b;
738
+ border: 1px solid #fecaca;
739
+ }
740
+
741
+ :global(.theme-dark) .alert-card-err {
742
+ background: rgba(239, 68, 68, 0.15);
743
+ color: #f87171;
744
+ border-color: rgba(239, 68, 68, 0.3);
745
+ }
746
+
747
+ .btn-group {
748
+ display: grid;
749
+ grid-template-columns: 1fr 1.5fr;
750
+ gap: 0.75rem;
751
+ margin-top: auto;
752
+ padding-top: 1rem;
753
+ }
754
+
755
+ .ui-btn {
756
+ padding: 0.8rem;
757
+ border-radius: 12px;
758
+ border: none;
759
+ font-size: 0.75rem;
760
+ font-weight: 800;
761
+ cursor: pointer;
762
+ text-transform: uppercase;
763
+ transition: all 0.2s;
764
+ }
765
+
766
+ .ui-btn-primary {
767
+ background: var(--gauge-primary);
768
+ color: white;
769
+ }
770
+
771
+ .ui-btn-primary:hover {
772
+ filter: brightness(1.1);
773
+ transform: translateY(-1px);
774
+ }
775
+
776
+ .ui-btn-secondary {
777
+ background: rgba(0, 0, 0, 0.05);
778
+ border: 1px solid rgba(0, 0, 0, 0.1);
779
+ color: inherit;
780
+ }
781
+
782
+ :global(.theme-dark) .ui-btn-secondary {
783
+ background: rgba(255, 255, 255, 0.06);
784
+ border-color: rgba(255, 255, 255, 0.15);
785
+ }
786
+
787
+ .ui-btn-secondary:hover {
788
+ background: rgba(0, 0, 0, 0.08);
789
+ }
790
+
791
+ :global(.theme-dark) .ui-btn-secondary:hover {
792
+ background: rgba(255, 255, 255, 0.12);
793
+ }
794
+
795
+ @keyframes fade-in {
796
+ from {
797
+ opacity: 0;
798
+ transform: translateY(10px);
799
+ }
800
+ to {
801
+ opacity: 1;
802
+ transform: translateY(0);
803
+ }
804
+ }
805
+
806
+ @media (max-width: 900px) {
807
+ .knitting-gauge-card {
808
+ grid-template-columns: 1fr;
809
+ max-height: none;
810
+ }
811
+
812
+ .gauge-sidebar {
813
+ border-right: none;
814
+ border-bottom: 1px solid var(--gauge-border);
815
+ max-height: none;
816
+ }
817
+
818
+ .top-display {
819
+ grid-template-columns: 1fr;
820
+ text-align: center;
821
+ justify-items: center;
822
+ }
823
+
824
+ .metrics-grid {
825
+ grid-template-columns: 1fr;
826
+ }
827
+ }
828
+ </style>