@jjlmoya/utils-diy 1.21.0 → 1.22.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 (30) hide show
  1. package/package.json +2 -2
  2. package/src/entries.ts +4 -1
  3. package/src/tests/locale_completeness.test.ts +2 -2
  4. package/src/tests/tool_validation.test.ts +2 -2
  5. package/src/tool/drywallCalculator/bibliography.astro +7 -0
  6. package/src/tool/drywallCalculator/bibliography.ts +16 -0
  7. package/src/tool/drywallCalculator/component.astro +324 -0
  8. package/src/tool/drywallCalculator/drywall-calculator.css +426 -0
  9. package/src/tool/drywallCalculator/entry.ts +24 -0
  10. package/src/tool/drywallCalculator/i18n/de.ts +169 -0
  11. package/src/tool/drywallCalculator/i18n/en.ts +169 -0
  12. package/src/tool/drywallCalculator/i18n/es.ts +169 -0
  13. package/src/tool/drywallCalculator/i18n/fr.ts +169 -0
  14. package/src/tool/drywallCalculator/i18n/id.ts +169 -0
  15. package/src/tool/drywallCalculator/i18n/it.ts +169 -0
  16. package/src/tool/drywallCalculator/i18n/ja.ts +169 -0
  17. package/src/tool/drywallCalculator/i18n/ko.ts +169 -0
  18. package/src/tool/drywallCalculator/i18n/nl.ts +169 -0
  19. package/src/tool/drywallCalculator/i18n/pl.ts +169 -0
  20. package/src/tool/drywallCalculator/i18n/pt.ts +169 -0
  21. package/src/tool/drywallCalculator/i18n/ru.ts +169 -0
  22. package/src/tool/drywallCalculator/i18n/sv.ts +169 -0
  23. package/src/tool/drywallCalculator/i18n/tr.ts +169 -0
  24. package/src/tool/drywallCalculator/i18n/zh.ts +169 -0
  25. package/src/tool/drywallCalculator/index.ts +10 -0
  26. package/src/tool/drywallCalculator/logic.test.ts +39 -0
  27. package/src/tool/drywallCalculator/logic.ts +33 -0
  28. package/src/tool/drywallCalculator/seo.astro +14 -0
  29. package/src/tool/drywallCalculator/ui.ts +52 -0
  30. package/src/tools.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-diy",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -28,7 +28,7 @@
28
28
  "check": "astro check",
29
29
  "type-check": "astro check",
30
30
  "test": "vitest run",
31
- "preversion": "npm run lint && npm run test",
31
+ "preversion": "npm run lint && npm run test && npm run build",
32
32
  "postversion": "git push && git push --tags",
33
33
  "patch": "npm version patch",
34
34
  "minor": "npm version minor",
package/src/entries.ts CHANGED
@@ -17,6 +17,8 @@ export { voltageDropCalculator } from './tool/voltageDropCalculator/entry';
17
17
  export { workshopFractionConverter } from './tool/workshopFractionConverter/entry';
18
18
  export { pythagoreanRightAngleCalculator } from './tool/pythagoreanRightAngleCalculator/entry';
19
19
  export { twoStrokeMixtureCalculator } from './tool/twoStrokeMixtureCalculator/entry';
20
+ export { drywallCalculatorEntry } from './tool/drywallCalculator/entry';
21
+ export type { DrywallCalculatorLocaleContent } from './tool/drywallCalculator/entry';
20
22
  export { diyCategory } from './category';
21
23
  import { balusterCalculator } from './tool/balusterCalculator/entry';
22
24
  import { clayCalculator } from './tool/clayCalculator/entry';
@@ -24,6 +26,7 @@ import { concreteCalculator } from './tool/concreteCalculator/entry';
24
26
  import { cutOptimizer } from './tool/cutOptimizer/entry';
25
27
  import { drillCalculator } from './tool/drillCalculator/entry';
26
28
  import { drillSharpener } from './tool/drillSharpener/entry';
29
+ import { drywallCalculatorEntry } from './tool/drywallCalculator/entry';
27
30
  import { epoxyCalculator } from './tool/epoxyCalculator/entry';
28
31
  import { furnitureFit } from './tool/furnitureFit/entry';
29
32
  import { mortarCalculator } from './tool/mortarCalculator/entry';
@@ -34,4 +37,4 @@ import { voltageDropCalculator } from './tool/voltageDropCalculator/entry';
34
37
  import { workshopFractionConverter } from './tool/workshopFractionConverter/entry';
35
38
  import { pythagoreanRightAngleCalculator } from './tool/pythagoreanRightAngleCalculator/entry';
36
39
  import { twoStrokeMixtureCalculator } from './tool/twoStrokeMixtureCalculator/entry';
37
- export const ALL_ENTRIES = [balusterCalculator, clayCalculator, concreteCalculator, cutOptimizer, drillCalculator, drillSharpener, epoxyCalculator, furnitureFit, mortarCalculator, passepartoutCalculator, stairCalculator, thermalExpansionCalculator, voltageDropCalculator, workshopFractionConverter, pythagoreanRightAngleCalculator, twoStrokeMixtureCalculator];
40
+ export const ALL_ENTRIES = [balusterCalculator, clayCalculator, concreteCalculator, cutOptimizer, drillCalculator, drillSharpener, drywallCalculatorEntry, epoxyCalculator, furnitureFit, mortarCalculator, passepartoutCalculator, stairCalculator, thermalExpansionCalculator, voltageDropCalculator, workshopFractionConverter, pythagoreanRightAngleCalculator, twoStrokeMixtureCalculator];
@@ -33,8 +33,8 @@ describe('Locale Completeness Validation', () => {
33
33
  });
34
34
  });
35
35
 
36
- it('all 16 tools registered', () => {
37
- expect(ALL_TOOLS.length).toBe(16);
36
+ it('all 17 tools registered', () => {
37
+ expect(ALL_TOOLS.length).toBe(17);
38
38
  });
39
39
  });
40
40
 
@@ -4,8 +4,8 @@ import { diyCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 16 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(16);
7
+ it('should have 17 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(17);
9
9
  });
10
10
 
11
11
  it('diyCategory should be defined', () => {
@@ -0,0 +1,7 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { drywallCalculatorBibliography } from './bibliography';
4
+ ---
5
+
6
+ <SharedBibliography links={drywallCalculatorBibliography} />
7
+
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const drywallCalculatorBibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Knauf Drywall Partition Systems Manual & Design Standards',
6
+ url: 'https://knauf.com/api/download-center/v1/assets/72f4ccc1-63bd-4251-b5e0-cb64610991d7?download=true',
7
+ },
8
+ {
9
+ name: 'EN 520: Gypsum Plasterboards - Definitions, Requirements and Test Methods',
10
+ url: 'https://cdn.standards.iteh.ai/samples/33605/9778d6a1a3b042238a64d39451890d1c/SIST-EN-520-2005-A1-2009.pdf',
11
+ },
12
+ {
13
+ name: 'ASTM C840: Standard Specification for Application and Finishing of Gypsum Board',
14
+ url: 'https://store.astm.org/c0840-20.html',
15
+ },
16
+ ];
@@ -0,0 +1,324 @@
1
+ ---
2
+ import './drywall-calculator.css';
3
+ import type { DrywallCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ ui?: Record<string, unknown>;
7
+ }
8
+ const { ui } = Astro.props;
9
+ const t = (ui ?? {}) as DrywallCalculatorUI;
10
+ ---
11
+
12
+ <div class="dw-root" data-dw-root
13
+ data-dw-txt-sp60-metric={t.paramSpacing60Metric}
14
+ data-dw-txt-sp40-metric={t.paramSpacing40Metric}
15
+ data-dw-txt-sp60-imperial={t.paramSpacing60Imperial}
16
+ data-dw-txt-sp40-imperial={t.paramSpacing40Imperial}
17
+ >
18
+ <div class="dw-card-shell">
19
+
20
+ <div class="dw-top-bar">
21
+ <span class="dw-header-title">{t.unitSystemLabel}</span>
22
+ <div class="dw-segmented-control" style="width: 220px;">
23
+ <button type="button" class="dw-chip-btn dw-active" data-dw-unit="metric">{t.unitMetric}</button>
24
+ <button type="button" class="dw-chip-btn" data-dw-unit="imperial">{t.unitImperial}</button>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="dw-card-content">
29
+
30
+ <div class="dw-form-column">
31
+ <div class="dw-field">
32
+ <label class="dw-label" for="dw-length">{t.paramLengthLabel}</label>
33
+ <div class="dw-input-wrap">
34
+ <input type="number" id="dw-length" data-dw-length value="5" step="0.1" min="0.5" />
35
+ <span class="dw-unit-badge" data-dw-unit-len>m</span>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="dw-field">
40
+ <label class="dw-label" for="dw-height">{t.paramHeightLabel}</label>
41
+ <div class="dw-input-wrap">
42
+ <input type="number" id="dw-height" data-dw-height value="2.5" step="0.1" min="0.5" />
43
+ <span class="dw-unit-badge" data-dw-unit-height>m</span>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="dw-field">
48
+ <label class="dw-label">{t.paramSpacingLabel}</label>
49
+ <div class="dw-segmented-control">
50
+ <button type="button" class="dw-chip-btn dw-active" data-dw-spacing="60" data-dw-spacing-btn="60">{t.paramSpacing60Metric}</button>
51
+ <button type="button" class="dw-chip-btn" data-dw-spacing="40" data-dw-spacing-btn="40">{t.paramSpacing40Metric}</button>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="dw-field">
56
+ <label class="dw-label">{t.paramBoardTypeLabel}</label>
57
+ <div class="dw-segmented-control dw-grid-4">
58
+ <button type="button" class="dw-chip-btn dw-active" data-dw-type="standard">{t.boardTypeStandard}</button>
59
+ <button type="button" class="dw-chip-btn" data-dw-type="moisture">{t.boardTypeMoisture}</button>
60
+ <button type="button" class="dw-chip-btn" data-dw-type="fire">{t.boardTypeFire}</button>
61
+ <button type="button" class="dw-chip-btn" data-dw-type="acoustic">{t.boardTypeAcoustic}</button>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="dw-field">
66
+ <label class="dw-label">{t.paramLayersLabel}</label>
67
+ <div class="dw-segmented-control">
68
+ <button type="button" class="dw-chip-btn dw-active" data-dw-layers="1">{t.paramSingleLayer}</button>
69
+ <button type="button" class="dw-chip-btn" data-dw-layers="2">{t.paramDoubleLayer}</button>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="dw-field">
74
+ <label class="dw-label">{t.paramSidesLabel}</label>
75
+ <div class="dw-segmented-control">
76
+ <button type="button" class="dw-chip-btn dw-active" data-dw-sides="double">{t.paramDoubleSided}</button>
77
+ <button type="button" class="dw-chip-btn" data-dw-sides="single">{t.paramSingleSided}</button>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="dw-field">
82
+ <label class="dw-label" for="dw-waste">{t.paramWasteLabel}</label>
83
+ <div class="dw-input-wrap">
84
+ <input type="number" id="dw-waste" data-dw-waste value="10" step="1" min="0" max="30" />
85
+ <span class="dw-unit-badge">%</span>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="dw-view-column">
91
+ <div class="dw-cad-viewport">
92
+ <div class="dw-cad-header">
93
+ <span class="dw-cad-title">{t.diagramWallTitle}</span>
94
+ <div class="dw-segmented-control" style="width: 180px;">
95
+ <button type="button" class="dw-chip-btn dw-active" data-dw-view="board">{t.diagramModeBoard}</button>
96
+ <button type="button" class="dw-chip-btn" data-dw-view="frame">{t.diagramModeStructure}</button>
97
+ </div>
98
+ </div>
99
+
100
+ <div class="dw-cad-canvas">
101
+ <div class="dw-cad-frame">
102
+ <div class="dw-cad-studs-layer" data-dw-studs-visual>
103
+ </div>
104
+ <div class="dw-cad-boards-overlay" data-dw-boards-overlay>
105
+ <div class="dw-cad-board-panel">Panel</div>
106
+ <div class="dw-cad-board-panel">Panel</div>
107
+ <div class="dw-cad-board-panel">Panel</div>
108
+ <div class="dw-cad-board-panel">Panel</div>
109
+ <div class="dw-cad-board-panel">Panel</div>
110
+ <div class="dw-cad-board-panel">Panel</div>
111
+ </div>
112
+ </div>
113
+ <span class="dw-cad-dimension-tag" data-dw-dim-tag>5.0m × 2.5m</span>
114
+ </div>
115
+ </div>
116
+
117
+ <div class="dw-summary-strip">
118
+ <div class="dw-summary-box">
119
+ <span class="lbl">{t.summaryWallArea}</span>
120
+ <span class="val"><span data-dw-res-wall-area>12.50</span> <span data-dw-unit-area>m²</span></span>
121
+ </div>
122
+ <div class="dw-summary-box">
123
+ <span class="lbl">{t.summaryBoardArea}</span>
124
+ <span class="val"><span data-dw-res-board-area>27.50</span> <span data-dw-unit-area>m²</span></span>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="dw-material-dashboard">
129
+ <div class="dw-mat-card">
130
+ <span class="dw-mat-num" data-dw-res-boards>12</span>
131
+ <span class="dw-mat-title">{t.resBoardsLabel}</span>
132
+ <span class="dw-mat-sub">{t.resBoardsUnit}</span>
133
+ </div>
134
+
135
+ <div class="dw-mat-card">
136
+ <span class="dw-mat-num" data-dw-res-studs>11</span>
137
+ <span class="dw-mat-title">{t.resStudsLabel}</span>
138
+ <span class="dw-mat-sub">{t.resStudsUnit}</span>
139
+ </div>
140
+
141
+ <div class="dw-mat-card">
142
+ <span class="dw-mat-num" data-dw-res-tracks>4</span>
143
+ <span class="dw-mat-title">{t.resTracksLabel}</span>
144
+ <span class="dw-mat-sub">{t.resTracksUnit}</span>
145
+ </div>
146
+
147
+ <div class="dw-mat-card">
148
+ <span class="dw-mat-num" data-dw-res-dw-screws>375</span>
149
+ <span class="dw-mat-title">{t.resDrywallScrewsLabel}</span>
150
+ <span class="dw-mat-sub">{t.resDrywallScrewsUnit}</span>
151
+ </div>
152
+
153
+ <div class="dw-mat-card">
154
+ <span class="dw-mat-num" data-dw-res-frame-screws>44</span>
155
+ <span class="dw-mat-title">{t.resFramingScrewsLabel}</span>
156
+ <span class="dw-mat-sub">{t.resFramingScrewsUnit}</span>
157
+ </div>
158
+
159
+ <div class="dw-mat-card">
160
+ <span class="dw-mat-num" data-dw-res-tape>42</span>
161
+ <span class="dw-mat-title">{t.resJointTapeLabel}</span>
162
+ <span class="dw-mat-sub" data-dw-res-tape-unit>m</span>
163
+ </div>
164
+
165
+ <div class="dw-mat-card">
166
+ <span class="dw-mat-num" data-dw-res-compound>22</span>
167
+ <span class="dw-mat-title">{t.resJointCompoundLabel}</span>
168
+ <span class="dw-mat-sub" data-dw-res-compound-unit>kg</span>
169
+ </div>
170
+
171
+ <div class="dw-mat-card">
172
+ <span class="dw-mat-num" data-dw-res-band>15</span>
173
+ <span class="dw-mat-title">{t.resAcousticBandLabel}</span>
174
+ <span class="dw-mat-sub" data-dw-res-band-unit>m</span>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ </div>
180
+
181
+ </div>
182
+ </div>
183
+
184
+ <script>
185
+ import { calculateDrywall } from './logic';
186
+ import type { DrywallInput } from './ui';
187
+
188
+ function bindBtn(root: Element, sel: string, fn: (el: HTMLElement) => void) {
189
+ root.querySelectorAll(sel).forEach((btn) => {
190
+ btn.addEventListener('click', (e) => {
191
+ root.querySelectorAll(sel).forEach((b) => b.classList.remove('dw-active'));
192
+ const target = e.currentTarget as HTMLElement;
193
+ target.classList.add('dw-active');
194
+ fn(target);
195
+ });
196
+ });
197
+ }
198
+
199
+ function getFormValues(root: Element, s: Record<string, unknown>): DrywallInput {
200
+ const len = root.querySelector('[data-dw-length]') as HTMLInputElement;
201
+ const hgt = root.querySelector('[data-dw-height]') as HTMLInputElement;
202
+ const wst = root.querySelector('[data-dw-waste]') as HTMLInputElement;
203
+ return {
204
+ unitSystem: s.unit as 'metric' | 'imperial',
205
+ length: parseFloat(len?.value || '0'),
206
+ height: parseFloat(hgt?.value || '0'),
207
+ studSpacingCm: s.spacing as 40 | 60,
208
+ layers: s.layers as 1 | 2,
209
+ doubleSided: s.doubleSided as boolean,
210
+ boardType: s.type as 'standard' | 'moisture' | 'fire' | 'acoustic',
211
+ wastePercentage: parseFloat(wst?.value || '0'),
212
+ };
213
+ }
214
+
215
+ function updateVisuals(root: Element, count: number, view: string, boardType: string) {
216
+ const studsVis = root.querySelector('[data-dw-studs-visual]');
217
+ const overlay = root.querySelector('[data-dw-boards-overlay]') as HTMLElement | null;
218
+ if (studsVis) {
219
+ studsVis.innerHTML = '';
220
+ const cnt = Math.min(24, Math.max(3, count));
221
+ for (let i = 0; i < cnt; i++) {
222
+ const d = document.createElement('div');
223
+ d.className = 'dw-cad-stud-element';
224
+ studsVis.appendChild(d);
225
+ }
226
+ }
227
+ if (overlay) {
228
+ overlay.style.display = view === 'board' ? 'grid' : 'none';
229
+ const colors: Record<string, string> = { standard: '#fef3c7', moisture: '#dcfce7', fire: '#ffe4e6', acoustic: '#dbeafe' };
230
+ overlay.querySelectorAll('.dw-cad-board-panel').forEach((p) => {
231
+ (p as HTMLElement).style.background = colors[boardType] || '#fef3c7';
232
+ });
233
+ }
234
+ }
235
+
236
+ function getSpacingText(root: Element, key: string, fallback: string): string {
237
+ return root.getAttribute(key) || fallback;
238
+ }
239
+
240
+ function updateSpacingButtons(root: Element, isImp: boolean) {
241
+ const sp60 = root.querySelector('[data-dw-spacing-btn="60"]');
242
+ const sp40 = root.querySelector('[data-dw-spacing-btn="40"]');
243
+ if (sp60) {
244
+ sp60.textContent = getSpacingText(root, isImp ? 'data-dw-txt-sp60-imperial' : 'data-dw-txt-sp60-metric', isImp ? '24 in' : '60 cm');
245
+ }
246
+ if (sp40) {
247
+ sp40.textContent = getSpacingText(root, isImp ? 'data-dw-txt-sp40-imperial' : 'data-dw-txt-sp40-metric', isImp ? '16 in' : '40 cm');
248
+ }
249
+ }
250
+
251
+ function updateUnitsDOM(root: Element, isImp: boolean) {
252
+ updateSpacingButtons(root, isImp);
253
+ root.querySelectorAll('[data-dw-unit-len], [data-dw-unit-height]').forEach((el) => { el.textContent = isImp ? 'ft' : 'm'; });
254
+ root.querySelectorAll('[data-dw-unit-area]').forEach((el) => { el.textContent = isImp ? 'sq ft' : 'm²'; });
255
+ }
256
+
257
+ function renderTakeoff(root: Element, s: Record<string, unknown>) {
258
+ const isImp = s.unit === 'imperial';
259
+ const res = calculateDrywall(getFormValues(root, s));
260
+ const setT = (sel: string, val: string | number) => {
261
+ const el = root.querySelector(sel);
262
+ if (el) el.textContent = String(val);
263
+ };
264
+
265
+ setT('[data-dw-res-wall-area]', res.wallArea.toFixed(2));
266
+ setT('[data-dw-res-board-area]', res.totalBoardArea.toFixed(2));
267
+ setT('[data-dw-res-boards]', res.drywallBoards);
268
+ setT('[data-dw-res-studs]', res.studsCount);
269
+ setT('[data-dw-res-tracks]', res.tracksStandardLengths);
270
+ setT('[data-dw-res-dw-screws]', res.drywallScrews);
271
+ setT('[data-dw-res-frame-screws]', res.framingScrews);
272
+ setT('[data-dw-res-tape]', res.jointTapeLength);
273
+ setT('[data-dw-res-compound]', res.jointCompoundWeight);
274
+ setT('[data-dw-res-band]', res.acousticBandLength);
275
+
276
+ updateUnitsDOM(root, isImp);
277
+
278
+ setT('[data-dw-res-tape-unit]', isImp ? 'ft' : 'm');
279
+ setT('[data-dw-res-compound-unit]', isImp ? 'lb' : 'kg');
280
+ setT('[data-dw-res-band-unit]', isImp ? 'ft' : 'm');
281
+
282
+ updateVisuals(root, res.studsCount, String(s.view), String(s.type));
283
+ }
284
+
285
+ function attachListeners(root: Element, s: Record<string, unknown>) {
286
+ const lenInp = root.querySelector('[data-dw-length]') as HTMLInputElement;
287
+ const hgtInp = root.querySelector('[data-dw-height]') as HTMLInputElement;
288
+ const wstInp = root.querySelector('[data-dw-waste]') as HTMLInputElement;
289
+
290
+ [lenInp, hgtInp, wstInp].forEach((el) => el?.addEventListener('input', () => renderTakeoff(root, s)));
291
+
292
+ bindBtn(root, '[data-dw-unit]', (el) => {
293
+ const prev = s.unit;
294
+ s.unit = el.dataset.dwUnit || 'metric';
295
+ if (prev !== s.unit) {
296
+ const factor = s.unit === 'imperial' ? 3.28084 : 0.3048;
297
+ lenInp.value = (parseFloat(lenInp.value || '5') * factor).toFixed(1);
298
+ hgtInp.value = (parseFloat(hgtInp.value || '2.5') * factor).toFixed(1);
299
+ }
300
+ renderTakeoff(root, s);
301
+ });
302
+
303
+ bindBtn(root, '[data-dw-spacing]', (el) => { s.spacing = parseInt(el.dataset.dwSpacing || '60', 10); renderTakeoff(root, s); });
304
+ bindBtn(root, '[data-dw-type]', (el) => { s.type = el.dataset.dwType || 'standard'; renderTakeoff(root, s); });
305
+ bindBtn(root, '[data-dw-layers]', (el) => { s.layers = parseInt(el.dataset.dwLayers || '1', 10); renderTakeoff(root, s); });
306
+ bindBtn(root, '[data-dw-sides]', (el) => { s.doubleSided = el.dataset.dwSides === 'double'; renderTakeoff(root, s); });
307
+ bindBtn(root, '[data-dw-view]', (el) => { s.view = el.dataset.dwView || 'board'; renderTakeoff(root, s); });
308
+ }
309
+
310
+ function initDrywallCalculator(root: Element) {
311
+ const state: Record<string, unknown> = {
312
+ unit: 'metric',
313
+ spacing: 60,
314
+ layers: 1,
315
+ doubleSided: true,
316
+ type: 'standard',
317
+ view: 'board',
318
+ };
319
+ attachListeners(root, state);
320
+ renderTakeoff(root, state);
321
+ }
322
+
323
+ document.querySelectorAll('[data-dw-root]').forEach(initDrywallCalculator);
324
+ </script>