@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,987 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { FiberPrepUI } from './ui';
4
+
5
+ interface Props {
6
+ ui?: Record<string, unknown>;
7
+ }
8
+
9
+ const { ui = {} } = Astro.props;
10
+ const prepUI = ui as FiberPrepUI;
11
+ const proteinFibers = prepUI.proteinFibers ?? [];
12
+ const cellulosicFibers = prepUI.cellulosicFibers ?? [];
13
+ ---
14
+
15
+ <div class="fiber-prep" id="fiber-prep-root">
16
+ <section class="profile-section">
17
+ <div class="profile-header">
18
+ <h3 class="profile-title">{prepUI.profileTitle ?? 'Profile Selection'}</h3>
19
+ <p class="profile-desc">{prepUI.profileDesc ?? ''}</p>
20
+ </div>
21
+
22
+ <div class="profile-grid">
23
+ <button class="fiber-card" data-fiber-btn data-fiber-type="protein">
24
+ <div class="card-bg-icon" aria-hidden="true">
25
+ <Icon name="mdi:sheep" />
26
+ </div>
27
+ <div class="card-body">
28
+ <div class="card-icon card-icon-protein">
29
+ <Icon name="mdi:sheep" />
30
+ </div>
31
+ <div class="card-text">
32
+ <h4 class="card-title">{prepUI.proteinTitle ?? 'Protein Fibers'}</h4>
33
+ <p class="card-origin card-origin-protein">{prepUI.proteinOrigin ?? 'Animal Origin'}</p>
34
+ <p class="card-desc">{prepUI.proteinDesc ?? ''}</p>
35
+ </div>
36
+ <div class="fiber-icon-list">
37
+ {proteinFibers.map((f) => (
38
+ <div class="fiber-icon-item" title={f.name}>
39
+ <Icon name={f.icon} />
40
+ </div>
41
+ ))}
42
+ </div>
43
+ </div>
44
+ </button>
45
+
46
+ <button class="fiber-card" data-fiber-btn data-fiber-type="cellulosic">
47
+ <div class="card-bg-icon" aria-hidden="true">
48
+ <Icon name="mdi:leaf" />
49
+ </div>
50
+ <div class="card-body">
51
+ <div class="card-icon card-icon-cellulosic">
52
+ <Icon name="mdi:leaf" />
53
+ </div>
54
+ <div class="card-text">
55
+ <h4 class="card-title">{prepUI.cellulosicTitle ?? 'Cellulosic Fibers'}</h4>
56
+ <p class="card-origin card-origin-cellulosic">{prepUI.cellulosicOrigin ?? 'Plant Origin'}</p>
57
+ <p class="card-desc">{prepUI.cellulosicDesc ?? ''}</p>
58
+ </div>
59
+ <div class="fiber-icon-list">
60
+ {cellulosicFibers.map((f) => (
61
+ <div class="fiber-icon-item" title={f.name}>
62
+ <Icon name={f.icon} />
63
+ </div>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ </button>
68
+ </div>
69
+ </section>
70
+
71
+ <div class="tool-grid">
72
+ <section class="calc-card">
73
+ <div class="calc-header">
74
+ <div class="calc-icon calc-icon-green">
75
+ <Icon name="mdi:calculator" />
76
+ </div>
77
+ <h3 class="calc-title">{prepUI.wofTitle ?? 'WOF Calculator'}</h3>
78
+ </div>
79
+
80
+ <div class="wof-body">
81
+ <div class="wof-overlay" id="wof-overlay">
82
+ <p class="overlay-msg">{prepUI.wofEmptyMsg ?? 'Select a profile'}</p>
83
+ </div>
84
+
85
+ <div class="field-group">
86
+ <label class="field-label" for="weight-input">{prepUI.weightLabel ?? 'Weight (g)'}</label>
87
+ <div class="field-input-wrap">
88
+ <input
89
+ type="number"
90
+ id="weight-input"
91
+ class="field-input"
92
+ placeholder="0"
93
+ min="0"
94
+ />
95
+ <span class="field-unit">{prepUI.weightUnit ?? 'g'}</span>
96
+ </div>
97
+ </div>
98
+
99
+ <div class="wof-results dimmed" id="wof-results">
100
+ <div class="result-row result-row-indigo">
101
+ <div class="result-row-left">
102
+ <Icon name="mdi:bottle-tonic-skull" class="result-icon" />
103
+ <span class="result-label">{prepUI.alumLabel ?? 'Alum'}</span>
104
+ </div>
105
+ <span class="result-val result-val-indigo" id="alum-val">0g</span>
106
+ </div>
107
+ <div class="result-row result-row-amber" id="cot-container">
108
+ <div class="result-row-left">
109
+ <Icon name="mdi:spoon-sugar" class="result-icon" />
110
+ <span class="result-label">{prepUI.cremLabel ?? 'Cream of Tartar'}</span>
111
+ </div>
112
+ <span class="result-val result-val-amber" id="cot-val">0g</span>
113
+ </div>
114
+ <div class="result-row result-row-slate">
115
+ <div class="result-row-left">
116
+ <Icon name="mdi:nut" class="result-icon" />
117
+ <span class="result-label">{prepUI.ironLabel ?? 'Iron (Caution)'}</span>
118
+ </div>
119
+ <span class="result-val result-val-slate" id="iron-val">0g</span>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </section>
124
+
125
+ <section class="calc-card">
126
+ <div class="calc-header">
127
+ <div class="calc-icon calc-icon-orange">
128
+ <Icon name="mdi:chart-bell-curve" />
129
+ </div>
130
+ <h3 class="calc-title">{prepUI.rampTitle ?? 'Thermal Ramp'}</h3>
131
+ </div>
132
+
133
+ <div class="ramp-body">
134
+ <div class="ramp-empty" id="ramp-empty">
135
+ <div class="ramp-empty-icon">
136
+ <Icon name="mdi:clock-outline" />
137
+ </div>
138
+ <p class="ramp-empty-title">{prepUI.rampEmptyTitle ?? 'Awaiting Selection'}</p>
139
+ <p class="ramp-empty-desc">{prepUI.rampEmptyDesc ?? ''}</p>
140
+ </div>
141
+
142
+ <div class="ramp-active hidden" id="ramp-active">
143
+ <div class="ramp-chart-wrap">
144
+ <svg viewBox="0 0 240 130" class="ramp-svg" id="ramp-svg">
145
+ <defs>
146
+ <linearGradient id="rampGradient" x1="0" y1="0" x2="0" y2="1">
147
+ <stop offset="0%" stop-color="rgb(99 102 241 / 0.2)"></stop>
148
+ <stop offset="100%" stop-color="transparent"></stop>
149
+ </linearGradient>
150
+ </defs>
151
+ <g class="ramp-labels">
152
+ <text x="0" y="23">90°C</text>
153
+ <text x="0" y="63">60°C</text>
154
+ <text x="0" y="103">30°C</text>
155
+ </g>
156
+ <g stroke="currentcolor" class="ramp-grid">
157
+ <line x1="25" y1="20" x2="230" y2="20" stroke-dasharray="2 2"></line>
158
+ <line x1="25" y1="60" x2="230" y2="60" stroke-dasharray="2 2"></line>
159
+ <line x1="25" y1="100" x2="230" y2="100" stroke-width="1"></line>
160
+ </g>
161
+ <path id="ramp-fill" d="M 25 100 L 230 100 L 230 100 L 25 100 Z" fill="url(#rampGradient)" class="ramp-fill-path"></path>
162
+ <path id="ramp-path" d="M 25 100 L 230 100" fill="none" stroke="currentcolor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="ramp-line"></path>
163
+ <g class="ramp-time-labels">
164
+ <text x="25" y="115">0'</text>
165
+ <text x="93" y="115">60'</text>
166
+ <text x="161" y="115">120'</text>
167
+ <text x="210" y="115">180'</text>
168
+ </g>
169
+ </svg>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </section>
174
+ </div>
175
+
176
+ <section class="calc-card ph-card">
177
+ <div class="calc-header">
178
+ <div class="calc-icon calc-icon-purple">
179
+ <Icon name="mdi:beaker-outline" />
180
+ </div>
181
+ <div class="ph-header-text">
182
+ <h3 class="calc-title">{prepUI.phTitle ?? 'pH Stabilizer'}</h3>
183
+ <p class="ph-subtitle">{prepUI.phDesc ?? ''}</p>
184
+ </div>
185
+ </div>
186
+
187
+ <div class="ph-body">
188
+ <div class="ph-overlay" id="ph-overlay">
189
+ <p class="overlay-msg">{prepUI.phEmptyMsg ?? 'Select a profile'}</p>
190
+ </div>
191
+
192
+ <div class="ph-grid">
193
+ <div class="ph-field">
194
+ <label class="field-label" for="ph-vol">{prepUI.phVolumeLabel ?? 'Bath Volume (L)'}</label>
195
+ <input type="number" id="ph-vol" class="field-input" value="5" min="0.1" />
196
+ </div>
197
+ <div class="ph-field">
198
+ <label class="field-label" for="ph-slider">{prepUI.phSliderLabel ?? 'Current pH'}</label>
199
+ <input type="range" id="ph-slider" min="1" max="14" step="0.5" value="7" class="ph-slider" />
200
+ <div class="ph-range-labels">
201
+ <span>{prepUI.phAcidLabel ?? 'Acid (1)'}</span>
202
+ <span id="ph-current-label">7.0</span>
203
+ <span>{prepUI.phAlkalineLabel ?? 'Alkaline (14)'}</span>
204
+ </div>
205
+ </div>
206
+ <div class="ph-result">
207
+ <span id="ph-advice">{prepUI.phStrings?.optimal ?? 'Optimal pH'}</span>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </section>
212
+ </div>
213
+
214
+ <script define:vars={{ ui }} is:inline>
215
+ window.__fiberPrepUI = ui;
216
+ </script>
217
+
218
+ <script>
219
+ import type { FiberPrepUI } from './ui';
220
+ import type { FiberType, WOFResult, PhStrings } from './logic';
221
+
222
+ const rawUI = (window as unknown as { __fiberPrepUI: FiberPrepUI }).__fiberPrepUI;
223
+
224
+ let selectedType: FiberType | null = null;
225
+
226
+ function getEl(id: string): HTMLElement | null {
227
+ return document.getElementById(id);
228
+ }
229
+
230
+ function setElText(id: string, text: string) {
231
+ const el = getEl(id);
232
+ if (el) el.textContent = text;
233
+ }
234
+
235
+ function calcWOF(w: number, type: FiberType): WOFResult {
236
+ if (type === 'protein') {
237
+ return { alum: w * 0.15, creamOfTartar: w * 0.02, iron: w * 0.01 };
238
+ }
239
+ return { alum: w * 0.2, creamOfTartar: 0, iron: w * 0.02 };
240
+ }
241
+
242
+ function buildPhAdvice(ph: number, target: number, vol: number, s: PhStrings): string {
243
+ const diff = target - ph;
244
+ if (Math.abs(diff) < 0.1) return s.optimal;
245
+ if (diff > 0) {
246
+ return `${s.raisePrefix} ${(diff * vol * 0.5).toFixed(1)}g ${s.raiseSuffix} ${target}`;
247
+ }
248
+ return `${s.lowerPrefix} ${(Math.abs(diff) * vol * 0.2).toFixed(1)}ml ${s.lowerSuffix} ${target}`;
249
+ }
250
+
251
+ function updateWOF() {
252
+ if (!selectedType) return;
253
+ const weightEl = getEl('weight-input') as HTMLInputElement | null;
254
+ const w = weightEl ? parseFloat(weightEl.value) || 0 : 0;
255
+ const r = calcWOF(w, selectedType);
256
+ setElText('alum-val', `${r.alum.toFixed(1)}g`);
257
+ setElText('cot-val', `${r.creamOfTartar.toFixed(1)}g`);
258
+ setElText('iron-val', `${r.iron.toFixed(1)}g`);
259
+ getEl('wof-overlay')?.classList.add('hidden');
260
+ getEl('wof-results')?.classList.remove('dimmed');
261
+ const cotRow = getEl('cot-container');
262
+ if (cotRow) cotRow.classList.toggle('na', selectedType !== 'protein');
263
+ }
264
+
265
+ const RAMP_PATHS = {
266
+ protein: {
267
+ line: 'M 25 115 L 65 30 L 165 30 L 225 115',
268
+ fill: 'M 25 115 L 65 30 L 165 30 L 225 115 L 225 115 L 25 115 Z',
269
+ },
270
+ cellulosic: {
271
+ line: 'M 25 115 L 85 115 L 115 40 L 195 40 L 225 115',
272
+ fill: 'M 25 115 L 85 115 L 115 40 L 195 40 L 225 115 L 225 115 L 25 115 Z',
273
+ },
274
+ };
275
+
276
+ function updateRamp() {
277
+ if (!selectedType) return;
278
+ const paths = RAMP_PATHS[selectedType];
279
+ getEl('ramp-path')?.setAttribute('d', paths.line);
280
+ getEl('ramp-fill')?.setAttribute('d', paths.fill);
281
+ getEl('ramp-empty')?.classList.add('hidden');
282
+ getEl('ramp-active')?.classList.remove('hidden');
283
+ }
284
+
285
+ function updatePH() {
286
+ const sliderEl = getEl('ph-slider') as HTMLInputElement | null;
287
+ const volEl = getEl('ph-vol') as HTMLInputElement | null;
288
+ const ph = sliderEl ? parseFloat(sliderEl.value) : 7;
289
+ const vol = volEl ? parseFloat(volEl.value) || 5 : 5;
290
+ setElText('ph-current-label', ph.toFixed(1));
291
+ getEl('ph-overlay')?.classList.add('hidden');
292
+ const target = selectedType === 'protein' ? 4.5 : 7.0;
293
+ const advice = buildPhAdvice(ph, target, vol, rawUI.phStrings);
294
+ setElText('ph-advice', advice);
295
+ }
296
+
297
+ function updateButtonStates(type: FiberType) {
298
+ const btns = document.querySelectorAll('[data-fiber-btn]');
299
+ btns.forEach((btn) => {
300
+ const isActive = (btn as HTMLElement).dataset.fiberType === type;
301
+ btn.classList.toggle('active', isActive);
302
+ });
303
+ }
304
+
305
+ function activateFiberType(type: FiberType) {
306
+ selectedType = type;
307
+ updateButtonStates(type);
308
+ updateWOF();
309
+ updateRamp();
310
+ updatePH();
311
+ }
312
+
313
+ function init() {
314
+ const btns = document.querySelectorAll('[data-fiber-btn]');
315
+ btns.forEach((btn) => {
316
+ const el = btn as HTMLElement;
317
+ el.addEventListener('click', () => activateFiberType(el.dataset.fiberType as FiberType));
318
+ });
319
+ getEl('weight-input')?.addEventListener('input', updateWOF);
320
+ getEl('ph-slider')?.addEventListener('input', updatePH);
321
+ getEl('ph-vol')?.addEventListener('input', updatePH);
322
+ }
323
+
324
+ function startInit() {
325
+ if (document.readyState === 'loading') {
326
+ document.addEventListener('DOMContentLoaded', init);
327
+ } else {
328
+ setTimeout(init, 100);
329
+ }
330
+ document.addEventListener('astro:page-load', init);
331
+ }
332
+
333
+ startInit();
334
+ </script>
335
+
336
+ <style>
337
+ .fiber-prep {
338
+ width: 100%;
339
+ padding: 1rem;
340
+ display: flex;
341
+ flex-direction: column;
342
+ gap: 2rem;
343
+ }
344
+
345
+ .profile-section {
346
+ display: flex;
347
+ flex-direction: column;
348
+ gap: 1.5rem;
349
+ }
350
+
351
+ .profile-header {
352
+ text-align: center;
353
+ }
354
+
355
+ .profile-title {
356
+ font-size: 1.25rem;
357
+ font-weight: 900;
358
+ text-transform: uppercase;
359
+ letter-spacing: 0.05em;
360
+ color: var(--text-main);
361
+ margin: 0 0 0.5rem;
362
+ }
363
+
364
+ .profile-desc {
365
+ font-size: 0.875rem;
366
+ color: var(--text-muted);
367
+ margin: 0;
368
+ }
369
+
370
+ .profile-grid {
371
+ display: grid;
372
+ grid-template-columns: 1fr 1fr;
373
+ gap: 1.5rem;
374
+ }
375
+
376
+ .fiber-card {
377
+ position: relative;
378
+ display: flex;
379
+ flex-direction: column;
380
+ padding: 2rem;
381
+ border-radius: 2rem;
382
+ border: 2px solid var(--border-color);
383
+ background: var(--bg-surface);
384
+ text-align: left;
385
+ cursor: pointer;
386
+ overflow: hidden;
387
+ transition: border-color 0.2s, box-shadow 0.2s;
388
+ }
389
+
390
+ .fiber-card:hover {
391
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
392
+ }
393
+
394
+ .fiber-card[data-fiber-type="protein"].active {
395
+ border-color: #f59e0b;
396
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15);
397
+ }
398
+
399
+ .fiber-card[data-fiber-type="cellulosic"].active {
400
+ border-color: #10b981;
401
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
402
+ }
403
+
404
+ .card-bg-icon {
405
+ position: absolute;
406
+ top: 0;
407
+ right: 0;
408
+ padding: 2rem;
409
+ opacity: 0.05;
410
+ pointer-events: none;
411
+ transition: opacity 0.2s;
412
+ }
413
+
414
+ .fiber-card:hover .card-bg-icon {
415
+ opacity: 0.1;
416
+ }
417
+
418
+ .card-bg-icon svg {
419
+ width: 9rem;
420
+ height: 9rem;
421
+ }
422
+
423
+ .card-body {
424
+ position: relative;
425
+ z-index: 1;
426
+ display: flex;
427
+ flex-direction: column;
428
+ gap: 1rem;
429
+ }
430
+
431
+ .card-icon {
432
+ width: 3.5rem;
433
+ height: 3.5rem;
434
+ border-radius: 1rem;
435
+ display: flex;
436
+ align-items: center;
437
+ justify-content: center;
438
+ }
439
+
440
+ .card-icon svg {
441
+ width: 1.75rem;
442
+ height: 1.75rem;
443
+ }
444
+
445
+ .card-icon-protein {
446
+ background: rgba(245, 158, 11, 0.1);
447
+ color: #f59e0b;
448
+ }
449
+
450
+ .card-icon-cellulosic {
451
+ background: rgba(16, 185, 129, 0.1);
452
+ color: #10b981;
453
+ }
454
+
455
+ .card-title {
456
+ font-size: 1.125rem;
457
+ font-weight: 900;
458
+ text-transform: uppercase;
459
+ color: var(--text-main);
460
+ margin: 0 0 0.25rem;
461
+ }
462
+
463
+ .card-origin {
464
+ font-size: 0.65rem;
465
+ font-weight: 700;
466
+ text-transform: uppercase;
467
+ letter-spacing: 0.1em;
468
+ margin: 0 0 0.5rem;
469
+ }
470
+
471
+ .card-origin-protein {
472
+ color: rgba(245, 158, 11, 0.7);
473
+ }
474
+
475
+ .card-origin-cellulosic {
476
+ color: rgba(16, 185, 129, 0.7);
477
+ }
478
+
479
+ .card-desc {
480
+ font-size: 0.875rem;
481
+ color: var(--text-muted);
482
+ line-height: 1.5;
483
+ margin: 0;
484
+ }
485
+
486
+ .fiber-icon-list {
487
+ display: flex;
488
+ gap: 0.5rem;
489
+ flex-wrap: wrap;
490
+ }
491
+
492
+ .fiber-icon-item {
493
+ width: 2rem;
494
+ height: 2rem;
495
+ border-radius: 0.5rem;
496
+ background: var(--bg-page);
497
+ border: 1px solid var(--border-color);
498
+ display: flex;
499
+ align-items: center;
500
+ justify-content: center;
501
+ }
502
+
503
+ .fiber-icon-item svg {
504
+ width: 1rem;
505
+ height: 1rem;
506
+ color: var(--text-muted);
507
+ }
508
+
509
+ .tool-grid {
510
+ display: grid;
511
+ grid-template-columns: 1fr 1fr;
512
+ gap: 2rem;
513
+ }
514
+
515
+ .calc-card {
516
+ background: var(--bg-surface);
517
+ border: 1px solid var(--border-color);
518
+ border-radius: 1.5rem;
519
+ padding: 1.5rem;
520
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
521
+ display: flex;
522
+ flex-direction: column;
523
+ gap: 1.5rem;
524
+ }
525
+
526
+ .calc-header {
527
+ display: flex;
528
+ align-items: center;
529
+ gap: 1rem;
530
+ }
531
+
532
+ .calc-icon {
533
+ width: 2.5rem;
534
+ height: 2.5rem;
535
+ border-radius: 0.75rem;
536
+ display: flex;
537
+ align-items: center;
538
+ justify-content: center;
539
+ flex-shrink: 0;
540
+ }
541
+
542
+ .calc-icon svg {
543
+ width: 1.25rem;
544
+ height: 1.25rem;
545
+ }
546
+
547
+ .calc-icon-green {
548
+ background: rgba(16, 185, 129, 0.1);
549
+ color: #10b981;
550
+ }
551
+
552
+ .calc-icon-orange {
553
+ background: rgba(249, 115, 22, 0.1);
554
+ color: #f97316;
555
+ }
556
+
557
+ .calc-icon-purple {
558
+ background: rgba(139, 92, 246, 0.1);
559
+ color: #8b5cf6;
560
+ }
561
+
562
+ .calc-title {
563
+ font-size: 1rem;
564
+ font-weight: 700;
565
+ color: var(--text-main);
566
+ margin: 0;
567
+ }
568
+
569
+ .wof-body {
570
+ position: relative;
571
+ display: flex;
572
+ flex-direction: column;
573
+ gap: 1.5rem;
574
+ }
575
+
576
+ .wof-overlay {
577
+ position: absolute;
578
+ inset: -0.5rem;
579
+ z-index: 2;
580
+ background: rgba(255, 255, 255, 0.85);
581
+ backdrop-filter: blur(2px);
582
+ border-radius: 1rem;
583
+ display: flex;
584
+ align-items: center;
585
+ justify-content: center;
586
+ text-align: center;
587
+ padding: 1.5rem;
588
+ transition: opacity 0.5s;
589
+ }
590
+
591
+ .theme-dark .wof-overlay {
592
+ background: rgba(15, 23, 42, 0.85);
593
+ }
594
+
595
+ .wof-overlay.hidden {
596
+ display: none;
597
+ }
598
+
599
+ .ph-overlay {
600
+ position: absolute;
601
+ inset: -0.5rem;
602
+ z-index: 2;
603
+ background: rgba(255, 255, 255, 0.85);
604
+ backdrop-filter: blur(2px);
605
+ border-radius: 1rem;
606
+ display: flex;
607
+ align-items: center;
608
+ justify-content: center;
609
+ text-align: center;
610
+ padding: 1.5rem;
611
+ transition: opacity 0.5s;
612
+ }
613
+
614
+ .theme-dark .ph-overlay {
615
+ background: rgba(15, 23, 42, 0.85);
616
+ }
617
+
618
+ .ph-overlay.hidden {
619
+ display: none;
620
+ }
621
+
622
+ .overlay-msg {
623
+ font-size: 0.625rem;
624
+ font-weight: 900;
625
+ text-transform: uppercase;
626
+ letter-spacing: 0.1em;
627
+ color: var(--text-muted);
628
+ margin: 0;
629
+ }
630
+
631
+ .field-group {
632
+ display: flex;
633
+ flex-direction: column;
634
+ gap: 0.5rem;
635
+ }
636
+
637
+ .field-label {
638
+ font-size: 0.625rem;
639
+ font-weight: 900;
640
+ text-transform: uppercase;
641
+ letter-spacing: 0.1em;
642
+ color: var(--text-muted);
643
+ }
644
+
645
+ .field-input-wrap {
646
+ position: relative;
647
+ }
648
+
649
+ .field-input {
650
+ width: 100%;
651
+ background: var(--bg-page);
652
+ border: 2px solid var(--border-color);
653
+ border-radius: 1rem;
654
+ padding: 0.75rem 3rem 0.75rem 1rem;
655
+ font-size: 1.5rem;
656
+ font-weight: 900;
657
+ color: var(--text-main);
658
+ outline: none;
659
+ transition: border-color 0.2s;
660
+ box-sizing: border-box;
661
+ }
662
+
663
+ .field-input:focus {
664
+ border-color: #6366f1;
665
+ }
666
+
667
+ .field-unit {
668
+ position: absolute;
669
+ right: 1rem;
670
+ top: 50%;
671
+ transform: translateY(-50%);
672
+ color: var(--text-muted);
673
+ font-weight: 700;
674
+ }
675
+
676
+ .wof-results {
677
+ display: flex;
678
+ flex-direction: column;
679
+ gap: 0.75rem;
680
+ transition: opacity 0.5s, filter 0.5s;
681
+ }
682
+
683
+ .wof-results.dimmed {
684
+ opacity: 0.4;
685
+ filter: grayscale(1);
686
+ pointer-events: none;
687
+ }
688
+
689
+ .result-row {
690
+ display: flex;
691
+ align-items: center;
692
+ justify-content: space-between;
693
+ padding: 1rem;
694
+ border-radius: 1rem;
695
+ border: 1px solid transparent;
696
+ transition: opacity 0.3s;
697
+ }
698
+
699
+ .result-row.na {
700
+ opacity: 0.3;
701
+ }
702
+
703
+ .result-row-indigo {
704
+ background: rgba(99, 102, 241, 0.05);
705
+ border-color: rgba(99, 102, 241, 0.15);
706
+ }
707
+
708
+ .result-row-amber {
709
+ background: rgba(245, 158, 11, 0.05);
710
+ border-color: rgba(245, 158, 11, 0.15);
711
+ }
712
+
713
+ .result-row-slate {
714
+ background: var(--bg-page);
715
+ border-color: var(--border-color);
716
+ }
717
+
718
+ .result-row-left {
719
+ display: flex;
720
+ align-items: center;
721
+ gap: 0.75rem;
722
+ }
723
+
724
+ .result-icon {
725
+ width: 1.25rem;
726
+ height: 1.25rem;
727
+ }
728
+
729
+ .result-row-indigo .result-icon {
730
+ color: #6366f1;
731
+ }
732
+
733
+ .result-row-amber .result-icon {
734
+ color: #f59e0b;
735
+ }
736
+
737
+ .result-row-slate .result-icon {
738
+ color: var(--text-muted);
739
+ }
740
+
741
+ .result-label {
742
+ font-size: 0.875rem;
743
+ font-weight: 700;
744
+ color: var(--text-main);
745
+ }
746
+
747
+ .result-val {
748
+ font-size: 1.25rem;
749
+ font-weight: 900;
750
+ }
751
+
752
+ .result-val-indigo {
753
+ color: #6366f1;
754
+ }
755
+
756
+ .result-val-amber {
757
+ color: #f59e0b;
758
+ }
759
+
760
+ .result-val-slate {
761
+ color: var(--text-muted);
762
+ }
763
+
764
+ .ramp-body {
765
+ position: relative;
766
+ min-height: 200px;
767
+ display: flex;
768
+ align-items: center;
769
+ justify-content: center;
770
+ }
771
+
772
+ .ramp-empty {
773
+ display: flex;
774
+ flex-direction: column;
775
+ align-items: center;
776
+ justify-content: center;
777
+ gap: 0.75rem;
778
+ text-align: center;
779
+ padding: 2rem;
780
+ transition: opacity 0.5s;
781
+ }
782
+
783
+ .ramp-empty.hidden {
784
+ display: none;
785
+ }
786
+
787
+ .ramp-empty-icon {
788
+ width: 4rem;
789
+ height: 4rem;
790
+ border-radius: 1.5rem;
791
+ background: var(--bg-page);
792
+ border: 2px dashed var(--border-color);
793
+ display: flex;
794
+ align-items: center;
795
+ justify-content: center;
796
+ color: var(--text-muted);
797
+ }
798
+
799
+ .ramp-empty-icon svg {
800
+ width: 2rem;
801
+ height: 2rem;
802
+ }
803
+
804
+ .ramp-empty-title {
805
+ font-size: 0.875rem;
806
+ font-weight: 700;
807
+ color: var(--text-muted);
808
+ margin: 0;
809
+ }
810
+
811
+ .ramp-empty-desc {
812
+ font-size: 0.65rem;
813
+ font-weight: 700;
814
+ text-transform: uppercase;
815
+ letter-spacing: 0.1em;
816
+ color: var(--text-muted);
817
+ opacity: 0.6;
818
+ margin: 0;
819
+ }
820
+
821
+ .ramp-active {
822
+ width: 100%;
823
+ animation: fade-in 0.7s ease;
824
+ }
825
+
826
+ .ramp-active.hidden {
827
+ display: none;
828
+ }
829
+
830
+ .ramp-chart-wrap {
831
+ width: 100%;
832
+ aspect-ratio: 2 / 1;
833
+ background: var(--bg-page);
834
+ border: 1px solid var(--border-color);
835
+ border-radius: 1rem;
836
+ padding: 1rem;
837
+ overflow: hidden;
838
+ }
839
+
840
+ .ramp-svg {
841
+ width: 100%;
842
+ height: 100%;
843
+ overflow: visible;
844
+ }
845
+
846
+ .ramp-labels {
847
+ font-size: 7px;
848
+ font-weight: 900;
849
+ fill: var(--text-muted);
850
+ text-transform: uppercase;
851
+ }
852
+
853
+ .ramp-grid {
854
+ color: var(--border-color);
855
+ stroke-width: 0.5;
856
+ }
857
+
858
+ .ramp-fill-path {
859
+ transition: all 1s ease;
860
+ }
861
+
862
+ .ramp-line {
863
+ color: #6366f1;
864
+ transition: all 1s ease;
865
+ }
866
+
867
+ .ramp-time-labels {
868
+ font-size: 7px;
869
+ font-weight: 900;
870
+ fill: var(--text-muted);
871
+ text-transform: uppercase;
872
+ }
873
+
874
+ .ph-card {
875
+ gap: 1.5rem;
876
+ }
877
+
878
+ .ph-header-text {
879
+ display: flex;
880
+ flex-direction: column;
881
+ gap: 0.125rem;
882
+ }
883
+
884
+ .ph-subtitle {
885
+ font-size: 0.75rem;
886
+ color: var(--text-muted);
887
+ margin: 0;
888
+ }
889
+
890
+ .ph-body {
891
+ position: relative;
892
+ }
893
+
894
+ .ph-grid {
895
+ display: grid;
896
+ grid-template-columns: 1fr 1fr 1fr;
897
+ gap: 1.5rem;
898
+ align-items: start;
899
+ }
900
+
901
+ .ph-field {
902
+ display: flex;
903
+ flex-direction: column;
904
+ gap: 0.75rem;
905
+ }
906
+
907
+ .ph-slider {
908
+ width: 100%;
909
+ height: 0.5rem;
910
+ border-radius: 9999px;
911
+ background: var(--border-color);
912
+ appearance: none;
913
+ cursor: pointer;
914
+ accent-color: #8b5cf6;
915
+ }
916
+
917
+ .ph-range-labels {
918
+ display: flex;
919
+ justify-content: space-between;
920
+ font-size: 0.625rem;
921
+ font-weight: 700;
922
+ color: var(--text-muted);
923
+ padding: 0 0.25rem;
924
+ }
925
+
926
+ .ph-result {
927
+ background: var(--bg-page);
928
+ border: 2px dashed var(--border-color);
929
+ border-radius: 1rem;
930
+ padding: 1rem;
931
+ display: flex;
932
+ align-items: center;
933
+ justify-content: center;
934
+ min-height: 4rem;
935
+ }
936
+
937
+ .ph-result span {
938
+ font-size: 0.875rem;
939
+ font-weight: 700;
940
+ color: var(--text-main);
941
+ text-align: center;
942
+ }
943
+
944
+ input[type="number"]::-webkit-inner-spin-button,
945
+ input[type="number"]::-webkit-outer-spin-button {
946
+ -webkit-appearance: none;
947
+ margin: 0;
948
+ }
949
+
950
+ @keyframes fade-in {
951
+ from {
952
+ opacity: 0;
953
+ }
954
+ to {
955
+ opacity: 1;
956
+ }
957
+ }
958
+
959
+ @keyframes slide-in {
960
+ from {
961
+ opacity: 0;
962
+ transform: translateY(10px);
963
+ }
964
+ to {
965
+ opacity: 1;
966
+ transform: translateY(0);
967
+ }
968
+ }
969
+
970
+ [data-fiber-btn] {
971
+ animation: slide-in 0.3s ease-out forwards;
972
+ }
973
+
974
+ @media (max-width: 768px) {
975
+ .profile-grid {
976
+ grid-template-columns: 1fr;
977
+ }
978
+
979
+ .tool-grid {
980
+ grid-template-columns: 1fr;
981
+ }
982
+
983
+ .ph-grid {
984
+ grid-template-columns: 1fr;
985
+ }
986
+ }
987
+ </style>