@jjlmoya/utils-home 1.30.0 → 1.32.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 (56) hide show
  1. package/package.json +2 -1
  2. package/src/entries.ts +7 -1
  3. package/src/index.ts +1 -0
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/lightingCalculator/bibliography.astro +36 -0
  7. package/src/tool/lightingCalculator/bibliography.ts +10 -0
  8. package/src/tool/lightingCalculator/component.astro +308 -0
  9. package/src/tool/lightingCalculator/draw.ts +247 -0
  10. package/src/tool/lightingCalculator/entry.ts +29 -0
  11. package/src/tool/lightingCalculator/how-many-lights-per-room.css +493 -0
  12. package/src/tool/lightingCalculator/i18n/de.ts +213 -0
  13. package/src/tool/lightingCalculator/i18n/en.ts +213 -0
  14. package/src/tool/lightingCalculator/i18n/es.ts +213 -0
  15. package/src/tool/lightingCalculator/i18n/fr.ts +213 -0
  16. package/src/tool/lightingCalculator/i18n/id.ts +213 -0
  17. package/src/tool/lightingCalculator/i18n/it.ts +213 -0
  18. package/src/tool/lightingCalculator/i18n/ja.ts +213 -0
  19. package/src/tool/lightingCalculator/i18n/ko.ts +213 -0
  20. package/src/tool/lightingCalculator/i18n/nl.ts +213 -0
  21. package/src/tool/lightingCalculator/i18n/pl.ts +213 -0
  22. package/src/tool/lightingCalculator/i18n/pt.ts +213 -0
  23. package/src/tool/lightingCalculator/i18n/ru.ts +213 -0
  24. package/src/tool/lightingCalculator/i18n/sv.ts +213 -0
  25. package/src/tool/lightingCalculator/i18n/tr.ts +213 -0
  26. package/src/tool/lightingCalculator/i18n/zh.ts +213 -0
  27. package/src/tool/lightingCalculator/index.ts +9 -0
  28. package/src/tool/lightingCalculator/logic.ts +119 -0
  29. package/src/tool/lightingCalculator/seo.astro +15 -0
  30. package/src/tool/lightingCalculator/state.ts +113 -0
  31. package/src/tool/lightingCalculator/ui.ts +48 -0
  32. package/src/tool/tileLayoutCalculator/bibliography.astro +14 -0
  33. package/src/tool/tileLayoutCalculator/bibliography.ts +10 -0
  34. package/src/tool/tileLayoutCalculator/component.astro +415 -0
  35. package/src/tool/tileLayoutCalculator/entry.ts +29 -0
  36. package/src/tool/tileLayoutCalculator/i18n/de.ts +208 -0
  37. package/src/tool/tileLayoutCalculator/i18n/en.ts +208 -0
  38. package/src/tool/tileLayoutCalculator/i18n/es.ts +208 -0
  39. package/src/tool/tileLayoutCalculator/i18n/fr.ts +208 -0
  40. package/src/tool/tileLayoutCalculator/i18n/id.ts +208 -0
  41. package/src/tool/tileLayoutCalculator/i18n/it.ts +208 -0
  42. package/src/tool/tileLayoutCalculator/i18n/ja.ts +208 -0
  43. package/src/tool/tileLayoutCalculator/i18n/ko.ts +208 -0
  44. package/src/tool/tileLayoutCalculator/i18n/nl.ts +208 -0
  45. package/src/tool/tileLayoutCalculator/i18n/pl.ts +208 -0
  46. package/src/tool/tileLayoutCalculator/i18n/pt.ts +208 -0
  47. package/src/tool/tileLayoutCalculator/i18n/ru.ts +208 -0
  48. package/src/tool/tileLayoutCalculator/i18n/sv.ts +208 -0
  49. package/src/tool/tileLayoutCalculator/i18n/tr.ts +208 -0
  50. package/src/tool/tileLayoutCalculator/i18n/zh.ts +208 -0
  51. package/src/tool/tileLayoutCalculator/index.ts +9 -0
  52. package/src/tool/tileLayoutCalculator/logic.ts +55 -0
  53. package/src/tool/tileLayoutCalculator/seo.astro +15 -0
  54. package/src/tool/tileLayoutCalculator/tile-layout-calculator.css +404 -0
  55. package/src/tool/tileLayoutCalculator/ui.ts +37 -0
  56. package/src/tools.ts +4 -0
@@ -0,0 +1,247 @@
1
+ import type { LightingResult } from './logic';
2
+
3
+ interface PlanConfig {
4
+ widthM: number;
5
+ lengthM: number;
6
+ optimalBulbs: number;
7
+ luxRatio: number;
8
+ ambiance: number;
9
+ status: 'optimal' | 'insufficient' | 'excess';
10
+ }
11
+
12
+ interface DialConfig {
13
+ value: number;
14
+ target: number;
15
+ max: number;
16
+ label: string;
17
+ }
18
+
19
+ function addRect(svg: SVGSVGElement, cfg: { x: number; y: number; w: number; h: number; fill: string; stroke: string; strokeWidth: number }) {
20
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
21
+ rect.setAttribute('x', String(cfg.x));
22
+ rect.setAttribute('y', String(cfg.y));
23
+ rect.setAttribute('width', String(cfg.w));
24
+ rect.setAttribute('height', String(cfg.h));
25
+ rect.setAttribute('rx', '16');
26
+ rect.setAttribute('fill', cfg.fill);
27
+ rect.setAttribute('stroke', cfg.stroke);
28
+ rect.setAttribute('stroke-width', String(cfg.strokeWidth));
29
+ svg.appendChild(rect);
30
+ }
31
+
32
+ function addGlow(svg: SVGSVGElement, cfg: { cx: number; cy: number; color: string; opacity: number; luxRatio: number; ambiance: number }) {
33
+ const radius = (6 + cfg.luxRatio * 18) * cfg.ambiance;
34
+ const gradientId = `glow-${cfg.cx}-${cfg.cy}`.replace(/\./g, '_');
35
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
36
+ const radial = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient');
37
+ radial.setAttribute('id', gradientId);
38
+ radial.setAttribute('cx', '50%');
39
+ radial.setAttribute('cy', '50%');
40
+ radial.setAttribute('r', '50%');
41
+ const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
42
+ stop1.setAttribute('offset', '0%');
43
+ stop1.setAttribute('stop-color', cfg.color);
44
+ stop1.setAttribute('stop-opacity', String(cfg.opacity * 0.6));
45
+ const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
46
+ stop2.setAttribute('offset', '100%');
47
+ stop2.setAttribute('stop-color', cfg.color);
48
+ stop2.setAttribute('stop-opacity', '0');
49
+ radial.appendChild(stop1);
50
+ radial.appendChild(stop2);
51
+ defs.appendChild(radial);
52
+ svg.appendChild(defs);
53
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
54
+ circle.setAttribute('cx', String(cfg.cx));
55
+ circle.setAttribute('cy', String(cfg.cy));
56
+ circle.setAttribute('r', String(radius));
57
+ circle.setAttribute('fill', `url(#${gradientId})`);
58
+ circle.setAttribute('pointer-events', 'none');
59
+ svg.appendChild(circle);
60
+ }
61
+
62
+ function addFixture(svg: SVGSVGElement, cfg: { cx: number; cy: number; color: string; opacity: number }) {
63
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
64
+ circle.setAttribute('cx', String(cfg.cx));
65
+ circle.setAttribute('cy', String(cfg.cy));
66
+ circle.setAttribute('r', '3');
67
+ circle.setAttribute('fill', cfg.color);
68
+ circle.setAttribute('opacity', String(cfg.opacity));
69
+ svg.appendChild(circle);
70
+ }
71
+
72
+ function addOverlay(svg: SVGSVGElement, cfg: { x: number; y: number; w: number; h: number; opacity: number }) {
73
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
74
+ rect.setAttribute('x', String(cfg.x));
75
+ rect.setAttribute('y', String(cfg.y));
76
+ rect.setAttribute('width', String(cfg.w));
77
+ rect.setAttribute('height', String(cfg.h));
78
+ rect.setAttribute('rx', '8');
79
+ rect.setAttribute('fill', `rgba(0,0,0,${cfg.opacity})`);
80
+ rect.setAttribute('pointer-events', 'none');
81
+ svg.appendChild(rect);
82
+ }
83
+
84
+ function getAmbianceColor(ambiance: number): string {
85
+ if (ambiance < 0.9) return '#f59e0b';
86
+ if (ambiance > 1.1) return '#fff';
87
+ return '#facc15';
88
+ }
89
+
90
+ function getPositions(count: number, w: number, h: number): Array<{ x: number; y: number }> {
91
+ const positions: Array<{ x: number; y: number }> = [];
92
+ const pad = 20;
93
+ const effW = w - pad * 2;
94
+ const effH = h - pad * 2;
95
+ if (count <= 1) {
96
+ positions.push({ x: w / 2, y: h / 2 });
97
+ return positions;
98
+ }
99
+ const cols = Math.ceil(Math.sqrt(count * (effW / effH)));
100
+ let rows = Math.ceil(count / cols);
101
+ if (rows * cols < count) rows += 1;
102
+ const cellW = effW / cols;
103
+ const cellH = effH / rows;
104
+ for (let i = 0; i < count; i++) {
105
+ const c = i % cols;
106
+ const r = Math.floor(i / cols);
107
+ const x = pad + cellW * c + cellW / 2;
108
+ const y = pad + cellH * r + cellH / 2;
109
+ positions.push({ x, y });
110
+ }
111
+ return positions;
112
+ }
113
+
114
+ function drawFixtures(svg: SVGSVGElement, cfg: PlanConfig, origin: { cx: number; cy: number; rx: number; ry: number }) {
115
+ const w = origin.rx * 2;
116
+ const h = origin.ry * 2;
117
+ const opacity = 0.6 + cfg.luxRatio * 0.4;
118
+ const haloColor = getAmbianceColor(cfg.ambiance);
119
+ const positions = getPositions(cfg.optimalBulbs, w, h);
120
+ positions.forEach((p) => {
121
+ const bx = origin.cx - origin.rx + p.x;
122
+ const by = origin.cy - origin.ry + p.y;
123
+ addGlow(svg, { cx: bx, cy: by, color: haloColor, opacity, luxRatio: cfg.luxRatio, ambiance: cfg.ambiance });
124
+ addFixture(svg, { cx: bx, cy: by, color: '#fff', opacity });
125
+ });
126
+ }
127
+
128
+ export function drawPlan(svg: SVGSVGElement, cfg: PlanConfig) {
129
+ svg.innerHTML = '';
130
+ const maxDim = Math.max(cfg.widthM, cfg.lengthM);
131
+ const scale = 280 / maxDim;
132
+ const w = cfg.widthM * scale;
133
+ const h = cfg.lengthM * scale;
134
+ const cx = 150;
135
+ const cy = 100;
136
+ const rx = w / 2;
137
+ const ry = h / 2;
138
+ addRect(svg, { x: 0, y: 0, w: 300, h: 200, fill: '#ffffff', stroke: '#ffffff', strokeWidth: 0 });
139
+ const opacity = Math.min(0.85, Math.max(0, 1 - cfg.luxRatio));
140
+ addOverlay(svg, { x: cx - rx, y: cy - ry, w, h, opacity });
141
+ drawFixtures(svg, cfg, { cx, cy, rx, ry });
142
+ }
143
+
144
+ function addDialArc(svg: SVGSVGElement, cfg: { radius: number; color: string; dash: string; offset: number }) {
145
+ const cx = 60;
146
+ const cy = 55;
147
+ const arc = document.createElementNS('http://www.w3.org/2000/svg', 'path');
148
+ arc.setAttribute('d', `M ${cx - cfg.radius} ${cy} A ${cfg.radius} ${cfg.radius} 0 0 1 ${cx + cfg.radius} ${cy}`);
149
+ arc.setAttribute('fill', 'none');
150
+ arc.setAttribute('stroke', cfg.color);
151
+ arc.setAttribute('stroke-width', '8');
152
+ arc.setAttribute('stroke-linecap', 'round');
153
+ arc.setAttribute('stroke-dasharray', cfg.dash);
154
+ if (cfg.offset !== 0) arc.setAttribute('stroke-dashoffset', String(cfg.offset));
155
+ svg.appendChild(arc);
156
+ }
157
+
158
+ function addDialText(svg: SVGSVGElement, cfg: { x: number; y: number; text: string; size: string; color: string; weight: string }) {
159
+ const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
160
+ t.setAttribute('x', String(cfg.x));
161
+ t.setAttribute('y', String(cfg.y));
162
+ t.setAttribute('text-anchor', 'middle');
163
+ t.setAttribute('fill', cfg.color);
164
+ t.setAttribute('font-size', cfg.size);
165
+ t.setAttribute('font-weight', cfg.weight);
166
+ t.textContent = cfg.text;
167
+ svg.appendChild(t);
168
+ }
169
+
170
+ function getDialColor(value: number, target: number): string {
171
+ const minOk = target * 0.9;
172
+ const maxOk = target * 1.3;
173
+ if (value >= minOk && value <= maxOk) return '#22c55e';
174
+ if (value < minOk) return '#f59e0b';
175
+ return '#ef4444';
176
+ }
177
+
178
+ export function drawDial(svg: SVGSVGElement, cfg: DialConfig) {
179
+ svg.innerHTML = '';
180
+ const radius = 45;
181
+ const arcLen = Math.PI * radius;
182
+ const minOk = cfg.target * 0.9;
183
+ const maxOk = cfg.target * 1.3;
184
+ const minOkLen = (minOk / cfg.max) * arcLen;
185
+ const maxOkLen = (maxOk / cfg.max) * arcLen;
186
+ const valueLen = (cfg.value / cfg.max) * arcLen;
187
+ addDialArc(svg, { radius, color: 'var(--border-color)', dash: `${minOkLen} ${arcLen - minOkLen}`, offset: 0 });
188
+ addDialArc(svg, { radius, color: 'rgba(34, 197, 94, 0.2)', dash: `${maxOkLen - minOkLen} ${arcLen - (maxOkLen - minOkLen)}`, offset: -minOkLen });
189
+ addDialArc(svg, { radius, color: 'var(--border-color)', dash: `${arcLen - maxOkLen} ${maxOkLen}`, offset: -maxOkLen });
190
+ addDialArc(svg, { radius, color: getDialColor(cfg.value, cfg.target), dash: `${valueLen} ${arcLen - valueLen}`, offset: 0 });
191
+ addDialText(svg, { x: 60, y: 53, text: String(Math.round(cfg.value)), size: '12', color: 'var(--text-main)', weight: '700' });
192
+ addDialText(svg, { x: 60, y: 65, text: cfg.label, size: '7', color: 'var(--text-muted)', weight: '500' });
193
+ }
194
+
195
+ export function drawBulbs(container: HTMLElement, optimal: number) {
196
+ container.innerHTML = '';
197
+ const maxVisible = Math.min(optimal, 20);
198
+ const activeColor = '#facc15';
199
+ for (let i = 0; i < maxVisible; i++) {
200
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
201
+ svg.setAttribute('viewBox', '0 0 24 24');
202
+ svg.setAttribute('width', '20');
203
+ svg.setAttribute('height', '20');
204
+ svg.style.filter = 'drop-shadow(0 0 3px rgba(250, 204, 21, 0.5))';
205
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
206
+ path.setAttribute('d', 'M12 2C8.13 2 5 5.13 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.87-3.13-7-7-7z');
207
+ path.setAttribute('fill', activeColor);
208
+ path.setAttribute('stroke', activeColor);
209
+ path.setAttribute('stroke-width', '1');
210
+ svg.appendChild(path);
211
+ container.appendChild(svg);
212
+ }
213
+ if (optimal > maxVisible) {
214
+ const span = document.createElement('span');
215
+ span.style.cssText = 'font-size:0.75rem;color:var(--text-muted);align-self:center;';
216
+ span.textContent = `+${optimal - maxVisible}`;
217
+ container.appendChild(span);
218
+ }
219
+ }
220
+
221
+ interface PDFData {
222
+ roomType: string;
223
+ width: number;
224
+ length: number;
225
+ height: number;
226
+ r: LightingResult;
227
+ unitSys: string;
228
+ unitLabel: string;
229
+ }
230
+
231
+ export function generatePDF(data: PDFData) {
232
+ import('jspdf').then(({ jsPDF }) => {
233
+ const doc = new jsPDF();
234
+ doc.setFontSize(18);
235
+ doc.text('Lighting Plan', 14, 20);
236
+ doc.setFontSize(12);
237
+ doc.text(`Room: ${data.roomType}`, 14, 35);
238
+ doc.text(`Dimensions: ${data.width}${data.unitLabel} × ${data.length}${data.unitLabel} × ${data.height}${data.unitLabel}`, 14, 45);
239
+ doc.text(`Area: ${Math.round(data.r.roomArea)} m²`, 14, 55);
240
+ doc.text(`Target lux: ${data.r.targetLux}`, 14, 65);
241
+ doc.text(`Current lux: ${data.r.currentLux}`, 14, 75);
242
+ doc.text(`Required lumens: ${data.r.requiredLumens}`, 14, 85);
243
+ doc.text(`Recommended: ${data.r.suggestedProducts}`, 14, 95);
244
+ doc.text(`Status: ${data.r.status}`, 14, 105);
245
+ doc.save('lighting-plan.pdf');
246
+ });
247
+ }
@@ -0,0 +1,29 @@
1
+ import type { HomeToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { LightingCalculatorUI } from './ui';
3
+
4
+ export type LightingCalculatorLocaleContent = ToolLocaleContent<LightingCalculatorUI>;
5
+
6
+ export const lightingCalculator: HomeToolEntry<LightingCalculatorUI> = {
7
+ id: 'how-many-lights-per-room',
8
+ icons: {
9
+ bg: 'mdi:lightbulb',
10
+ fg: 'mdi:flash',
11
+ },
12
+ i18n: {
13
+ de: async () => (await import('./i18n/de')).content,
14
+ en: async () => (await import('./i18n/en')).content,
15
+ es: async () => (await import('./i18n/es')).content,
16
+ fr: async () => (await import('./i18n/fr')).content,
17
+ id: async () => (await import('./i18n/id')).content,
18
+ it: async () => (await import('./i18n/it')).content,
19
+ ja: async () => (await import('./i18n/ja')).content,
20
+ ko: async () => (await import('./i18n/ko')).content,
21
+ nl: async () => (await import('./i18n/nl')).content,
22
+ pl: async () => (await import('./i18n/pl')).content,
23
+ pt: async () => (await import('./i18n/pt')).content,
24
+ ru: async () => (await import('./i18n/ru')).content,
25
+ sv: async () => (await import('./i18n/sv')).content,
26
+ tr: async () => (await import('./i18n/tr')).content,
27
+ zh: async () => (await import('./i18n/zh')).content,
28
+ },
29
+ };