@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,34 @@
1
+ import type { TextilesToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
2
+ import NeedleConverterComponent from './component.astro';
3
+ import NeedleConverterSEO from './seo.astro';
4
+ import NeedleConverterBibliography from './bibliography.astro';
5
+
6
+ import type { NeedleConverterUI } from './ui';
7
+
8
+ export type NeedleConverterLocaleContent = ToolLocaleContent<NeedleConverterUI>;
9
+
10
+ import { content as es } from './i18n/es';
11
+ import { content as en } from './i18n/en';
12
+ import { content as fr } from './i18n/fr';
13
+
14
+ export const needleConverter: TextilesToolEntry<NeedleConverterUI> = {
15
+ id: 'needle-converter',
16
+ icons: {
17
+ bg: 'mdi:needle',
18
+ fg: 'mdi:swap-horizontal',
19
+ },
20
+ i18n: {
21
+ es: async () => es,
22
+ en: async () => en,
23
+ fr: async () => fr,
24
+ },
25
+ };
26
+
27
+ export { NeedleConverterComponent, NeedleConverterSEO, NeedleConverterBibliography };
28
+
29
+ export const NEEDLE_CONVERTER_TOOL: ToolDefinition = {
30
+ entry: needleConverter,
31
+ Component: NeedleConverterComponent,
32
+ SEOComponent: NeedleConverterSEO,
33
+ BibliographyComponent: NeedleConverterBibliography,
34
+ };
@@ -0,0 +1,31 @@
1
+ export interface NeedleItem {
2
+ mm: number;
3
+ usK: string;
4
+ usC: string;
5
+ uk: string;
6
+ yarn: string;
7
+ wpi: string;
8
+ }
9
+
10
+ export const NEEDLE_DATA: NeedleItem[] = [
11
+ { mm: 2.0, usK: '0', usC: 'B-1', uk: '14', yarn: 'Lace / Fingering', wpi: '18+' },
12
+ { mm: 2.25, usK: '1', usC: 'B-1', uk: '13', yarn: 'Lace / Fingering', wpi: '18+' },
13
+ { mm: 2.5, usK: '1.5', usC: 'C-2', uk: '12', yarn: 'Lace / Fingering', wpi: '18+' },
14
+ { mm: 2.75, usK: '2', usC: 'C-2', uk: '12', yarn: 'Fingering', wpi: '18-22' },
15
+ { mm: 3.0, usK: '2.5', usC: 'D-3', uk: '11', yarn: 'Fingering', wpi: '18-22' },
16
+ { mm: 3.25, usK: '3', usC: 'D-3', uk: '10', yarn: 'Sport', wpi: '14-16' },
17
+ { mm: 3.5, usK: '4', usC: 'E-4', uk: '9', yarn: 'Sport', wpi: '14-16' },
18
+ { mm: 3.75, usK: '5', usC: 'F-5', uk: '9', yarn: 'Sport', wpi: '14-16' },
19
+ { mm: 4.0, usK: '6', usC: 'G-6', uk: '8', yarn: 'DK / Light Worsted', wpi: '11-13' },
20
+ { mm: 4.5, usK: '7', usC: '7', uk: '7', yarn: 'DK / Light Worsted', wpi: '11-13' },
21
+ { mm: 5.0, usK: '8', usC: 'H-8', uk: '6', yarn: 'Worsted / Aran', wpi: '9-11' },
22
+ { mm: 5.5, usK: '9', usC: 'I-9', uk: '5', yarn: 'Worsted / Aran', wpi: '9-11' },
23
+ { mm: 6.0, usK: '10', usC: 'J-10', uk: '4', yarn: 'Bulky / Chunky', wpi: '7-8' },
24
+ { mm: 6.5, usK: '10.5', usC: 'K-10.5', uk: '3', yarn: 'Bulky / Chunky', wpi: '7-8' },
25
+ { mm: 7.0, usK: '-', usC: '-', uk: '2', yarn: 'Bulky / Chunky', wpi: '7-8' },
26
+ { mm: 8.0, usK: '11', usC: 'L-11', uk: '\u2014', yarn: 'Super Bulky', wpi: '5-6' },
27
+ { mm: 9.0, usK: '13', usC: 'M-13', uk: '\u2014', yarn: 'Super Bulky', wpi: '5-6' },
28
+ { mm: 10.0, usK: '15', usC: 'N-15', uk: '\u2014', yarn: 'Super Bulky', wpi: '5-6' },
29
+ ];
30
+
31
+ export const DEFAULT_IDX = 8;
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { needleConverter } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await needleConverter.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ <SEORenderer content={{ locale: locale as string, sections: content.seo }} />
@@ -0,0 +1,17 @@
1
+ export interface NeedleConverterUI {
2
+ labelSystem: string;
3
+ modeKnitting: string;
4
+ modeCrochet: string;
5
+ calibratorLabel: string;
6
+ calibratorDesc: string;
7
+ tagMetric: string;
8
+ tagUS: string;
9
+ tagUK: string;
10
+ yarnLabel: string;
11
+ wpiFormat: string;
12
+ tableSectionLabel: string;
13
+ tableMetric: string;
14
+ tableUS: string;
15
+ tableUK: string;
16
+ tableYarn: string;
17
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ ---
3
+ <ul>
4
+ <li>
5
+ <a href="https://www.scribd.com/document/696165843/patronaje-las-bases-by-teresa-gilewska" target="_blank" rel="noopener noreferrer">
6
+ Patronaje: Las Bases - Teresa Gilewska
7
+ </a>
8
+ </li>
9
+ <li>
10
+ <a href="https://download.e-bookshelf.de/download/0000/5989/88/L-G-0000598988-0002339537.pdf" target="_blank" rel="noopener noreferrer">
11
+ Metric Pattern Cutting for Women's Wear - Winifred Aldrich
12
+ </a>
13
+ </li>
14
+ </ul>
@@ -0,0 +1,550 @@
1
+ ---
2
+ import type { SewingPatternScalerUI } from './ui';
3
+
4
+ interface Props {
5
+ ui?: Record<string, unknown>;
6
+ }
7
+
8
+ const { ui = {} } = Astro.props;
9
+ const scalerUI = ui as SewingPatternScalerUI;
10
+ const SIZES = ['36', '38', '40', '42', '44', '46'];
11
+ ---
12
+
13
+ <div class="scaler-wrapper" id="master-scaler">
14
+ <div class="scaler-card">
15
+ <div class="card-left">
16
+ <span class="label-tiny">{scalerUI.labelSystem}</span>
17
+ <div class="segmented-control" id="mode-selector">
18
+ <button class="segment-btn active" data-mode="standard">{scalerUI.modeStandard}</button>
19
+ <button class="segment-btn" data-mode="custom">{scalerUI.modeCustom}</button>
20
+ </div>
21
+
22
+ <div id="warn-area" class="warn-banner hidden">
23
+ <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20" style="flex-shrink:0"><path d="M12,2L1,21H23L12,2M12,6L19.53,19H4.47L12,6M11,10V14H13V10H11M11,16V18H13V16H11Z"/></svg>
24
+ <span>{scalerUI.warnText}</span>
25
+ </div>
26
+
27
+ <div id="standard-grid">
28
+ <div class="input-grid">
29
+ <div class="input-group">
30
+ <label class="label-tiny">{scalerUI.labelPatternSize}</label>
31
+ <select id="origin-size" class="form-input form-select">
32
+ {SIZES.map((s) => <option value={s} selected={s === '38'}>{scalerUI.sizePrefix} {s}</option>)}
33
+ </select>
34
+ </div>
35
+ <div class="input-group">
36
+ <label class="label-tiny">{scalerUI.labelTargetSize}</label>
37
+ <select id="target-size" class="form-input form-select">
38
+ {SIZES.map((s) => <option value={s} selected={s === '42'}>{scalerUI.sizePrefix} {s}</option>)}
39
+ </select>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <div id="custom-grid" class="hidden">
45
+ <span class="label-tiny">{scalerUI.labelOriginMeasures}</span>
46
+ <div class="input-grid">
47
+ <div class="input-item"><label class="label-tiny">{scalerUI.labelChest}</label><input type="number" id="origin-chest" class="form-input" value="88" /></div>
48
+ <div class="input-item"><label class="label-tiny">{scalerUI.labelWaist}</label><input type="number" id="origin-waist" class="form-input" value="68" /></div>
49
+ <div class="input-item"><label class="label-tiny">{scalerUI.labelHips}</label><input type="number" id="origin-hips" class="form-input" value="94" /></div>
50
+ <div class="input-item"><label class="label-tiny">{scalerUI.labelLength}</label><input type="number" id="origin-length" class="form-input" value="60" /></div>
51
+ </div>
52
+ <span class="label-tiny target-lbl">{scalerUI.labelTargetMeasures}</span>
53
+ <div class="input-grid">
54
+ <div class="input-item"><label class="label-tiny">{scalerUI.labelChest}</label><input type="number" id="target-chest" class="form-input" value="96" /></div>
55
+ <div class="input-item"><label class="label-tiny">{scalerUI.labelWaist}</label><input type="number" id="target-waist" class="form-input" value="76" /></div>
56
+ <div class="input-item"><label class="label-tiny">{scalerUI.labelHips}</label><input type="number" id="target-hips" class="form-input" value="102" /></div>
57
+ <div class="input-item"><label class="label-tiny">{scalerUI.labelLength}</label><input type="number" id="target-length" class="form-input" value="62" /></div>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="ease-group">
62
+ <label class="label-tiny">{scalerUI.labelEase}</label>
63
+ <input type="number" id="ease-val" class="form-input" value="4" />
64
+ </div>
65
+ </div>
66
+
67
+ <div class="card-right">
68
+ <div class="viz-container">
69
+ <canvas id="scaler-canvas"></canvas>
70
+ <div class="legend-box">
71
+ <div class="legend-item"><span class="ln-o"></span><span>{scalerUI.legendOriginal}</span></div>
72
+ <div class="legend-item"><span class="ln-s"></span><span>{scalerUI.legendUpdated}</span></div>
73
+ </div>
74
+ </div>
75
+
76
+ <div class="action-strip" id="action-pills"></div>
77
+
78
+ <div class="totals-section">
79
+ <div class="totals-grid">
80
+ <div class="total-card"><span class="total-val" id="res-c">100.0</span><span class="total-tag">{scalerUI.resBust}</span></div>
81
+ <div class="total-card"><span class="total-val" id="res-w">80.0</span><span class="total-tag">{scalerUI.resWaist}</span></div>
82
+ <div class="total-card"><span class="total-val" id="res-h">106.0</span><span class="total-tag">{scalerUI.resHips}</span></div>
83
+ <div class="total-card"><span class="total-val" id="res-l">62.0</span><span class="total-tag">{scalerUI.resLength}</span></div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <script define:vars={{ ui }} is:inline>
91
+ window.__scalerUI = ui;
92
+ </script>
93
+
94
+ <script>
95
+ import type { SewingPatternScalerUI } from './ui';
96
+ import type { MeasureSet } from './logic';
97
+ import { getStandardMeasures, computeResult, computeLateralAdj, computeLengthAdj, isLargeScale } from './logic';
98
+
99
+ declare global {
100
+ interface Window { __scalerUI: SewingPatternScalerUI; }
101
+ }
102
+
103
+ const SVG1 = `<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M7 2H17C18.11 2 19 2.9 19 4V20C19 21.11 18.11 22 17 22H7C5.9 22 5 21.11 5 20V4C5 2.9 5.9 2 7 2M7 4V20H17V4H7M9 6H12V8H9V6M9 10H15V12H9V10M9 14H12V16H9V14Z"/></svg>`;
104
+ const SVG2 = `<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M19,3L13,9L15,11L19,7L21,9V3M13,10C12.5,10.5 12.5,11.5 13,12L15,14L19,10L13,10M3,17V21H7L19,9L15,5L3,17M6.12,19.88L5,18.76L16.06,7.7L17.18,8.82L6.12,19.88Z"/></svg>`;
105
+
106
+ function getNum(id: string): number {
107
+ return parseFloat((document.getElementById(id) as HTMLInputElement | null)?.value || '0') || 0;
108
+ }
109
+
110
+ function setText(id: string, v: string): void {
111
+ const el = document.getElementById(id);
112
+ if (el) el.innerText = v;
113
+ }
114
+
115
+ function getMode(): string {
116
+ const active = document.querySelector<HTMLElement>('#mode-selector .active');
117
+ return active?.dataset.mode || 'standard';
118
+ }
119
+
120
+ function getMeasures(pfx: string, mode: string): MeasureSet {
121
+ if (mode === 'standard') {
122
+ const el = document.getElementById(`${pfx}-size`) as HTMLSelectElement | null;
123
+ return getStandardMeasures(el?.value || '38');
124
+ }
125
+ const f = (id: string) => getNum(`${pfx}-${id}`);
126
+ return { chest: f('chest'), waist: f('waist'), hips: f('hips'), length: f('length') };
127
+ }
128
+
129
+ function buildActionHtml(lat: number, len: number, ui: SewingPatternScalerUI): string {
130
+ const latPos = lat >= 0;
131
+ const lenPos = len >= 0;
132
+ const latVerb = latPos ? ui.actionLatAdd : ui.actionLatRemove;
133
+ const lenVerb = lenPos ? ui.actionLenExtend : ui.actionLenShorten;
134
+ const latCls = latPos ? 'action-val-pos' : 'action-val-neg';
135
+ const lenCls = lenPos ? 'action-val-pos' : 'action-val-neg';
136
+ const latStr = `${latPos ? '+' : ''}${lat}`;
137
+ const lenStr = `${lenPos ? '+' : ''}${len}`;
138
+ return `<div class="action-pill"><div class="icon-box">${SVG1}</div><div class="action-content">` +
139
+ `<span class="action-text">${latVerb} <strong class="${latCls}">${latStr} cm</strong> ${ui.actionLatSuffix}</span>` +
140
+ `<span class="action-note">${ui.actionLatNote}</span></div></div>` +
141
+ `<div class="action-pill"><div class="icon-box">${SVG2}</div><div class="action-content">` +
142
+ `<span class="action-text">${lenVerb} <strong class="${lenCls}">${lenStr} cm</strong></span>` +
143
+ `<span class="action-note">${ui.actionLenNote}</span></div></div>`;
144
+ }
145
+
146
+ interface ShapeOpts { s: number; cX: number; cY: number; }
147
+
148
+ function drawShape(ctx: CanvasRenderingContext2D, d: MeasureSet, isO: boolean, opts: ShapeOpts): void {
149
+ const { s, cX, cY } = opts;
150
+ ctx.beginPath();
151
+ ctx.lineWidth = isO ? 1.5 : 3;
152
+ ctx.strokeStyle = isO ? 'rgba(148, 163, 184, 0.5)' : '#4f46e5';
153
+ if (isO) ctx.setLineDash([12, 10]); else ctx.setLineDash([]);
154
+ const cW = d.chest * s / 4;
155
+ const wW = d.waist * s / 4;
156
+ const hW = d.hips * s / 4;
157
+ const len = d.length * s;
158
+ const neckW = 2 * s;
159
+ const shldW = 5.5 * s;
160
+ ctx.moveTo(cX, cY);
161
+ ctx.quadraticCurveTo(cX - neckW * 0.8, cY, cX - neckW, cY + s);
162
+ ctx.lineTo(cX - shldW - d.chest * 0.04 * s / 4, cY + s * 0.2);
163
+ ctx.quadraticCurveTo(cX - shldW * 0.7 - d.chest * 0.12 * s / 4, cY + s * 2.5, cX - cW, cY + len * 0.4);
164
+ ctx.lineTo(cX - wW, cY + len * 0.65);
165
+ ctx.lineTo(cX - hW, cY + len);
166
+ ctx.lineTo(cX, cY + len);
167
+ ctx.lineTo(cX, cY);
168
+ ctx.stroke();
169
+ }
170
+
171
+ function draw(org: MeasureSet, tgt: MeasureSet): void {
172
+ const canvas = document.getElementById('scaler-canvas') as HTMLCanvasElement | null;
173
+ const ctx = canvas?.getContext('2d');
174
+ if (!canvas || !ctx) return;
175
+ const dpr = window.devicePixelRatio || 1;
176
+ const par = canvas.parentElement;
177
+ if (!par) return;
178
+ const w = par.clientWidth;
179
+ const h = par.clientHeight;
180
+ canvas.width = w * dpr;
181
+ canvas.height = h * dpr;
182
+ ctx.scale(dpr, dpr);
183
+ ctx.clearRect(0, 0, w, h);
184
+ const maxV = Math.max(org.chest, tgt.chest, org.length, tgt.length, 100);
185
+ const s = Math.min(w, h) * 0.85 / maxV;
186
+ const opts = { s, cX: w / 2, cY: h * 0.08 };
187
+ drawShape(ctx, org, true, opts);
188
+ drawShape(ctx, tgt, false, opts);
189
+ }
190
+
191
+ function run(): void {
192
+ const mode = getMode();
193
+ const ease = getNum('ease-val');
194
+ const org = getMeasures('origin', mode);
195
+ const tgt = getMeasures('target', mode);
196
+ const res = computeResult(tgt, ease);
197
+ const ui = window.__scalerUI;
198
+ setText('res-c', res.c.toFixed(1));
199
+ setText('res-w', res.w.toFixed(1));
200
+ setText('res-h', res.h.toFixed(1));
201
+ setText('res-l', res.l.toFixed(1));
202
+ const lat = computeLateralAdj(org.chest, tgt.chest);
203
+ const len = computeLengthAdj(org.length, tgt.length);
204
+ const pils = document.getElementById('action-pills');
205
+ if (pils) pils.innerHTML = buildActionHtml(lat, len, ui);
206
+ draw(org, tgt);
207
+ const warn = document.getElementById('warn-area');
208
+ if (warn) warn.classList.toggle('hidden', !isLargeScale(org.chest, tgt.chest));
209
+ }
210
+
211
+ function attachListeners(): void {
212
+ const modeBtns = document.querySelectorAll<HTMLElement>('#mode-selector button');
213
+ modeBtns.forEach((btn) => {
214
+ btn.addEventListener('click', () => {
215
+ modeBtns.forEach((b) => b.classList.remove('active'));
216
+ btn.classList.add('active');
217
+ const mode = btn.dataset.mode || 'standard';
218
+ document.getElementById('standard-grid')?.classList.toggle('hidden', mode !== 'standard');
219
+ document.getElementById('custom-grid')?.classList.toggle('hidden', mode === 'standard');
220
+ run();
221
+ });
222
+ });
223
+ window.addEventListener('resize', run);
224
+ document.querySelectorAll('input, select').forEach((i) => i.addEventListener('input', run));
225
+ }
226
+
227
+ function init(): void {
228
+ attachListeners();
229
+ run();
230
+ }
231
+
232
+ document.addEventListener('astro:page-load', init);
233
+ init();
234
+ </script>
235
+
236
+ <style>
237
+ .scaler-wrapper {
238
+ padding: 1.5rem 0;
239
+ width: 100%;
240
+ }
241
+
242
+ .scaler-card {
243
+ --sp-p: #4f46e5;
244
+
245
+ background: var(--bg-surface);
246
+ color: var(--text-main);
247
+ width: calc(100% - 24px);
248
+ max-width: 1100px;
249
+ margin: 0 auto;
250
+ border-radius: 24px;
251
+ border: 1px solid var(--border-color);
252
+ display: flex;
253
+ flex-direction: column;
254
+ overflow: hidden;
255
+ }
256
+
257
+ @media (min-width: 1024px) {
258
+ .scaler-card { flex-direction: row; }
259
+ }
260
+
261
+ .card-left {
262
+ flex: 0 0 auto;
263
+ padding: 24px;
264
+ width: 100%;
265
+ background: var(--bg-surface);
266
+ }
267
+
268
+ @media (min-width: 1024px) {
269
+ .card-left {
270
+ width: 380px;
271
+ padding: 40px;
272
+ border-right: 1px solid var(--border-color);
273
+ }
274
+ }
275
+
276
+ .card-right {
277
+ flex: 1 1 auto;
278
+ padding: 24px;
279
+ background: var(--bg-surface);
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: 24px;
283
+ min-width: 0;
284
+ }
285
+
286
+ @media (min-width: 1024px) {
287
+ .card-right {
288
+ padding: 40px;
289
+ gap: 32px;
290
+ }
291
+ }
292
+
293
+ .label-tiny {
294
+ font-size: 10px;
295
+ font-weight: 700;
296
+ text-transform: uppercase;
297
+ letter-spacing: 0.12em;
298
+ color: var(--text-muted);
299
+ margin-bottom: 10px;
300
+ display: block;
301
+ }
302
+
303
+ .segmented-control {
304
+ display: flex;
305
+ background: var(--bg-muted);
306
+ border-radius: 12px;
307
+ padding: 4px;
308
+ margin-bottom: 24px;
309
+ }
310
+
311
+ .segment-btn {
312
+ flex: 1;
313
+ background: transparent;
314
+ border: none;
315
+ padding: 10px 4px;
316
+ font-size: 10px;
317
+ font-weight: 700;
318
+ text-transform: uppercase;
319
+ letter-spacing: 0.05em;
320
+ color: var(--text-muted);
321
+ cursor: pointer;
322
+ border-radius: 8px;
323
+ transition: all 0.2s ease;
324
+ }
325
+
326
+ .segment-btn.active {
327
+ color: var(--text-main);
328
+ background: var(--bg-surface);
329
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
330
+ }
331
+
332
+ .input-grid {
333
+ display: grid;
334
+ grid-template-columns: repeat(2, 1fr);
335
+ gap: 12px;
336
+ margin-bottom: 16px;
337
+ }
338
+
339
+ .form-input {
340
+ width: 100%;
341
+ background: var(--bg-surface);
342
+ border: 1px solid var(--border-color);
343
+ border-radius: 8px;
344
+ padding: 10px 12px;
345
+ font-size: 14px;
346
+ color: var(--text-main);
347
+ }
348
+
349
+ .form-input:focus {
350
+ outline: none;
351
+ border-color: var(--sp-p);
352
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.06);
353
+ }
354
+
355
+ .form-select {
356
+ cursor: pointer;
357
+ appearance: none;
358
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
359
+ background-repeat: no-repeat;
360
+ background-position: right 8px center;
361
+ background-size: 12px;
362
+ padding-right: 28px;
363
+ }
364
+
365
+ .target-lbl {
366
+ margin-top: 16px;
367
+ }
368
+
369
+ .ease-group {
370
+ margin-top: 24px;
371
+ border-top: 1px solid var(--border-color);
372
+ padding-top: 24px;
373
+ }
374
+
375
+ .viz-container {
376
+ background: var(--bg-muted);
377
+ background-image:
378
+ linear-gradient(rgba(0, 0, 0, 0.02) 1px, transparent 1px),
379
+ linear-gradient(90deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px);
380
+ background-size: 20px 20px;
381
+ border-radius: 12px;
382
+ aspect-ratio: 1/1;
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: center;
386
+ position: relative;
387
+ border: 1px solid var(--border-color);
388
+ }
389
+
390
+ @media (min-width: 768px) {
391
+ .viz-container { aspect-ratio: 16/9; }
392
+ }
393
+
394
+ .theme-dark .viz-container {
395
+ background-image:
396
+ linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
397
+ linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
398
+ }
399
+
400
+ #scaler-canvas {
401
+ position: absolute;
402
+ inset: 0;
403
+ width: 100%;
404
+ height: 100%;
405
+ }
406
+
407
+ .legend-box {
408
+ position: absolute;
409
+ bottom: 12px;
410
+ right: 12px;
411
+ display: flex;
412
+ gap: 12px;
413
+ font-size: 9px;
414
+ font-weight: 700;
415
+ text-transform: uppercase;
416
+ letter-spacing: 0.05em;
417
+ color: var(--text-muted);
418
+ }
419
+
420
+ .legend-item {
421
+ display: flex;
422
+ align-items: center;
423
+ gap: 4px;
424
+ }
425
+
426
+ .ln-o {
427
+ width: 10px;
428
+ height: 1px;
429
+ border: 1px dashed var(--border-color);
430
+ }
431
+
432
+ .ln-s {
433
+ width: 10px;
434
+ height: 2px;
435
+ background: var(--sp-p);
436
+ }
437
+
438
+ .action-strip {
439
+ display: flex;
440
+ flex-direction: column;
441
+ gap: 12px;
442
+ }
443
+
444
+ :global(.action-pill) {
445
+ background: var(--bg-surface);
446
+ border: 1px solid var(--border-color);
447
+ border-radius: 16px;
448
+ padding: 16px 20px;
449
+ display: flex;
450
+ align-items: center;
451
+ gap: 16px;
452
+ }
453
+
454
+ :global(.icon-box) {
455
+ width: 44px;
456
+ height: 44px;
457
+ background: var(--bg-muted);
458
+ border-radius: 12px;
459
+ display: flex;
460
+ align-items: center;
461
+ justify-content: center;
462
+ color: var(--sp-p);
463
+ font-size: 20px;
464
+ flex-shrink: 0;
465
+ }
466
+
467
+ :global(.action-content) { flex: 1; }
468
+
469
+ :global(.action-text) {
470
+ display: block;
471
+ font-size: 14px;
472
+ color: var(--text-muted);
473
+ }
474
+
475
+ :global(.action-text strong) { font-weight: 700; }
476
+
477
+ :global(.action-val-pos) { color: var(--sp-p); }
478
+
479
+ :global(.action-val-neg) { color: #f87171; }
480
+
481
+ :global(.action-note) {
482
+ display: block;
483
+ font-size: 10px;
484
+ color: var(--text-muted);
485
+ margin-top: 2px;
486
+ text-transform: uppercase;
487
+ letter-spacing: 0.05em;
488
+ }
489
+
490
+ .totals-section {
491
+ background: var(--bg-muted);
492
+ border-radius: 10px;
493
+ padding: 24px;
494
+ }
495
+
496
+ .totals-grid {
497
+ display: grid;
498
+ grid-template-columns: repeat(2, 1fr);
499
+ gap: 16px;
500
+ }
501
+
502
+ @media (min-width: 640px) {
503
+ .totals-grid { grid-template-columns: repeat(4, 1fr); }
504
+ }
505
+
506
+ .total-card {
507
+ text-align: center;
508
+ display: flex;
509
+ flex-direction: column;
510
+ gap: 4px;
511
+ }
512
+
513
+ .total-val {
514
+ font-size: 16px;
515
+ font-weight: 700;
516
+ color: var(--text-main);
517
+ }
518
+
519
+ .total-tag {
520
+ font-size: 10px;
521
+ font-weight: 700;
522
+ text-transform: uppercase;
523
+ color: var(--text-muted);
524
+ letter-spacing: 0.08em;
525
+ }
526
+
527
+ .warn-banner {
528
+ background: #fefce8;
529
+ border-left: 3px solid #ca8a04;
530
+ color: #854d0e;
531
+ padding: 16px 20px;
532
+ border-radius: 8px;
533
+ font-size: 11px;
534
+ font-weight: 600;
535
+ margin-bottom: 24px;
536
+ display: flex;
537
+ align-items: center;
538
+ gap: 12px;
539
+ }
540
+
541
+ .theme-dark .warn-banner {
542
+ background: #451a03;
543
+ border-left-color: #fbbf24;
544
+ color: #fbbf24;
545
+ }
546
+
547
+ .hidden {
548
+ display: none;
549
+ }
550
+ </style>