homebridge-multiple-switch 1.5.0 → 1.5.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.1] - 2026-03-21
4
+
5
+ ### Added
6
+ - Collapsible device cards — click the device header to expand/collapse
7
+ - Collapsible switch cards — click the switch header to expand/collapse
8
+ - Summary info shown when collapsed (switch count for devices, type/delay for switches)
9
+ - Chevron indicator (▶) with rotation animation for open/closed state
10
+
3
11
  ## [1.5.0] - 2026-03-21
4
12
 
5
13
  ### Added
@@ -52,29 +52,58 @@
52
52
  }
53
53
  .device-card {
54
54
  border: 2px solid var(--ui-device-border);
55
- border-radius: 10px; padding: 20px; margin-bottom: 20px;
55
+ border-radius: 10px; margin-bottom: 20px;
56
56
  background: var(--ui-device-bg);
57
+ overflow: hidden;
57
58
  }
58
59
  .device-header {
59
60
  display: flex; justify-content: space-between; align-items: center;
60
- margin-bottom: 16px; padding-bottom: 10px;
61
- border-bottom: 1px solid var(--ui-border);
61
+ padding: 14px 20px; cursor: pointer; user-select: none;
62
62
  }
63
- .device-header strong {
63
+ .device-header:hover { opacity: 0.85; }
64
+ .device-header-left {
65
+ display: flex; align-items: center; gap: 10px;
66
+ }
67
+ .device-header-left strong {
64
68
  font-size: 16px; color: var(--ui-accent);
65
69
  }
70
+ .device-header-left .chevron {
71
+ font-size: 12px; color: var(--ui-text-muted);
72
+ transition: transform 0.2s;
73
+ display: inline-block;
74
+ }
75
+ .device-header-left .chevron.open { transform: rotate(90deg); }
76
+ .device-header-right { display: flex; align-items: center; gap: 8px; }
77
+ .device-body { padding: 0 20px 20px; }
78
+ .device-body.collapsed { display: none; }
66
79
  .switch-card {
67
80
  border: 1px solid var(--ui-border);
68
- border-radius: 8px; padding: 16px; margin-bottom: 12px;
81
+ border-radius: 8px; margin-bottom: 12px;
69
82
  background: var(--ui-card-bg);
70
- position: relative;
83
+ position: relative; overflow: hidden;
84
+ }
85
+ .switch-header {
86
+ display: flex; justify-content: space-between; align-items: center;
87
+ padding: 12px 16px; cursor: pointer; user-select: none;
71
88
  }
72
- .switch-card .switch-header {
73
- display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;
89
+ .switch-header:hover { opacity: 0.85; }
90
+ .switch-header-left {
91
+ display: flex; align-items: center; gap: 8px;
74
92
  }
75
- .switch-card .switch-header strong {
76
- font-size: 14px;
77
- color: var(--ui-text);
93
+ .switch-header-left strong {
94
+ font-size: 14px; color: var(--ui-text);
95
+ }
96
+ .switch-header-left .chevron {
97
+ font-size: 11px; color: var(--ui-text-muted);
98
+ transition: transform 0.2s;
99
+ display: inline-block;
100
+ }
101
+ .switch-header-left .chevron.open { transform: rotate(90deg); }
102
+ .switch-header-right { display: flex; align-items: center; gap: 8px; }
103
+ .switch-body { padding: 0 16px 16px; }
104
+ .switch-body.collapsed { display: none; }
105
+ .switch-summary {
106
+ font-size: 12px; color: var(--ui-text-muted); margin-left: 4px;
78
107
  }
79
108
  .btn {
80
109
  padding: 8px 16px; border: none; border-radius: 6px;
@@ -82,6 +111,7 @@
82
111
  }
83
112
  .btn-danger { background: #dc3545; color: #fff; }
84
113
  .btn-danger:hover { background: #c82333; }
114
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
85
115
  .btn-primary { background: var(--ui-accent); color: #fff; }
86
116
  .btn-primary:hover { opacity: 0.85; }
87
117
  .btn-outline {
@@ -150,6 +180,15 @@
150
180
  config.devices = [];
151
181
  }
152
182
 
183
+ // Track expand/collapse state
184
+ const expandedDevices = new Set(config.devices.map((_, i) => i));
185
+ const expandedSwitches = new Set();
186
+ config.devices.forEach((dev, di) => {
187
+ (dev.switches || []).forEach((_, si) => {
188
+ expandedSwitches.add(`${di}_${si}`);
189
+ });
190
+ });
191
+
153
192
  const app = document.getElementById('app');
154
193
 
155
194
  function render() {
@@ -178,15 +217,47 @@
178
217
  });
179
218
 
180
219
  document.getElementById('btn-add-device').addEventListener('click', () => {
220
+ const di = config.devices.length;
181
221
  config.devices.push({
182
222
  name: '',
183
223
  switchBehavior: 'independent',
184
224
  switches: [{ name: '', type: 'outlet', defaultState: false, delayOff: 0 }],
185
225
  });
226
+ expandedDevices.add(di);
227
+ expandedSwitches.add(`${di}_0`);
186
228
  render();
187
229
  save();
188
230
  });
189
231
 
232
+ // Device toggle
233
+ document.querySelectorAll('.device-toggle').forEach(el => {
234
+ el.addEventListener('click', (e) => {
235
+ // Don't toggle if clicking a button inside header
236
+ if (e.target.closest('.btn')) return;
237
+ const di = parseInt(el.dataset.dev);
238
+ if (expandedDevices.has(di)) {
239
+ expandedDevices.delete(di);
240
+ } else {
241
+ expandedDevices.add(di);
242
+ }
243
+ render();
244
+ });
245
+ });
246
+
247
+ // Switch toggle
248
+ document.querySelectorAll('.switch-toggle').forEach(el => {
249
+ el.addEventListener('click', (e) => {
250
+ if (e.target.closest('.btn')) return;
251
+ const key = `${el.dataset.dev}_${el.dataset.sw}`;
252
+ if (expandedSwitches.has(key)) {
253
+ expandedSwitches.delete(key);
254
+ } else {
255
+ expandedSwitches.add(key);
256
+ }
257
+ render();
258
+ });
259
+ });
260
+
190
261
  // Device-level events
191
262
  document.querySelectorAll('.dev-field').forEach(input => {
192
263
  input.addEventListener('change', () => {
@@ -198,8 +269,12 @@
198
269
  });
199
270
 
200
271
  document.querySelectorAll('.btn-remove-device').forEach(btn => {
201
- btn.addEventListener('click', () => {
202
- config.devices.splice(parseInt(btn.dataset.dev), 1);
272
+ btn.addEventListener('click', (e) => {
273
+ e.stopPropagation();
274
+ const di = parseInt(btn.dataset.dev);
275
+ config.devices.splice(di, 1);
276
+ // Rebuild expand state
277
+ rebuildExpandState();
203
278
  render();
204
279
  save();
205
280
  });
@@ -209,17 +284,21 @@
209
284
  document.querySelectorAll('.btn-add-switch').forEach(btn => {
210
285
  btn.addEventListener('click', () => {
211
286
  const di = parseInt(btn.dataset.dev);
287
+ const si = config.devices[di].switches.length;
212
288
  config.devices[di].switches.push({ name: '', type: 'outlet', defaultState: false, delayOff: 0 });
289
+ expandedSwitches.add(`${di}_${si}`);
213
290
  render();
214
291
  save();
215
292
  });
216
293
  });
217
294
 
218
295
  document.querySelectorAll('.btn-remove-switch').forEach(btn => {
219
- btn.addEventListener('click', () => {
296
+ btn.addEventListener('click', (e) => {
297
+ e.stopPropagation();
220
298
  const di = parseInt(btn.dataset.dev);
221
299
  const si = parseInt(btn.dataset.sw);
222
300
  config.devices[di].switches.splice(si, 1);
301
+ rebuildExpandState();
223
302
  render();
224
303
  save();
225
304
  });
@@ -242,69 +321,111 @@
242
321
  });
243
322
  }
244
323
 
324
+ function rebuildExpandState() {
325
+ // Keep devices that still exist expanded
326
+ const newDevices = new Set();
327
+ config.devices.forEach((_, i) => {
328
+ if (expandedDevices.has(i)) newDevices.add(i);
329
+ });
330
+ expandedDevices.clear();
331
+ newDevices.forEach(i => expandedDevices.add(i));
332
+
333
+ const newSwitches = new Set();
334
+ config.devices.forEach((dev, di) => {
335
+ (dev.switches || []).forEach((_, si) => {
336
+ if (expandedSwitches.has(`${di}_${si}`)) newSwitches.add(`${di}_${si}`);
337
+ });
338
+ });
339
+ expandedSwitches.clear();
340
+ newSwitches.forEach(k => expandedSwitches.add(k));
341
+ }
342
+
245
343
  function renderDevice(dev, di) {
246
344
  const switches = dev.switches || [];
345
+ const isOpen = expandedDevices.has(di);
346
+ const devLabel = dev.name || `${t.device || 'Device'} #${di + 1}`;
347
+ const switchCount = switches.length;
348
+
247
349
  return `
248
350
  <div class="device-card">
249
- <div class="device-header">
250
- <strong>${t.device || 'Device'} #${di + 1}</strong>
251
- <button class="btn btn-danger btn-remove-device" data-dev="${di}">${t.removeDevice || 'Remove Device'}</button>
252
- </div>
253
-
254
- <div class="inline-row">
255
- <div class="form-group">
256
- <label>${t.deviceName || 'Device Name'}</label>
257
- <input type="text" class="dev-field" data-dev="${di}" data-field="name" value="${esc(dev.name || '')}">
351
+ <div class="device-header device-toggle" data-dev="${di}">
352
+ <div class="device-header-left">
353
+ <span class="chevron ${isOpen ? 'open' : ''}">&#9654;</span>
354
+ <strong>${esc(devLabel)}</strong>
355
+ ${!isOpen ? `<span class="switch-summary">(${switchCount} ${switchCount === 1 ? (t.switchSingular || 'switch') : (t.switches || 'switches').toLowerCase()})</span>` : ''}
258
356
  </div>
259
- <div class="form-group">
260
- <label>${t.switchBehavior || 'Switch Behavior Mode'}</label>
261
- <select class="dev-field" data-dev="${di}" data-field="switchBehavior">
262
- <option value="independent" ${dev.switchBehavior === 'independent' || !dev.switchBehavior ? 'selected' : ''}>${t.behaviorIndependent || 'Independent'}</option>
263
- <option value="master" ${dev.switchBehavior === 'master' ? 'selected' : ''}>${t.behaviorMaster || 'Master'}</option>
264
- <option value="single" ${dev.switchBehavior === 'single' ? 'selected' : ''}>${t.behaviorSingle || 'Single'}</option>
265
- </select>
357
+ <div class="device-header-right">
358
+ <button class="btn btn-danger btn-sm btn-remove-device" data-dev="${di}">${t.removeDevice || 'Remove Device'}</button>
266
359
  </div>
267
360
  </div>
268
361
 
269
- <div class="section-title">${t.switches || 'Switches'}</div>
362
+ <div class="device-body ${isOpen ? '' : 'collapsed'}">
363
+ <div class="inline-row">
364
+ <div class="form-group">
365
+ <label>${t.deviceName || 'Device Name'}</label>
366
+ <input type="text" class="dev-field" data-dev="${di}" data-field="name" value="${esc(dev.name || '')}">
367
+ </div>
368
+ <div class="form-group">
369
+ <label>${t.switchBehavior || 'Switch Behavior Mode'}</label>
370
+ <select class="dev-field" data-dev="${di}" data-field="switchBehavior">
371
+ <option value="independent" ${dev.switchBehavior === 'independent' || !dev.switchBehavior ? 'selected' : ''}>${t.behaviorIndependent || 'Independent'}</option>
372
+ <option value="master" ${dev.switchBehavior === 'master' ? 'selected' : ''}>${t.behaviorMaster || 'Master'}</option>
373
+ <option value="single" ${dev.switchBehavior === 'single' ? 'selected' : ''}>${t.behaviorSingle || 'Single'}</option>
374
+ </select>
375
+ </div>
376
+ </div>
270
377
 
271
- ${switches.map((sw, si) => renderSwitch(sw, di, si)).join('')}
378
+ <div class="section-title">${t.switches || 'Switches'}</div>
272
379
 
273
- <button class="btn btn-primary btn-add-switch" data-dev="${di}">+ ${t.addSwitch || 'Add Switch'}</button>
380
+ ${switches.map((sw, si) => renderSwitch(sw, di, si)).join('')}
381
+
382
+ <button class="btn btn-primary btn-add-switch" data-dev="${di}">+ ${t.addSwitch || 'Add Switch'}</button>
383
+ </div>
274
384
  </div>
275
385
  `;
276
386
  }
277
387
 
278
388
  function renderSwitch(sw, di, si) {
389
+ const isOpen = expandedSwitches.has(`${di}_${si}`);
390
+ const swLabel = sw.name || `#${si + 1}`;
391
+
279
392
  return `
280
393
  <div class="switch-card">
281
- <div class="switch-header">
282
- <strong>#${si + 1}</strong>
283
- <button class="btn btn-danger btn-remove-switch" data-dev="${di}" data-sw="${si}">${t.removeSwitch || 'Remove'}</button>
284
- </div>
285
- <div class="inline-row">
286
- <div class="form-group">
287
- <label>${t.switchName || 'Switch Name'}</label>
288
- <input type="text" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="name" value="${esc(sw.name || '')}">
394
+ <div class="switch-header switch-toggle" data-dev="${di}" data-sw="${si}">
395
+ <div class="switch-header-left">
396
+ <span class="chevron ${isOpen ? 'open' : ''}">&#9654;</span>
397
+ <strong>${esc(swLabel)}</strong>
398
+ ${!isOpen ? `<span class="switch-summary">${sw.type || 'outlet'}${sw.delayOff ? ` / ${sw.delayOff}ms` : ''}</span>` : ''}
289
399
  </div>
290
- <div class="form-group">
291
- <label>${t.switchType || 'Switch Type'}</label>
292
- <select class="sw-field" data-dev="${di}" data-sw="${si}" data-field="type">
293
- <option value="switch" ${sw.type === 'switch' ? 'selected' : ''}>${t.typeSwitch || 'Switch'}</option>
294
- <option value="outlet" ${sw.type === 'outlet' || !sw.type ? 'selected' : ''}>${t.typeOutlet || 'Outlet'}</option>
295
- </select>
400
+ <div class="switch-header-right">
401
+ <button class="btn btn-danger btn-sm btn-remove-switch" data-dev="${di}" data-sw="${si}">${t.removeSwitch || 'Remove'}</button>
296
402
  </div>
297
403
  </div>
298
- <div class="inline-row">
299
- <div class="form-group">
300
- <label>${t.delayOff || 'Auto Turn Off (ms)'}</label>
301
- <input type="number" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="delayOff" min="0" value="${sw.delayOff || 0}">
404
+ <div class="switch-body ${isOpen ? '' : 'collapsed'}">
405
+ <div class="inline-row">
406
+ <div class="form-group">
407
+ <label>${t.switchName || 'Switch Name'}</label>
408
+ <input type="text" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="name" value="${esc(sw.name || '')}">
409
+ </div>
410
+ <div class="form-group">
411
+ <label>${t.switchType || 'Switch Type'}</label>
412
+ <select class="sw-field" data-dev="${di}" data-sw="${si}" data-field="type">
413
+ <option value="switch" ${sw.type === 'switch' ? 'selected' : ''}>${t.typeSwitch || 'Switch'}</option>
414
+ <option value="outlet" ${sw.type === 'outlet' || !sw.type ? 'selected' : ''}>${t.typeOutlet || 'Outlet'}</option>
415
+ </select>
416
+ </div>
302
417
  </div>
303
- <div class="form-group">
304
- <label>${t.defaultState || 'Default State'}</label>
305
- <div class="toggle-wrap" style="margin-top:6px">
306
- <input type="checkbox" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="defaultState" ${sw.defaultState ? 'checked' : ''}>
307
- <span>${sw.defaultState ? (t.on || 'On') : (t.off || 'Off')}</span>
418
+ <div class="inline-row">
419
+ <div class="form-group">
420
+ <label>${t.delayOff || 'Auto Turn Off (ms)'}</label>
421
+ <input type="number" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="delayOff" min="0" value="${sw.delayOff || 0}">
422
+ </div>
423
+ <div class="form-group">
424
+ <label>${t.defaultState || 'Default State'}</label>
425
+ <div class="toggle-wrap" style="margin-top:6px">
426
+ <input type="checkbox" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="defaultState" ${sw.defaultState ? 'checked' : ''}>
427
+ <span>${sw.defaultState ? (t.on || 'On') : (t.off || 'Off')}</span>
428
+ </div>
308
429
  </div>
309
430
  </div>
310
431
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-multiple-switch",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Multiple switch platform for Homebridge",
5
5
  "homepage": "https://github.com/azadaydinli/homebridge-multiple-switch",
6
6
  "main": "index.js",