@jjlmoya/utils-diy 1.11.0 → 1.13.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 (273) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +3 -1
  4. package/src/index.ts +2 -0
  5. package/src/pages/[locale]/[slug].astro +28 -12
  6. package/src/tests/locale_completeness.test.ts +7 -9
  7. package/src/tests/shared-test-helpers.ts +56 -0
  8. package/src/tests/tool_exports.test.ts +34 -0
  9. package/src/tests/tool_validation.test.ts +2 -2
  10. package/src/tool/balusterCalculator/bibliography.astro +2 -10
  11. package/src/tool/balusterCalculator/bibliography.ts +16 -0
  12. package/src/tool/balusterCalculator/i18n/de.ts +2 -15
  13. package/src/tool/balusterCalculator/i18n/en.ts +2 -15
  14. package/src/tool/balusterCalculator/i18n/es.ts +2 -15
  15. package/src/tool/balusterCalculator/i18n/fr.ts +2 -15
  16. package/src/tool/balusterCalculator/i18n/id.ts +2 -15
  17. package/src/tool/balusterCalculator/i18n/it.ts +2 -15
  18. package/src/tool/balusterCalculator/i18n/ja.ts +2 -15
  19. package/src/tool/balusterCalculator/i18n/ko.ts +2 -15
  20. package/src/tool/balusterCalculator/i18n/nl.ts +2 -15
  21. package/src/tool/balusterCalculator/i18n/pl.ts +2 -15
  22. package/src/tool/balusterCalculator/i18n/pt.ts +2 -15
  23. package/src/tool/balusterCalculator/i18n/ru.ts +2 -15
  24. package/src/tool/balusterCalculator/i18n/sv.ts +2 -15
  25. package/src/tool/balusterCalculator/i18n/tr.ts +2 -15
  26. package/src/tool/balusterCalculator/i18n/zh.ts +2 -15
  27. package/src/tool/balusterCalculator/seo.astro +1 -1
  28. package/src/tool/clayCalculator/bibliography.astro +2 -10
  29. package/src/tool/clayCalculator/bibliography.ts +12 -0
  30. package/src/tool/clayCalculator/i18n/de.ts +2 -11
  31. package/src/tool/clayCalculator/i18n/en.ts +2 -11
  32. package/src/tool/clayCalculator/i18n/es.ts +2 -11
  33. package/src/tool/clayCalculator/i18n/fr.ts +2 -11
  34. package/src/tool/clayCalculator/i18n/id.ts +2 -11
  35. package/src/tool/clayCalculator/i18n/it.ts +2 -11
  36. package/src/tool/clayCalculator/i18n/ja.ts +2 -11
  37. package/src/tool/clayCalculator/i18n/ko.ts +2 -11
  38. package/src/tool/clayCalculator/i18n/nl.ts +2 -11
  39. package/src/tool/clayCalculator/i18n/pl.ts +2 -11
  40. package/src/tool/clayCalculator/i18n/pt.ts +2 -11
  41. package/src/tool/clayCalculator/i18n/ru.ts +2 -11
  42. package/src/tool/clayCalculator/i18n/sv.ts +2 -11
  43. package/src/tool/clayCalculator/i18n/tr.ts +2 -11
  44. package/src/tool/clayCalculator/i18n/zh.ts +2 -11
  45. package/src/tool/clayCalculator/seo.astro +1 -1
  46. package/src/tool/concreteCalculator/bibliography.astro +2 -9
  47. package/src/tool/concreteCalculator/bibliography.ts +7 -0
  48. package/src/tool/concreteCalculator/i18n/de.ts +2 -15
  49. package/src/tool/concreteCalculator/i18n/en.ts +2 -15
  50. package/src/tool/concreteCalculator/i18n/es.ts +2 -15
  51. package/src/tool/concreteCalculator/i18n/fr.ts +2 -15
  52. package/src/tool/concreteCalculator/i18n/id.ts +2 -15
  53. package/src/tool/concreteCalculator/i18n/it.ts +2 -15
  54. package/src/tool/concreteCalculator/i18n/ja.ts +2 -15
  55. package/src/tool/concreteCalculator/i18n/ko.ts +2 -15
  56. package/src/tool/concreteCalculator/i18n/nl.ts +2 -15
  57. package/src/tool/concreteCalculator/i18n/pl.ts +2 -15
  58. package/src/tool/concreteCalculator/i18n/pt.ts +2 -15
  59. package/src/tool/concreteCalculator/i18n/ru.ts +2 -15
  60. package/src/tool/concreteCalculator/i18n/sv.ts +2 -15
  61. package/src/tool/concreteCalculator/i18n/tr.ts +2 -15
  62. package/src/tool/concreteCalculator/i18n/zh.ts +2 -15
  63. package/src/tool/concreteCalculator/seo.astro +2 -1
  64. package/src/tool/cutOptimizer/bibliography.astro +2 -9
  65. package/src/tool/cutOptimizer/bibliography.ts +12 -0
  66. package/src/tool/cutOptimizer/i18n/de.ts +2 -11
  67. package/src/tool/cutOptimizer/i18n/en.ts +2 -11
  68. package/src/tool/cutOptimizer/i18n/es.ts +2 -11
  69. package/src/tool/cutOptimizer/i18n/fr.ts +2 -11
  70. package/src/tool/cutOptimizer/i18n/id.ts +2 -11
  71. package/src/tool/cutOptimizer/i18n/it.ts +2 -11
  72. package/src/tool/cutOptimizer/i18n/ja.ts +2 -11
  73. package/src/tool/cutOptimizer/i18n/ko.ts +2 -11
  74. package/src/tool/cutOptimizer/i18n/nl.ts +2 -11
  75. package/src/tool/cutOptimizer/i18n/pl.ts +2 -11
  76. package/src/tool/cutOptimizer/i18n/pt.ts +2 -11
  77. package/src/tool/cutOptimizer/i18n/ru.ts +2 -11
  78. package/src/tool/cutOptimizer/i18n/sv.ts +2 -11
  79. package/src/tool/cutOptimizer/i18n/tr.ts +2 -11
  80. package/src/tool/cutOptimizer/i18n/zh.ts +2 -11
  81. package/src/tool/cutOptimizer/seo.astro +2 -1
  82. package/src/tool/drillCalculator/bibliography.astro +2 -9
  83. package/src/tool/drillCalculator/bibliography.ts +12 -0
  84. package/src/tool/drillCalculator/i18n/de.ts +2 -5
  85. package/src/tool/drillCalculator/i18n/en.ts +2 -5
  86. package/src/tool/drillCalculator/i18n/es.ts +2 -5
  87. package/src/tool/drillCalculator/i18n/fr.ts +2 -5
  88. package/src/tool/drillCalculator/i18n/id.ts +2 -5
  89. package/src/tool/drillCalculator/i18n/it.ts +2 -5
  90. package/src/tool/drillCalculator/i18n/ja.ts +2 -5
  91. package/src/tool/drillCalculator/i18n/ko.ts +2 -5
  92. package/src/tool/drillCalculator/i18n/nl.ts +2 -5
  93. package/src/tool/drillCalculator/i18n/pl.ts +2 -5
  94. package/src/tool/drillCalculator/i18n/pt.ts +2 -5
  95. package/src/tool/drillCalculator/i18n/ru.ts +2 -5
  96. package/src/tool/drillCalculator/i18n/sv.ts +2 -5
  97. package/src/tool/drillCalculator/i18n/tr.ts +2 -5
  98. package/src/tool/drillCalculator/i18n/zh.ts +2 -5
  99. package/src/tool/drillCalculator/seo.astro +2 -1
  100. package/src/tool/drillSharpener/bibliography.astro +2 -9
  101. package/src/tool/drillSharpener/bibliography.ts +16 -0
  102. package/src/tool/drillSharpener/i18n/de.ts +2 -5
  103. package/src/tool/drillSharpener/i18n/en.ts +2 -6
  104. package/src/tool/drillSharpener/i18n/es.ts +2 -6
  105. package/src/tool/drillSharpener/i18n/fr.ts +2 -6
  106. package/src/tool/drillSharpener/i18n/id.ts +2 -5
  107. package/src/tool/drillSharpener/i18n/it.ts +2 -5
  108. package/src/tool/drillSharpener/i18n/ja.ts +2 -5
  109. package/src/tool/drillSharpener/i18n/ko.ts +2 -5
  110. package/src/tool/drillSharpener/i18n/nl.ts +2 -5
  111. package/src/tool/drillSharpener/i18n/pl.ts +2 -5
  112. package/src/tool/drillSharpener/i18n/pt.ts +2 -5
  113. package/src/tool/drillSharpener/i18n/ru.ts +2 -5
  114. package/src/tool/drillSharpener/i18n/sv.ts +2 -5
  115. package/src/tool/drillSharpener/i18n/tr.ts +2 -5
  116. package/src/tool/drillSharpener/i18n/zh.ts +2 -5
  117. package/src/tool/drillSharpener/seo.astro +2 -1
  118. package/src/tool/epoxyCalculator/bibliography.astro +2 -10
  119. package/src/tool/epoxyCalculator/bibliography.ts +20 -0
  120. package/src/tool/epoxyCalculator/i18n/de.ts +2 -5
  121. package/src/tool/epoxyCalculator/i18n/en.ts +2 -19
  122. package/src/tool/epoxyCalculator/i18n/es.ts +2 -19
  123. package/src/tool/epoxyCalculator/i18n/fr.ts +2 -19
  124. package/src/tool/epoxyCalculator/i18n/id.ts +2 -5
  125. package/src/tool/epoxyCalculator/i18n/it.ts +2 -5
  126. package/src/tool/epoxyCalculator/i18n/ja.ts +2 -5
  127. package/src/tool/epoxyCalculator/i18n/ko.ts +2 -5
  128. package/src/tool/epoxyCalculator/i18n/nl.ts +2 -5
  129. package/src/tool/epoxyCalculator/i18n/pl.ts +2 -5
  130. package/src/tool/epoxyCalculator/i18n/pt.ts +2 -5
  131. package/src/tool/epoxyCalculator/i18n/ru.ts +2 -5
  132. package/src/tool/epoxyCalculator/i18n/sv.ts +2 -5
  133. package/src/tool/epoxyCalculator/i18n/tr.ts +2 -5
  134. package/src/tool/epoxyCalculator/i18n/zh.ts +2 -5
  135. package/src/tool/epoxyCalculator/seo.astro +1 -1
  136. package/src/tool/furnitureFit/bibliography.astro +2 -9
  137. package/src/tool/furnitureFit/bibliography.ts +12 -0
  138. package/src/tool/furnitureFit/i18n/de.ts +2 -5
  139. package/src/tool/furnitureFit/i18n/en.ts +2 -5
  140. package/src/tool/furnitureFit/i18n/es.ts +2 -5
  141. package/src/tool/furnitureFit/i18n/fr.ts +2 -5
  142. package/src/tool/furnitureFit/i18n/id.ts +2 -5
  143. package/src/tool/furnitureFit/i18n/it.ts +2 -5
  144. package/src/tool/furnitureFit/i18n/ja.ts +2 -5
  145. package/src/tool/furnitureFit/i18n/ko.ts +2 -5
  146. package/src/tool/furnitureFit/i18n/nl.ts +2 -5
  147. package/src/tool/furnitureFit/i18n/pl.ts +2 -5
  148. package/src/tool/furnitureFit/i18n/pt.ts +2 -5
  149. package/src/tool/furnitureFit/i18n/ru.ts +2 -5
  150. package/src/tool/furnitureFit/i18n/sv.ts +2 -5
  151. package/src/tool/furnitureFit/i18n/tr.ts +2 -5
  152. package/src/tool/furnitureFit/i18n/zh.ts +2 -5
  153. package/src/tool/furnitureFit/seo.astro +2 -1
  154. package/src/tool/mortarCalculator/bibliography.astro +2 -9
  155. package/src/tool/mortarCalculator/bibliography.ts +20 -0
  156. package/src/tool/mortarCalculator/i18n/de.ts +2 -5
  157. package/src/tool/mortarCalculator/i18n/en.ts +2 -19
  158. package/src/tool/mortarCalculator/i18n/es.ts +2 -19
  159. package/src/tool/mortarCalculator/i18n/fr.ts +2 -19
  160. package/src/tool/mortarCalculator/i18n/id.ts +2 -5
  161. package/src/tool/mortarCalculator/i18n/it.ts +2 -5
  162. package/src/tool/mortarCalculator/i18n/ja.ts +2 -5
  163. package/src/tool/mortarCalculator/i18n/ko.ts +2 -5
  164. package/src/tool/mortarCalculator/i18n/nl.ts +2 -5
  165. package/src/tool/mortarCalculator/i18n/pl.ts +2 -5
  166. package/src/tool/mortarCalculator/i18n/pt.ts +2 -5
  167. package/src/tool/mortarCalculator/i18n/ru.ts +2 -5
  168. package/src/tool/mortarCalculator/i18n/sv.ts +2 -5
  169. package/src/tool/mortarCalculator/i18n/tr.ts +2 -5
  170. package/src/tool/mortarCalculator/i18n/zh.ts +2 -5
  171. package/src/tool/mortarCalculator/seo.astro +2 -1
  172. package/src/tool/passepartoutCalculator/bibliography.astro +2 -9
  173. package/src/tool/passepartoutCalculator/bibliography.ts +16 -0
  174. package/src/tool/passepartoutCalculator/i18n/de.ts +2 -6
  175. package/src/tool/passepartoutCalculator/i18n/en.ts +2 -15
  176. package/src/tool/passepartoutCalculator/i18n/es.ts +2 -15
  177. package/src/tool/passepartoutCalculator/i18n/fr.ts +2 -15
  178. package/src/tool/passepartoutCalculator/i18n/id.ts +2 -6
  179. package/src/tool/passepartoutCalculator/i18n/it.ts +2 -6
  180. package/src/tool/passepartoutCalculator/i18n/ja.ts +2 -6
  181. package/src/tool/passepartoutCalculator/i18n/ko.ts +2 -6
  182. package/src/tool/passepartoutCalculator/i18n/nl.ts +2 -6
  183. package/src/tool/passepartoutCalculator/i18n/pl.ts +2 -6
  184. package/src/tool/passepartoutCalculator/i18n/pt.ts +2 -6
  185. package/src/tool/passepartoutCalculator/i18n/ru.ts +2 -6
  186. package/src/tool/passepartoutCalculator/i18n/sv.ts +2 -6
  187. package/src/tool/passepartoutCalculator/i18n/tr.ts +2 -6
  188. package/src/tool/passepartoutCalculator/i18n/zh.ts +2 -6
  189. package/src/tool/passepartoutCalculator/seo.astro +2 -1
  190. package/src/tool/stairCalculator/bibliography.astro +2 -9
  191. package/src/tool/stairCalculator/bibliography.ts +16 -0
  192. package/src/tool/stairCalculator/i18n/de.ts +2 -5
  193. package/src/tool/stairCalculator/i18n/en.ts +2 -6
  194. package/src/tool/stairCalculator/i18n/es.ts +2 -6
  195. package/src/tool/stairCalculator/i18n/fr.ts +2 -6
  196. package/src/tool/stairCalculator/i18n/id.ts +2 -5
  197. package/src/tool/stairCalculator/i18n/it.ts +2 -5
  198. package/src/tool/stairCalculator/i18n/ja.ts +2 -5
  199. package/src/tool/stairCalculator/i18n/ko.ts +2 -5
  200. package/src/tool/stairCalculator/i18n/nl.ts +2 -5
  201. package/src/tool/stairCalculator/i18n/pl.ts +2 -5
  202. package/src/tool/stairCalculator/i18n/pt.ts +2 -5
  203. package/src/tool/stairCalculator/i18n/ru.ts +2 -5
  204. package/src/tool/stairCalculator/i18n/sv.ts +2 -5
  205. package/src/tool/stairCalculator/i18n/tr.ts +2 -5
  206. package/src/tool/stairCalculator/i18n/zh.ts +2 -5
  207. package/src/tool/stairCalculator/seo.astro +2 -1
  208. package/src/tool/thermalExpansionCalculator/bibliography.astro +2 -9
  209. package/src/tool/thermalExpansionCalculator/bibliography.ts +16 -0
  210. package/src/tool/thermalExpansionCalculator/i18n/de.ts +2 -6
  211. package/src/tool/thermalExpansionCalculator/i18n/en.ts +2 -6
  212. package/src/tool/thermalExpansionCalculator/i18n/es.ts +2 -6
  213. package/src/tool/thermalExpansionCalculator/i18n/fr.ts +2 -6
  214. package/src/tool/thermalExpansionCalculator/i18n/id.ts +2 -6
  215. package/src/tool/thermalExpansionCalculator/i18n/it.ts +2 -6
  216. package/src/tool/thermalExpansionCalculator/i18n/ja.ts +2 -6
  217. package/src/tool/thermalExpansionCalculator/i18n/ko.ts +2 -6
  218. package/src/tool/thermalExpansionCalculator/i18n/nl.ts +2 -6
  219. package/src/tool/thermalExpansionCalculator/i18n/pl.ts +2 -6
  220. package/src/tool/thermalExpansionCalculator/i18n/pt.ts +2 -6
  221. package/src/tool/thermalExpansionCalculator/i18n/ru.ts +2 -6
  222. package/src/tool/thermalExpansionCalculator/i18n/sv.ts +2 -6
  223. package/src/tool/thermalExpansionCalculator/i18n/tr.ts +2 -6
  224. package/src/tool/thermalExpansionCalculator/i18n/zh.ts +2 -6
  225. package/src/tool/thermalExpansionCalculator/seo.astro +2 -1
  226. package/src/tool/voltageDropCalculator/bibliography.astro +2 -9
  227. package/src/tool/voltageDropCalculator/bibliography.ts +16 -0
  228. package/src/tool/voltageDropCalculator/i18n/de.ts +2 -5
  229. package/src/tool/voltageDropCalculator/i18n/en.ts +2 -6
  230. package/src/tool/voltageDropCalculator/i18n/es.ts +2 -6
  231. package/src/tool/voltageDropCalculator/i18n/fr.ts +2 -6
  232. package/src/tool/voltageDropCalculator/i18n/id.ts +2 -5
  233. package/src/tool/voltageDropCalculator/i18n/it.ts +2 -5
  234. package/src/tool/voltageDropCalculator/i18n/ja.ts +2 -5
  235. package/src/tool/voltageDropCalculator/i18n/ko.ts +2 -5
  236. package/src/tool/voltageDropCalculator/i18n/nl.ts +2 -5
  237. package/src/tool/voltageDropCalculator/i18n/pl.ts +2 -5
  238. package/src/tool/voltageDropCalculator/i18n/pt.ts +2 -5
  239. package/src/tool/voltageDropCalculator/i18n/ru.ts +2 -5
  240. package/src/tool/voltageDropCalculator/i18n/sv.ts +2 -5
  241. package/src/tool/voltageDropCalculator/i18n/tr.ts +2 -5
  242. package/src/tool/voltageDropCalculator/i18n/zh.ts +2 -5
  243. package/src/tool/voltageDropCalculator/seo.astro +2 -1
  244. package/src/tool/workshopFractionConverter/bibliography.astro +6 -0
  245. package/src/tool/workshopFractionConverter/bibliography.ts +12 -0
  246. package/src/tool/workshopFractionConverter/client.ts +183 -0
  247. package/src/tool/workshopFractionConverter/component.astro +226 -0
  248. package/src/tool/workshopFractionConverter/converter.ts +110 -0
  249. package/src/tool/workshopFractionConverter/engine.ts +223 -0
  250. package/src/tool/workshopFractionConverter/entry.ts +24 -0
  251. package/src/tool/workshopFractionConverter/i18n/de.ts +147 -0
  252. package/src/tool/workshopFractionConverter/i18n/en.ts +147 -0
  253. package/src/tool/workshopFractionConverter/i18n/es.ts +147 -0
  254. package/src/tool/workshopFractionConverter/i18n/fr.ts +147 -0
  255. package/src/tool/workshopFractionConverter/i18n/id.ts +147 -0
  256. package/src/tool/workshopFractionConverter/i18n/it.ts +147 -0
  257. package/src/tool/workshopFractionConverter/i18n/ja.ts +147 -0
  258. package/src/tool/workshopFractionConverter/i18n/ko.ts +147 -0
  259. package/src/tool/workshopFractionConverter/i18n/nl.ts +147 -0
  260. package/src/tool/workshopFractionConverter/i18n/pl.ts +147 -0
  261. package/src/tool/workshopFractionConverter/i18n/pt.ts +147 -0
  262. package/src/tool/workshopFractionConverter/i18n/ru.ts +147 -0
  263. package/src/tool/workshopFractionConverter/i18n/sv.ts +147 -0
  264. package/src/tool/workshopFractionConverter/i18n/tr.ts +147 -0
  265. package/src/tool/workshopFractionConverter/i18n/zh.ts +147 -0
  266. package/src/tool/workshopFractionConverter/index.ts +11 -0
  267. package/src/tool/workshopFractionConverter/seo.astro +15 -0
  268. package/src/tool/workshopFractionConverter/types-client.ts +89 -0
  269. package/src/tool/workshopFractionConverter/ui-updater.ts +94 -0
  270. package/src/tool/workshopFractionConverter/ui.ts +38 -0
  271. package/src/tool/workshopFractionConverter/workshop-fraction-converter.css +563 -0
  272. package/src/tools.ts +2 -1
  273. package/src/types.ts +1 -1
@@ -0,0 +1,183 @@
1
+ import type { Fraction, HistoryEntry } from './types-client';
2
+ import { convertFraction, reverseSearch } from './converter';
3
+ import { displayResult, displayReverseResult } from './ui-updater';
4
+
5
+ export class WorkshopFractionConverter {
6
+ history: HistoryEntry[] = [];
7
+ currentMode = 'forward';
8
+ currentPrecision = 'carpentry';
9
+ ui: Record<string, string> = {};
10
+ constructor(ui?: Record<string, string>) {
11
+ if (ui) this.ui = ui;
12
+ this.setupEventListeners();
13
+ this.loadHistory();
14
+ }
15
+ setupEventListeners() {
16
+ document.querySelectorAll('#wfc-mode-toggle .wfc-mode-btn').forEach((btn) => {
17
+ btn.addEventListener('click', (e: Event) => this.handleModeToggle(e));
18
+ });
19
+ document.querySelectorAll('#wfc-precision-toggle .wfc-mode-btn').forEach((btn) => {
20
+ btn.addEventListener('click', (e: Event) => this.handlePrecisionToggle(e));
21
+ });
22
+ document.querySelectorAll('.wfc-stepper-btn').forEach((btn) => {
23
+ btn.addEventListener('click', (e: Event) => this.handleStepper(e));
24
+ });
25
+ document.querySelectorAll('.wfc-preset-btn').forEach((btn) => {
26
+ btn.addEventListener('click', (e: Event) => this.handlePreset(e));
27
+ });
28
+ this.setupActionButtons();
29
+ this.setupInputListeners();
30
+ }
31
+ setupActionButtons() {
32
+ document.getElementById('wfc-convert-btn')?.addEventListener('click', () => this.convert());
33
+ document.getElementById('wfc-clear-btn')?.addEventListener('click', () => this.clear());
34
+ document.getElementById('wfc-copy-btn')?.addEventListener('click', () => this.copyResult());
35
+ document.getElementById('wfc-save-btn')?.addEventListener('click', () => this.saveToHistory());
36
+ }
37
+ setupInputListeners() {
38
+ document.getElementById('wfc-whole')?.addEventListener('change', () => this.convert());
39
+ document.getElementById('wfc-numerator')?.addEventListener('change', () => this.convert());
40
+ document.getElementById('wfc-denominator')?.addEventListener('change', () => this.convert());
41
+ document.getElementById('wfc-reverse-mm')?.addEventListener('change', () => this.convert());
42
+ }
43
+ handleModeToggle(e: Event) {
44
+ const target = e.target as HTMLElement;
45
+ document.querySelectorAll('#wfc-mode-toggle .wfc-mode-btn').forEach((b) => b.classList.remove('active'));
46
+ target.classList.add('active');
47
+ this.currentMode = target.dataset.mode || 'forward';
48
+ this.toggleMode(this.currentMode);
49
+ }
50
+ handlePrecisionToggle(e: Event) {
51
+ const target = e.target as HTMLElement;
52
+ document.querySelectorAll('#wfc-precision-toggle .wfc-mode-btn').forEach((b) => b.classList.remove('active'));
53
+ target.classList.add('active');
54
+ this.currentPrecision = target.dataset.precision || 'carpentry';
55
+ }
56
+ handleStepper(e: Event) {
57
+ const target = e.target as HTMLElement;
58
+ const stepper = target.dataset.stepper || '';
59
+ const [field, direction] = stepper.split('-');
60
+ let inputId = '';
61
+ if (field === 'whole') inputId = 'wfc-whole';
62
+ else if (field === 'num') inputId = 'wfc-numerator';
63
+ else if (field === 'denom') inputId = 'wfc-denominator';
64
+ const input = document.getElementById(inputId) as HTMLInputElement | null;
65
+ if (!input) return;
66
+ const value = parseInt(input.value, 10) || 0;
67
+ const newValue = direction === 'up' ? value + 1 : Math.max(0, value - 1);
68
+ input.value = String(newValue);
69
+ this.convert();
70
+ }
71
+ handlePreset(e: Event) {
72
+ const target = e.target as HTMLElement;
73
+ const preset = target.dataset.preset || '';
74
+ const [num, denom] = preset.split('/').map((x) => parseInt(x, 10));
75
+ const wholeInput = document.getElementById('wfc-whole') as HTMLInputElement | null;
76
+ const numInput = document.getElementById('wfc-numerator') as HTMLInputElement | null;
77
+ const denomInput = document.getElementById('wfc-denominator') as HTMLSelectElement | null;
78
+ if (wholeInput) wholeInput.value = '0';
79
+ if (numInput) numInput.value = String(num);
80
+ if (denomInput) denomInput.value = String(denom);
81
+ document.querySelectorAll('.wfc-preset-btn').forEach((btn) => btn.classList.remove('active'));
82
+ target.classList.add('active');
83
+ this.convert();
84
+ }
85
+ toggleMode(mode: string) {
86
+ const forwardInput = document.getElementById('wfc-forward-input');
87
+ const reverseInput = document.getElementById('wfc-reverse-input');
88
+ if (mode === 'forward') {
89
+ forwardInput?.classList.remove('wfc-hidden');
90
+ reverseInput?.classList.add('wfc-hidden');
91
+ } else {
92
+ forwardInput?.classList.add('wfc-hidden');
93
+ reverseInput?.classList.remove('wfc-hidden');
94
+ }
95
+ }
96
+ convert() {
97
+ if (this.currentMode === 'forward') {
98
+ this.convertForward();
99
+ } else {
100
+ this.convertReverse();
101
+ }
102
+ }
103
+ convertForward() {
104
+ const whole = parseInt((document.getElementById('wfc-whole') as HTMLInputElement).value, 10) || 0;
105
+ const numerator = parseInt((document.getElementById('wfc-numerator') as HTMLInputElement).value, 10) || 0;
106
+ const denominator = parseInt((document.getElementById('wfc-denominator') as HTMLSelectElement).value, 10) || 1;
107
+ const input: Fraction = { whole, numerator, denominator };
108
+ displayResult(convertFraction(input), this.currentPrecision, this.ui);
109
+ }
110
+ convertReverse() {
111
+ const mm = parseFloat((document.getElementById('wfc-reverse-mm') as HTMLInputElement).value) || 0;
112
+ if (mm === 0) {
113
+ document.getElementById('wfc-no-result')?.classList.remove('wfc-hidden');
114
+ document.getElementById('wfc-result-content')?.classList.add('wfc-hidden');
115
+ return;
116
+ }
117
+ displayReverseResult(reverseSearch(mm), this.currentPrecision, this.ui);
118
+ }
119
+ copyResult() {
120
+ const fractionEl = document.getElementById('wfc-result-fraction');
121
+ const mmEl = document.getElementById('wfc-result-mm');
122
+ const fraction = fractionEl?.textContent || '';
123
+ const mm = mmEl?.textContent || '';
124
+ const text = `${fraction} = ${mm}`;
125
+ navigator.clipboard.writeText(text);
126
+ }
127
+ saveToHistory() {
128
+ const fractionEl = document.getElementById('wfc-result-fraction');
129
+ const mmEl = document.getElementById('wfc-result-mm');
130
+ const fraction = fractionEl?.textContent || '';
131
+ const mm = mmEl?.textContent || '';
132
+ const entry = { fraction, mm, time: Date.now() };
133
+ this.history.unshift(entry);
134
+ if (this.history.length > 20) this.history.pop();
135
+ this.saveHistoryLocal();
136
+ this.renderHistory();
137
+ }
138
+ loadHistory() {
139
+ const stored = localStorage.getItem('wfc-history');
140
+ if (stored) {
141
+ this.history = JSON.parse(stored);
142
+ this.renderHistory();
143
+ }
144
+ }
145
+ saveHistoryLocal() {
146
+ localStorage.setItem('wfc-history', JSON.stringify(this.history));
147
+ }
148
+ renderHistory() {
149
+ const list = document.getElementById('wfc-history-list');
150
+ if (!list) return;
151
+ if (this.history.length === 0) {
152
+ list.innerHTML = `<div style="text-align: center; color: #9ca3af; font-size: 0.85rem;">${this.ui.historyEmpty || 'No measurements yet'}</div>`;
153
+ return;
154
+ }
155
+ list.innerHTML = this.buildHistoryHTML();
156
+ this.attachHistoryHandlers(list);
157
+ }
158
+ buildHistoryHTML() {
159
+ return this.history
160
+ .map((item, idx) => `<div class="wfc-history-item"><span><span class="wfc-history-item-fraction">${item.fraction}</span> = ${item.mm}</span><button class="wfc-history-remove" data-idx="${idx}">${this.ui.historyRemove || 'Remove'}</button></div>`)
161
+ .join('');
162
+ }
163
+ attachHistoryHandlers(list: HTMLElement) {
164
+ list.querySelectorAll('.wfc-history-remove').forEach((btn) => {
165
+ btn.addEventListener('click', (e: Event) => {
166
+ const target = e.target as HTMLElement;
167
+ const idx = parseInt(target.dataset.idx || '0', 10);
168
+ this.history.splice(idx, 1);
169
+ this.saveHistoryLocal();
170
+ this.renderHistory();
171
+ });
172
+ });
173
+ }
174
+ clear() {
175
+ (document.getElementById('wfc-whole') as HTMLInputElement).value = '0';
176
+ (document.getElementById('wfc-numerator') as HTMLInputElement).value = '0';
177
+ (document.getElementById('wfc-denominator') as HTMLSelectElement).value = '1';
178
+ (document.getElementById('wfc-reverse-mm') as HTMLInputElement).value = '';
179
+ document.getElementById('wfc-no-result')?.classList.remove('wfc-hidden');
180
+ document.getElementById('wfc-result-content')?.classList.add('wfc-hidden');
181
+ document.querySelectorAll('.wfc-preset-btn').forEach((btn) => btn.classList.remove('active'));
182
+ }
183
+ }
@@ -0,0 +1,226 @@
1
+ ---
2
+ import type { WorkshopFractionConverterUI } from './ui';
3
+
4
+ interface Props { ui?: WorkshopFractionConverterUI; }
5
+ const { ui } = Astro.props;
6
+ const t = (ui ?? {}) as WorkshopFractionConverterUI;
7
+ ---
8
+
9
+ <div class="wfc-root" data-wfc-root data-ui={JSON.stringify(t)}>
10
+ <div class="wfc-card">
11
+ <div class="wfc-grid">
12
+ <div class="wfc-sidebar">
13
+ <div class="wfc-field">
14
+ <label class="wfc-field-label">{t.labelFraction}</label>
15
+ <div class="wfc-mode-toggle" id="wfc-mode-toggle">
16
+ <button class="wfc-mode-btn active" data-mode="forward">
17
+ {t.modeForward}
18
+ </button>
19
+ <button class="wfc-mode-btn" data-mode="reverse">
20
+ {t.modeReverse}
21
+ </button>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="wfc-field" id="wfc-forward-input" data-forward-input>
26
+ <label class="wfc-field-label">{t.labelFraction}</label>
27
+ <div class="wfc-fraction-input">
28
+ <div class="wfc-fraction-column">
29
+ <span class="wfc-fraction-label">{t.labelWhole}</span>
30
+ <div class="wfc-stepper-group">
31
+ <button class="wfc-stepper-btn" data-stepper="whole-down">−</button>
32
+ <button class="wfc-stepper-btn" data-stepper="whole-up">+</button>
33
+ </div>
34
+ <input
35
+ type="number"
36
+ class="wfc-input-number"
37
+ id="wfc-whole"
38
+ value="0"
39
+ min="0"
40
+ max="999"
41
+ inputmode="numeric"
42
+ />
43
+ </div>
44
+ <div class="wfc-fraction-column">
45
+ <span class="wfc-fraction-label">{t.labelNumerator}</span>
46
+ <div class="wfc-stepper-group">
47
+ <button class="wfc-stepper-btn" data-stepper="num-down">−</button>
48
+ <button class="wfc-stepper-btn" data-stepper="num-up">+</button>
49
+ </div>
50
+ <input
51
+ type="number"
52
+ class="wfc-input-number"
53
+ id="wfc-numerator"
54
+ value="0"
55
+ min="0"
56
+ max="64"
57
+ inputmode="numeric"
58
+ />
59
+ </div>
60
+ <div class="wfc-fraction-column">
61
+ <span class="wfc-fraction-label">{t.labelDenominator}</span>
62
+ <div class="wfc-stepper-group">
63
+ <button class="wfc-stepper-btn" data-stepper="denom-down">−</button>
64
+ <button class="wfc-stepper-btn" data-stepper="denom-up">+</button>
65
+ </div>
66
+ <select class="wfc-input-number" id="wfc-denominator" style="appearance: none;">
67
+ <option value="1">1</option>
68
+ <option value="2">2</option>
69
+ <option value="4">4</option>
70
+ <option value="8">8</option>
71
+ <option value="16">16</option>
72
+ <option value="32">32</option>
73
+ <option value="64">64</option>
74
+ </select>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="wfc-field">
79
+ <label class="wfc-field-label">{t.labelFraction}</label>
80
+ <div class="wfc-presets" id="wfc-presets">
81
+ <button class="wfc-preset-btn" data-preset="1/4">1/4"</button>
82
+ <button class="wfc-preset-btn" data-preset="1/2">1/2"</button>
83
+ <button class="wfc-preset-btn" data-preset="3/8">3/8"</button>
84
+ <button class="wfc-preset-btn" data-preset="5/16">5/16"</button>
85
+ <button class="wfc-preset-btn" data-preset="3/16">3/16"</button>
86
+ <button class="wfc-preset-btn" data-preset="1/8">1/8"</button>
87
+ <button class="wfc-preset-btn" data-preset="1/16">1/16"</button>
88
+ <button class="wfc-preset-btn" data-preset="5/8">5/8"</button>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <div class="wfc-field wfc-hidden" id="wfc-reverse-input" data-reverse-input>
94
+ <label class="wfc-field-label">{t.reverseSearchLabel}</label>
95
+ <div style="display: flex; gap: 0.5rem;">
96
+ <input
97
+ type="number"
98
+ class="wfc-input-number"
99
+ id="wfc-reverse-mm"
100
+ placeholder="12.7"
101
+ min="0.1"
102
+ max="999"
103
+ step="0.1"
104
+ inputmode="decimal"
105
+ style="flex: 1;"
106
+ />
107
+ <span style="align-self: flex-end; padding-bottom: 0.75rem; color: #6b7280;">mm</span>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="wfc-field">
112
+ <label class="wfc-field-label">{t.precisionLabel}</label>
113
+ <div class="wfc-mode-toggle" id="wfc-precision-toggle">
114
+ <button class="wfc-mode-btn active" data-precision="carpentry">
115
+ {t.precisionCarpentry}
116
+ </button>
117
+ <button class="wfc-mode-btn" data-precision="metal">
118
+ {t.precisionMetal}
119
+ </button>
120
+ </div>
121
+ </div>
122
+
123
+ <div class="wfc-button-group">
124
+ <button class="wfc-button wfc-button-primary" id="wfc-convert-btn">
125
+ {t.buttonConvert}
126
+ </button>
127
+ <button class="wfc-button wfc-button-secondary" id="wfc-clear-btn">
128
+ {t.buttonClear}
129
+ </button>
130
+ </div>
131
+
132
+ <div class="wfc-history" id="wfc-history">
133
+ <div class="wfc-history-title">{t.historyTitle}</div>
134
+ <div class="wfc-history-list" id="wfc-history-list">
135
+ <div style="text-align: center; color: #9ca3af; font-size: 0.85rem;">
136
+ {t.historyEmpty}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="wfc-main" id="wfc-main-content">
143
+ <div id="wfc-no-result" style="text-align: center; padding: 3rem 0; color: #9ca3af;">
144
+ <p>{t.helpText}</p>
145
+ </div>
146
+
147
+ <div class="wfc-hidden" id="wfc-result-content">
148
+ <div class="wfc-result-box">
149
+ <div class="wfc-result-title">{t.resultTitle}</div>
150
+ <div class="wfc-result-row">
151
+ <span class="wfc-result-label">{t.resultFraction}</span>
152
+ <span class="wfc-result-value wfc-text-mono" id="wfc-result-fraction">
153
+ 0"
154
+ </span>
155
+ </div>
156
+ <div class="wfc-result-row">
157
+ <span class="wfc-result-label">{t.resultInches}</span>
158
+ <span class="wfc-result-value wfc-text-mono" id="wfc-result-inches">
159
+ 0.00"
160
+ </span>
161
+ </div>
162
+ <div class="wfc-result-row">
163
+ <span class="wfc-result-label">{t.resultMillimeters}</span>
164
+ <span class="wfc-result-value wfc-text-mono" id="wfc-result-mm">
165
+ 0.00 mm
166
+ </span>
167
+ </div>
168
+ </div>
169
+
170
+ <div class="wfc-visual-scale">
171
+ <div style="font-size: 0.75rem; font-weight: 600; color: #6b7280; margin-bottom: 0.75rem;">
172
+ {t.labelVisualReference}
173
+ </div>
174
+ <div class="wfc-scale-ruler" id="wfc-scale-ruler">
175
+ <div class="wfc-scale-indicator" id="wfc-scale-indicator" style="width: 0;"></div>
176
+ </div>
177
+ <div style="font-size: 0.75rem; color: #6b7280;">
178
+ <span id="wfc-scale-text">0 mm</span>
179
+ </div>
180
+ </div>
181
+
182
+ <div>
183
+ <div class="wfc-result-title">{t.matchTitle}</div>
184
+ <div class="wfc-match-cards">
185
+ <div class="wfc-match-card" id="wfc-fastener-match">
186
+ <div class="wfc-match-header">
187
+ <span class="wfc-match-name" id="wfc-fastener-name">−</span>
188
+ <span class="wfc-match-badge" id="wfc-fastener-badge">−</span>
189
+ </div>
190
+ <div class="wfc-match-details" id="wfc-fastener-details">
191
+ {t.matchClosestFastener}
192
+ </div>
193
+ </div>
194
+
195
+ <div class="wfc-match-card" id="wfc-socket-match">
196
+ <div class="wfc-match-header">
197
+ <span class="wfc-match-name" id="wfc-socket-name">−</span>
198
+ <span class="wfc-match-badge" id="wfc-socket-badge">−</span>
199
+ </div>
200
+ <div class="wfc-match-details" id="wfc-socket-details">
201
+ {t.matchClosestSocket}
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+
207
+ <div class="wfc-button-group">
208
+ <button class="wfc-button wfc-button-primary" id="wfc-copy-btn">
209
+ {t.buttonCopyResult}
210
+ </button>
211
+ <button class="wfc-button wfc-button-secondary" id="wfc-save-btn">
212
+ {t.buttonSaveToHistory}
213
+ </button>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+
221
+ <script>
222
+ import { WorkshopFractionConverter } from './client';
223
+ const root = document.querySelector('[data-wfc-root]');
224
+ const ui = JSON.parse(root?.getAttribute('data-ui') || '{}');
225
+ new WorkshopFractionConverter(ui);
226
+ </script>
@@ -0,0 +1,110 @@
1
+ import type { Fraction, ConversionResult, ReverseSearchResult } from './types-client';
2
+ import { STANDARD_FASTENERS, STANDARD_SOCKETS } from './types-client';
3
+
4
+ export function fractionToInches(input: Fraction) {
5
+ if (input.denominator === 0) return 0;
6
+ return input.whole + input.numerator / input.denominator;
7
+ }
8
+
9
+ export function inchesToMillimeters(inches: number) {
10
+ return inches * 25.4;
11
+ }
12
+
13
+ export function formatFraction(input: Fraction) {
14
+ if (input.denominator === 0 || (input.numerator === 0 && input.denominator === 1)) {
15
+ return `${input.whole}"`;
16
+ }
17
+ if (input.whole === 0) {
18
+ return `${input.numerator}/${input.denominator}"`;
19
+ }
20
+ return `${input.whole} ${input.numerator}/${input.denominator}"`;
21
+ }
22
+
23
+ export function findClosestFastener(mm: number) {
24
+ if (STANDARD_FASTENERS.length === 0) return null;
25
+ let closest = STANDARD_FASTENERS[0];
26
+ let minDiff = Math.abs(closest.mm - mm);
27
+ for (const fastener of STANDARD_FASTENERS) {
28
+ const diff = Math.abs(fastener.mm - mm);
29
+ if (diff < minDiff) {
30
+ minDiff = diff;
31
+ closest = fastener;
32
+ }
33
+ }
34
+ const pct = (minDiff / mm) * 100;
35
+ return {
36
+ name: closest.name,
37
+ size_mm: closest.mm,
38
+ size_inch: closest.inch,
39
+ difference_mm: closest.mm - mm,
40
+ difference_pct: pct,
41
+ };
42
+ }
43
+
44
+ export function findClosestSocket(mm: number) {
45
+ if (STANDARD_SOCKETS.length === 0) return null;
46
+ let closest = STANDARD_SOCKETS[0];
47
+ let minDiff = Math.abs(closest.mm - mm);
48
+ for (const socket of STANDARD_SOCKETS) {
49
+ const diff = Math.abs(socket.mm - mm);
50
+ if (diff < minDiff) {
51
+ minDiff = diff;
52
+ closest = socket;
53
+ }
54
+ }
55
+ const pct = (minDiff / mm) * 100;
56
+ return {
57
+ name: closest.name,
58
+ size_mm: closest.mm,
59
+ size_inch: closest.inch,
60
+ difference_mm: closest.mm - mm,
61
+ difference_pct: pct,
62
+ };
63
+ }
64
+
65
+ export function mmToFraction(mm: number): Fraction {
66
+ const inches = mm / 25.4;
67
+ const whole = Math.floor(inches);
68
+ const fractional = inches - whole;
69
+ const denominators = [2, 4, 8, 16, 32, 64];
70
+ let bestNum = 0;
71
+ let bestDenom = 1;
72
+ let bestError = Math.abs(fractional);
73
+ for (const denom of denominators) {
74
+ const num = Math.round(fractional * denom);
75
+ const error = Math.abs(fractional - num / denom);
76
+ if (error < bestError) {
77
+ bestError = error;
78
+ bestNum = num;
79
+ bestDenom = denom;
80
+ }
81
+ }
82
+ if (bestNum === bestDenom) {
83
+ return { whole: whole + 1, numerator: 0, denominator: 1 };
84
+ }
85
+ return { whole, numerator: bestNum, denominator: bestDenom };
86
+ }
87
+
88
+ export function convertFraction(input: Fraction): ConversionResult {
89
+ const inches = fractionToInches(input);
90
+ const mm = inchesToMillimeters(inches);
91
+ return {
92
+ fraction: input,
93
+ inches,
94
+ millimeters: mm,
95
+ closestFastener: findClosestFastener(mm),
96
+ closestSocket: findClosestSocket(mm),
97
+ };
98
+ }
99
+
100
+ export function reverseSearch(millimeters: number): ReverseSearchResult {
101
+ const inches = millimeters / 25.4;
102
+ const fraction = mmToFraction(millimeters);
103
+ return {
104
+ input_mm: millimeters,
105
+ closest_fraction: fraction,
106
+ inches,
107
+ closestFastener: findClosestFastener(millimeters),
108
+ closestSocket: findClosestSocket(millimeters),
109
+ };
110
+ }