@mp-consulting/homebridge-unifi-access 1.0.7 → 1.0.9

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.
@@ -108,6 +108,19 @@ const bindSupportScreen = () => {
108
108
  // Initialize the plugin UI.
109
109
  const init = async () => {
110
110
 
111
+ // Confirm theme from Homebridge settings (overrides the early OS-preference detection)
112
+ try {
113
+ const settings = await homebridge.getUserSettings();
114
+ const scheme = settings.colorScheme;
115
+ if (scheme === 'dark' || scheme === 'light') {
116
+ document.documentElement.dataset.bsTheme = scheme;
117
+ } else if (scheme === 'auto') {
118
+ document.documentElement.dataset.bsTheme =
119
+ window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
120
+ }
121
+ } catch {
122
+ // getUserSettings not available in older versions — keep the early-detected theme
123
+ }
111
124
 
112
125
  bindDiscoveryScreen();
113
126
  bindSetupScreen();
@@ -1,267 +1,293 @@
1
- <link rel="stylesheet" href="styles.css">
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-bs-theme="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>UniFi Access</title>
7
+ <!-- Detect theme before CSS renders to avoid flash of wrong theme -->
8
+ <script>
9
+ (function () {
10
+ var dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
11
+ document.documentElement.dataset.bsTheme = dark ? 'dark' : 'light';
12
+ }());
13
+ </script>
14
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
15
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
16
+ <link rel="stylesheet" href="lib/kit.css">
17
+ <link rel="stylesheet" href="styles.css">
18
+ </head>
19
+ <body>
20
+ <div class="container py-3">
2
21
 
3
- <div id="app" class="container">
4
- <div class="text-center mb-3">
5
- <img src="https://raw.githubusercontent.com/mp-consulting/homebridge-unifi-access/main/docs/media/homebridge-unifi-access.svg" alt="homebridge-unifi-access logo" style="max-width: 200px;" />
6
- <hr>
7
- </div>
22
+ <div class="mp-header mb-3">
23
+ <span class="mp-header-icon" style="color: #0099ff;"><i class="bi bi-door-open-fill"></i></span>
24
+ <div>
25
+ <h1 class="mp-header-title">UniFi Access</h1>
26
+ <p class="mp-header-subtitle">Manage your UniFi Access controllers and devices in HomeKit</p>
27
+ </div>
28
+ </div>
8
29
 
9
- <!-- Screen: Discovery -->
10
- <div id="discoveryScreen">
11
- <h5><i class="fas fa-search"></i> Discover Your Controller</h5>
12
- <p class="text-muted">Scan your network for UniFi Access controllers, or enter the address manually.</p>
30
+ <!-- Screen: Discovery -->
31
+ <div id="discoveryScreen">
32
+ <h5><i class="bi bi-search"></i> Discover Your Controller</h5>
33
+ <p class="text-muted">Scan your network for UniFi Access controllers, or enter the address manually.</p>
13
34
 
14
- <div class="alert alert-info mb-3">
15
- <i class="fas fa-info-circle"></i> Ensure your UniFi console is powered on and connected to the same network.
16
- </div>
35
+ <div class="alert alert-info mb-3">
36
+ <i class="bi bi-info-circle"></i> Ensure your UniFi console is powered on and connected to the same network.
37
+ </div>
17
38
 
18
- <div class="row g-2 mb-3">
19
- <div class="col-12">
20
- <button id="discoverBtn" class="btn btn-secondary">
21
- <span id="discoverSpinner" class="spinner-border spinner-border-sm" style="display: none;"></span>
22
- <span id="discoverBtnText"><i class="fas fa-search"></i> Discover Controllers</span>
23
- </button>
24
- <button id="manualEntryBtn" class="btn btn-outline-primary">
25
- <i class="fas fa-keyboard"></i> Manual Entry
26
- </button>
27
- <button id="cancelDiscoveryBtn" class="btn btn-primary" style="display: none;">Cancel</button>
39
+ <div class="row g-2 mb-3">
40
+ <div class="col-12">
41
+ <button id="discoverBtn" class="btn btn-secondary">
42
+ <span id="discoverSpinner" class="spinner-border spinner-border-sm" style="display: none;"></span>
43
+ <span id="discoverBtnText"><i class="bi bi-search"></i> Discover Controllers</span>
44
+ </button>
45
+ <button id="manualEntryBtn" class="btn btn-outline-primary">
46
+ <i class="bi bi-keyboard"></i> Manual Entry
47
+ </button>
48
+ <button id="cancelDiscoveryBtn" class="btn btn-primary" style="display: none;">Cancel</button>
49
+ </div>
28
50
  </div>
29
- </div>
30
51
 
31
- <div id="deviceList" style="display: none;">
32
- <div id="deviceListContainer"></div>
52
+ <div id="deviceList" style="display: none;">
53
+ <div id="deviceListContainer"></div>
54
+ </div>
33
55
  </div>
34
- </div>
35
56
 
36
- <!-- Screen: Setup Controller (Add/Edit) -->
37
- <div id="setupScreen">
38
- <h5><i class="fas fa-shield-alt"></i> <span id="setupTitle">Add UniFi Access Controller</span></h5>
39
- <p class="text-muted" id="setupSubtitle">Enter your UniFi Access controller details and login credentials.</p>
57
+ <!-- Screen: Setup Controller (Add/Edit) -->
58
+ <div id="setupScreen">
59
+ <h5><i class="bi bi-shield-lock"></i> <span id="setupTitle">Add UniFi Access Controller</span></h5>
60
+ <p class="text-muted" id="setupSubtitle">Enter your UniFi Access controller details and login credentials.</p>
40
61
 
41
- <form id="setupForm" novalidate>
42
- <div class="mb-3 row">
43
- <label for="inputAddress" class="col-sm-3 col-form-label"><i class="fas fa-server"></i> Controller</label>
44
- <div class="col-sm-9">
45
- <input type="text" class="form-control" id="inputAddress" placeholder="Hostname or IP (e.g. unvr.local)" required>
46
- <small class="form-text text-muted">The address of your UniFi Access controller.</small>
62
+ <form id="setupForm" novalidate>
63
+ <div class="mb-3 row">
64
+ <label for="inputAddress" class="col-sm-3 col-form-label"><i class="bi bi-server"></i> Controller</label>
65
+ <div class="col-sm-9">
66
+ <input type="text" class="form-control" id="inputAddress" placeholder="Hostname or IP (e.g. unvr.local)" required>
67
+ <small class="form-text text-muted">The address of your UniFi Access controller.</small>
68
+ </div>
47
69
  </div>
48
- </div>
49
- <div class="mb-3 row">
50
- <label for="inputUsername" class="col-sm-3 col-form-label"><i class="fas fa-user"></i> Username</label>
51
- <div class="col-sm-9">
52
- <input type="text" class="form-control" id="inputUsername" autocomplete="username" placeholder="Local user account username" required>
70
+ <div class="mb-3 row">
71
+ <label for="inputUsername" class="col-sm-3 col-form-label"><i class="bi bi-person"></i> Username</label>
72
+ <div class="col-sm-9">
73
+ <input type="text" class="form-control" id="inputUsername" autocomplete="username" placeholder="Local user account username" required>
74
+ </div>
53
75
  </div>
54
- </div>
55
- <div class="mb-3 row">
56
- <label for="inputPassword" class="col-sm-3 col-form-label"><i class="fas fa-lock"></i> Password</label>
57
- <div class="col-sm-9">
58
- <input type="password" class="form-control" id="inputPassword" autocomplete="current-password" placeholder="Local user account password" required>
76
+ <div class="mb-3 row">
77
+ <label for="inputPassword" class="col-sm-3 col-form-label"><i class="bi bi-lock"></i> Password</label>
78
+ <div class="col-sm-9">
79
+ <input type="password" class="form-control" id="inputPassword" autocomplete="current-password" placeholder="Local user account password" required>
80
+ </div>
59
81
  </div>
60
- </div>
61
82
 
62
- <div id="setupError" class="alert alert-danger" style="display: none;">
63
- <i class="fas fa-exclamation-circle"></i> <span id="setupErrorText"></span>
64
- </div>
83
+ <div id="setupError" class="alert alert-danger" style="display: none;">
84
+ <i class="bi bi-exclamation-circle"></i> <span id="setupErrorText"></span>
85
+ </div>
65
86
 
66
- <div class="alert alert-info">
67
- <i class="fas fa-info-circle"></i>
68
- <strong>Local account required.</strong> Ubiquiti.com/UI.com accounts are not supported. Create one via the <em>Admins & Users</em> tab in your UniFi console.
69
- </div>
87
+ <div class="alert alert-info">
88
+ <i class="bi bi-info-circle"></i>
89
+ <strong>Local account required.</strong> Ubiquiti.com/UI.com accounts are not supported. Create one via the <em>Admins &amp; Users</em> tab in your UniFi console.
90
+ </div>
70
91
 
71
- <hr>
72
- <div class="col-12">
73
- <button id="saveControllerBtn" class="btn btn-success" type="submit">
74
- <i class="fas fa-save"></i> Save Controller
92
+ <hr>
93
+ <div class="col-12">
94
+ <button id="saveControllerBtn" class="btn btn-success" type="submit">
95
+ <i class="bi bi-floppy"></i> Save Controller
96
+ </button>
97
+ <button id="cancelSetupBtn" class="btn btn-secondary" type="button">Cancel</button>
98
+ </div>
99
+ </form>
100
+ </div>
101
+
102
+ <!-- Screen: Controllers List (Main) -->
103
+ <div id="controllersScreen">
104
+ <h5><i class="bi bi-hdd-network"></i> Configured Controllers</h5>
105
+ <div id="noControllersMessage" class="alert alert-info mb-3">
106
+ <i class="bi bi-info-circle"></i> No controllers configured yet. Add one to get started.
107
+ </div>
108
+ <ul id="controllersList" class="list-group mb-3"></ul>
109
+ <div class="d-flex gap-2">
110
+ <button id="addControllerBtn" class="btn btn-primary">
111
+ <i class="bi bi-plus"></i> Add Controller
112
+ </button>
113
+ <button id="supportBtn" class="btn btn-outline-secondary">
114
+ <i class="bi bi-life-preserver"></i> Support
75
115
  </button>
76
- <button id="cancelSetupBtn" class="btn btn-secondary" type="button">Cancel</button>
77
116
  </div>
78
- </form>
79
- </div>
80
-
81
- <!-- Screen: Controllers List (Main) -->
82
- <div id="controllersScreen">
83
- <h5><i class="fas fa-network-wired"></i> Configured Controllers</h5>
84
- <div id="noControllersMessage" class="alert alert-info mb-3">
85
- <i class="fas fa-info-circle"></i> No controllers configured yet. Add one to get started.
86
- </div>
87
- <ul id="controllersList" class="list-group mb-3"></ul>
88
- <div class="d-flex gap-2">
89
- <button id="addControllerBtn" class="btn btn-primary">
90
- <i class="fas fa-plus"></i> Add Controller
91
- </button>
92
- <button id="supportBtn" class="btn btn-outline-secondary">
93
- <i class="fas fa-life-ring"></i> Support
94
- </button>
95
117
  </div>
96
- </div>
97
118
 
98
- <!-- Screen: Feature Options (split layout) -->
99
- <div id="featureOptionsScreen">
100
- <div class="d-flex justify-content-between align-items-center mb-3">
101
- <h5 class="mb-0"><i class="fas fa-sliders-h"></i> Feature Options</h5>
102
- <button id="backFromOptionsBtn" class="btn btn-secondary btn-sm">
103
- <i class="fas fa-arrow-left"></i> Back
104
- </button>
105
- </div>
119
+ <!-- Screen: Feature Options (split layout) -->
120
+ <div id="featureOptionsScreen">
121
+ <div class="d-flex justify-content-between align-items-center mb-3">
122
+ <h5 class="mb-0"><i class="bi bi-sliders"></i> Feature Options</h5>
123
+ <button id="backFromOptionsBtn" class="btn btn-secondary btn-sm">
124
+ <i class="bi bi-arrow-left"></i> Back
125
+ </button>
126
+ </div>
106
127
 
107
- <div class="row">
108
- <!-- Left sidebar: scope -->
109
- <div class="col-sm-4">
110
- <div class="scope-sidebar">
111
- <!-- Scope selector -->
112
- <div class="card mb-3">
113
- <div class="card-body py-2">
114
- <label class="form-label mb-1 text-muted"><small><i class="fas fa-layer-group"></i> Scope</small></label>
115
- <select class="form-select form-select-sm" id="scopeSelect"></select>
128
+ <div class="row">
129
+ <!-- Left sidebar: scope -->
130
+ <div class="col-sm-4">
131
+ <div class="scope-sidebar">
132
+ <!-- Scope selector -->
133
+ <div class="card mb-3">
134
+ <div class="card-body py-2">
135
+ <label class="form-label mb-1 text-muted"><small><i class="bi bi-layers"></i> Scope</small></label>
136
+ <select class="form-select form-select-sm" id="scopeSelect"></select>
137
+ </div>
116
138
  </div>
117
- </div>
118
139
 
119
- <!-- Visual cascade -->
120
- <div class="card mb-3">
121
- <div class="card-body py-3" id="scopeCascade">
122
- <div class="scope-cascade">
123
- <div class="scope-level" data-scope="global">
124
- <div class="scope-dot"><i class="fas fa-globe-americas"></i></div>
125
- <div class="scope-text">
126
- <div class="scope-label">Global</div>
127
- <div class="scope-hint">All devices</div>
140
+ <!-- Visual cascade -->
141
+ <div class="card mb-3">
142
+ <div class="card-body py-3" id="scopeCascade">
143
+ <div class="scope-cascade">
144
+ <div class="scope-level" data-scope="global">
145
+ <div class="scope-dot"><i class="bi bi-globe"></i></div>
146
+ <div class="scope-text">
147
+ <div class="scope-label">Global</div>
148
+ <div class="scope-hint">All devices</div>
149
+ </div>
128
150
  </div>
129
- </div>
130
- <div class="scope-connector"><div class="scope-connector-line"></div></div>
131
- <div class="scope-level" data-scope="controller">
132
- <div class="scope-dot"><i class="fas fa-server"></i></div>
133
- <div class="scope-text">
134
- <div class="scope-label">Controller</div>
135
- <div class="scope-hint">Overrides global</div>
151
+ <div class="scope-connector"><div class="scope-connector-line"></div></div>
152
+ <div class="scope-level" data-scope="controller">
153
+ <div class="scope-dot"><i class="bi bi-server"></i></div>
154
+ <div class="scope-text">
155
+ <div class="scope-label">Controller</div>
156
+ <div class="scope-hint">Overrides global</div>
157
+ </div>
136
158
  </div>
137
- </div>
138
- <div class="scope-connector"><div class="scope-connector-line"></div></div>
139
- <div class="scope-level" data-scope="device">
140
- <div class="scope-dot"><i class="fas fa-microchip"></i></div>
141
- <div class="scope-text">
142
- <div class="scope-label">Device</div>
143
- <div class="scope-hint">Final value</div>
159
+ <div class="scope-connector"><div class="scope-connector-line"></div></div>
160
+ <div class="scope-level" data-scope="device">
161
+ <div class="scope-dot"><i class="bi bi-cpu"></i></div>
162
+ <div class="scope-text">
163
+ <div class="scope-label">Device</div>
164
+ <div class="scope-hint">Final value</div>
165
+ </div>
144
166
  </div>
145
167
  </div>
146
168
  </div>
147
169
  </div>
148
- </div>
149
170
 
150
- <!-- Device info panel -->
151
- <div id="deviceInfoPanel" class="card mb-3" style="display: none;">
152
- <div class="card-body py-2">
153
- <div class="mb-2"><small class="text-muted d-block">Model</small><strong id="infoModel"></strong></div>
154
- <div class="mb-2"><small class="text-muted d-block">MAC</small><code id="infoMac" style="font-size:0.75rem"></code></div>
155
- <div class="mb-2"><small class="text-muted d-block">IP</small><code id="infoIp"></code></div>
156
- <div><small class="text-muted d-block">Status</small><span id="infoStatus"></span></div>
171
+ <!-- Device info panel -->
172
+ <div id="deviceInfoPanel" class="card mb-3" style="display: none;">
173
+ <div class="card-body py-2">
174
+ <div class="mb-2"><small class="text-muted d-block">Model</small><strong id="infoModel"></strong></div>
175
+ <div class="mb-2"><small class="text-muted d-block">MAC</small><code id="infoMac" style="font-size:0.75rem"></code></div>
176
+ <div class="mb-2"><small class="text-muted d-block">IP</small><code id="infoIp"></code></div>
177
+ <div><small class="text-muted d-block">Status</small><span id="infoStatus"></span></div>
178
+ </div>
157
179
  </div>
158
- </div>
159
180
 
160
- <!-- Legend -->
161
- <div class="card mb-3">
162
- <div class="card-body py-2">
163
- <small class="text-muted d-block mb-2"><i class="fas fa-palette"></i> Legend</small>
164
- <div class="d-flex flex-column gap-1" style="font-size: 0.75rem;">
165
- <div class="d-flex align-items-center gap-2">
166
- <span class="scope-legend-bar" style="background: #ffc107;"></span> Set at global
167
- </div>
168
- <div class="d-flex align-items-center gap-2">
169
- <span class="scope-legend-bar" style="background: #198754;"></span> Set at controller
170
- </div>
171
- <div class="d-flex align-items-center gap-2">
172
- <span class="scope-legend-bar" style="background: #0dcaf0;"></span> Set at device
181
+ <!-- Legend -->
182
+ <div class="card mb-3">
183
+ <div class="card-body py-2">
184
+ <small class="text-muted d-block mb-2"><i class="bi bi-palette"></i> Legend</small>
185
+ <div class="d-flex flex-column gap-1" style="font-size: 0.75rem;">
186
+ <div class="d-flex align-items-center gap-2">
187
+ <span class="scope-legend-bar" style="background: #ffc107;"></span> Set at global
188
+ </div>
189
+ <div class="d-flex align-items-center gap-2">
190
+ <span class="scope-legend-bar" style="background: #198754;"></span> Set at controller
191
+ </div>
192
+ <div class="d-flex align-items-center gap-2">
193
+ <span class="scope-legend-bar" style="background: #0dcaf0;"></span> Set at device
194
+ </div>
173
195
  </div>
174
196
  </div>
175
197
  </div>
176
- </div>
177
198
 
199
+ </div>
178
200
  </div>
179
- </div>
180
201
 
181
- <!-- Right main: options -->
182
- <div class="col-sm-8">
183
- <!-- Search & filter -->
184
- <div class="d-flex align-items-center gap-2 mb-3">
185
- <div class="input-group flex-grow-1">
186
- <span class="input-group-text"><i class="fas fa-search"></i></span>
187
- <input type="text" class="form-control" placeholder="Search options..." id="optionsSearch">
188
- <button class="btn btn-outline-secondary" type="button" id="clearSearchBtn"><i class="fas fa-times"></i></button>
189
- </div>
190
- <div class="form-check form-switch mb-0 flex-shrink-0" style="min-width: 110px;">
191
- <input class="form-check-input" type="checkbox" id="modifiedOnlyToggle">
192
- <label class="form-check-label text-muted text-nowrap" for="modifiedOnlyToggle"><small>Modified <span id="modifiedSummary"></span></small></label>
202
+ <!-- Right main: options -->
203
+ <div class="col-sm-8">
204
+ <!-- Search & filter -->
205
+ <div class="d-flex align-items-center gap-2 mb-3">
206
+ <div class="input-group flex-grow-1">
207
+ <span class="input-group-text"><i class="bi bi-search"></i></span>
208
+ <input type="text" class="form-control" placeholder="Search options..." id="optionsSearch">
209
+ <button class="btn btn-outline-secondary" type="button" id="clearSearchBtn"><i class="bi bi-x"></i></button>
210
+ </div>
211
+ <div class="form-check form-switch mb-0 flex-shrink-0" style="min-width: 110px;">
212
+ <input class="form-check-input" type="checkbox" id="modifiedOnlyToggle">
213
+ <label class="form-check-label text-muted text-nowrap" for="modifiedOnlyToggle"><small>Modified <span id="modifiedSummary"></span></small></label>
214
+ </div>
193
215
  </div>
194
- </div>
195
216
 
196
- <!-- Loading -->
197
- <div id="optionsLoading" class="text-center my-4">
198
- <div class="spinner-border text-primary" role="status">
199
- <span class="visually-hidden">Loading...</span>
217
+ <!-- Loading -->
218
+ <div id="optionsLoading" class="text-center my-4">
219
+ <div class="spinner-border text-primary" role="status">
220
+ <span class="visually-hidden">Loading...</span>
221
+ </div>
222
+ <p class="mt-2 text-muted">Loading options...</p>
200
223
  </div>
201
- <p class="mt-2 text-muted">Loading options...</p>
202
- </div>
203
224
 
204
- <!-- Options container -->
205
- <div id="optionsContainer" class="mb-3"></div>
225
+ <!-- Options container -->
226
+ <div id="optionsContainer" class="mb-3"></div>
206
227
 
207
- <!-- Unsaved changes notice -->
208
- <div class="alert alert-warning mb-3" id="unsavedChanges" style="display: none;">
209
- <i class="fas fa-exclamation-triangle"></i> You have unsaved changes. Click <strong>Save</strong> in Homebridge to apply.
228
+ <!-- Unsaved changes notice -->
229
+ <div class="alert alert-warning mb-3" id="unsavedChanges" style="display: none;">
230
+ <i class="bi bi-exclamation-triangle"></i> You have unsaved changes. Click <strong>Save</strong> in Homebridge to apply.
231
+ </div>
210
232
  </div>
211
233
  </div>
212
234
  </div>
213
- </div>
214
235
 
215
- <!-- Screen: Support -->
216
- <div id="supportScreen">
217
- <div class="d-flex justify-content-between align-items-center mb-3">
218
- <h5 class="mb-0"><i class="fas fa-life-ring"></i> Support</h5>
219
- <button id="backFromSupportBtn" class="btn btn-secondary btn-sm">
220
- <i class="fas fa-arrow-left"></i> Back
221
- </button>
222
- </div>
236
+ <!-- Screen: Support -->
237
+ <div id="supportScreen">
238
+ <div class="d-flex justify-content-between align-items-center mb-3">
239
+ <h5 class="mb-0"><i class="bi bi-life-preserver"></i> Support</h5>
240
+ <button id="backFromSupportBtn" class="btn btn-secondary btn-sm">
241
+ <i class="bi bi-arrow-left"></i> Back
242
+ </button>
243
+ </div>
223
244
 
224
- <div class="card mb-3">
225
- <div class="card-header bg-transparent"><i class="fas fa-heart"></i> About This Plugin</div>
226
- <div class="card-body">
227
- <p class="card-text mb-0">If you find this plugin useful, please consider <a target="_blank" href="https://github.com/mp-consulting/homebridge-unifi-access">starring the project on GitHub</a>.</p>
245
+ <div class="card mb-3">
246
+ <div class="card-header bg-transparent"><i class="bi bi-heart-fill"></i> About This Plugin</div>
247
+ <div class="card-body">
248
+ <p class="card-text mb-0">If you find this plugin useful, please consider <a target="_blank" href="https://github.com/mp-consulting/homebridge-unifi-access">starring the project on GitHub</a>.</p>
249
+ </div>
228
250
  </div>
229
- </div>
230
251
 
231
- <div class="card mb-3">
232
- <div class="card-header bg-transparent"><i class="fas fa-book"></i> Getting Started</div>
233
- <div class="list-group list-group-flush">
234
- <a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/README.md#installation" target="_blank" class="list-group-item list-group-item-action">
235
- <i class="fas fa-download text-primary"></i> <strong>Installation</strong>
236
- <small class="text-muted d-block">Installing this plugin, including system requirements.</small>
237
- </a>
238
- <a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/docs/feature-options.md" target="_blank" class="list-group-item list-group-item-action">
239
- <i class="fas fa-sliders-h text-primary"></i> <strong>Feature Options</strong>
240
- <small class="text-muted d-block">Granular options to configure at a controller or device level.</small>
241
- </a>
242
- <a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/docs/mqtt.md" target="_blank" class="list-group-item list-group-item-action">
243
- <i class="fas fa-broadcast-tower text-primary"></i> <strong>MQTT</strong>
244
- <small class="text-muted d-block">How to configure MQTT support.</small>
245
- </a>
246
- <a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/CHANGELOG.md" target="_blank" class="list-group-item list-group-item-action">
247
- <i class="fas fa-history text-primary"></i> <strong>Changelog</strong>
248
- <small class="text-muted d-block">Changes and release history.</small>
249
- </a>
252
+ <div class="card mb-3">
253
+ <div class="card-header bg-transparent"><i class="bi bi-book"></i> Getting Started</div>
254
+ <div class="list-group list-group-flush">
255
+ <a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/README.md#installation" target="_blank" class="list-group-item list-group-item-action">
256
+ <i class="bi bi-download text-primary"></i> <strong>Installation</strong>
257
+ <small class="text-muted d-block">Installing this plugin, including system requirements.</small>
258
+ </a>
259
+ <a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/docs/feature-options.md" target="_blank" class="list-group-item list-group-item-action">
260
+ <i class="bi bi-sliders text-primary"></i> <strong>Feature Options</strong>
261
+ <small class="text-muted d-block">Granular options to configure at a controller or device level.</small>
262
+ </a>
263
+ <a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/docs/mqtt.md" target="_blank" class="list-group-item list-group-item-action">
264
+ <i class="bi bi-broadcast text-primary"></i> <strong>MQTT</strong>
265
+ <small class="text-muted d-block">How to configure MQTT support.</small>
266
+ </a>
267
+ <a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/CHANGELOG.md" target="_blank" class="list-group-item list-group-item-action">
268
+ <i class="bi bi-clock-history text-primary"></i> <strong>Changelog</strong>
269
+ <small class="text-muted d-block">Changes and release history.</small>
270
+ </a>
271
+ </div>
250
272
  </div>
251
- </div>
252
273
 
253
- <div class="card mb-3">
254
- <div class="card-header bg-transparent"><i class="fas fa-life-ring"></i> Get Help</div>
255
- <div class="list-group list-group-flush">
256
- <a href="https://discord.gg/QXqfHEW" target="_blank" class="list-group-item list-group-item-action">
257
- <i class="fab fa-discord text-primary"></i> <strong>Discord Support Channel</strong>
258
- </a>
259
- <a href="https://github.com/mp-consulting/homebridge-unifi-access/issues/new/choose" target="_blank" class="list-group-item list-group-item-action">
260
- <i class="fab fa-github text-primary"></i> <strong>Create a Developer Support Request</strong>
261
- </a>
274
+ <div class="card mb-3">
275
+ <div class="card-header bg-transparent"><i class="bi bi-life-preserver"></i> Get Help</div>
276
+ <div class="list-group list-group-flush">
277
+ <a href="https://discord.gg/QXqfHEW" target="_blank" class="list-group-item list-group-item-action">
278
+ <i class="bi bi-discord text-primary"></i> <strong>Discord Support Channel</strong>
279
+ </a>
280
+ <a href="https://github.com/mp-consulting/homebridge-unifi-access/issues/new/choose" target="_blank" class="list-group-item list-group-item-action">
281
+ <i class="bi bi-github text-primary"></i> <strong>Create a Developer Support Request</strong>
282
+ </a>
283
+ </div>
262
284
  </div>
263
285
  </div>
286
+
264
287
  </div>
265
- </div>
266
288
 
267
- <script type="module" src="app.js"></script>
289
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
290
+ <script src="lib/kit.js"></script>
291
+ <script type="module" src="app.js"></script>
292
+ </body>
293
+ </html>
@@ -0,0 +1,253 @@
1
+ /* @mp-consulting/homebridge-ui-kit v1.0.0 */
2
+
3
+ /* ── Design tokens ──────────────────────────────────────────────────────────
4
+ CSS custom properties used throughout the design system.
5
+ Override these in your plugin's styles.css to customize per-plugin.
6
+ ────────────────────────────────────────────────────────────────────────── */
7
+
8
+ :root {
9
+ /* Brand */
10
+ --mp-primary: #4f46e5;
11
+ --mp-primary-rgb: 79, 70, 229;
12
+ --mp-primary-hover: #4338ca;
13
+ --mp-primary-subtle: rgba(79, 70, 229, 0.12);
14
+
15
+ /* Status */
16
+ --mp-status-online: #10b981;
17
+ --mp-status-online-glow: rgba(16, 185, 129, 0.35);
18
+ --mp-status-offline: #ef4444;
19
+ --mp-status-checking: #94a3b8;
20
+
21
+ /* Surfaces */
22
+ --mp-surface: rgba(255, 255, 255, 0.03);
23
+ --mp-border: rgba(255, 255, 255, 0.08);
24
+
25
+ /* Shape */
26
+ --mp-radius: 0.5rem;
27
+ --mp-shadow-hover: 0 0.5rem 1rem rgba(0, 0, 0, 0.3);
28
+ }
29
+
30
+ /* ── Bootstrap 5 overrides ──────────────────────────────────────────────────
31
+ Remap Bootstrap components to use brand tokens.
32
+ ────────────────────────────────────────────────────────────────────────── */
33
+
34
+ /* Primary button */
35
+ .btn-primary {
36
+ --bs-btn-bg: var(--mp-primary);
37
+ --bs-btn-border-color: var(--mp-primary);
38
+ --bs-btn-hover-bg: var(--mp-primary-hover);
39
+ --bs-btn-hover-border-color: var(--mp-primary-hover);
40
+ --bs-btn-active-bg: var(--mp-primary-hover);
41
+ --bs-btn-active-border-color: var(--mp-primary-hover);
42
+ --bs-btn-focus-shadow-rgb: var(--mp-primary-rgb);
43
+ }
44
+
45
+ /* Primary spinner */
46
+ .spinner-border.text-primary,
47
+ .spinner-grow.text-primary {
48
+ color: var(--mp-primary) !important;
49
+ }
50
+
51
+ /* ── Components ─────────────────────────────────────────────────────────────
52
+ Reusable UI components used across all Homebridge plugins.
53
+ ────────────────────────────────────────────────────────────────────────── */
54
+
55
+ /* ── Plugin header ──────────────────────────────────────── */
56
+
57
+ .mp-header {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 0.875rem;
61
+ margin-bottom: 0.5rem;
62
+ }
63
+
64
+ .mp-header-icon {
65
+ font-size: 2rem;
66
+ flex-shrink: 0;
67
+ line-height: 1;
68
+ }
69
+
70
+ .mp-header-title {
71
+ font-size: 1.375rem;
72
+ font-weight: 600;
73
+ margin: 0;
74
+ line-height: 1.3;
75
+ }
76
+
77
+ .mp-header-subtitle {
78
+ font-size: 0.875rem;
79
+ color: var(--bs-secondary-color);
80
+ margin: 0;
81
+ }
82
+
83
+ /* ── Status indicator dot ───────────────────────────────── */
84
+
85
+ .mp-status {
86
+ width: 8px;
87
+ height: 8px;
88
+ border-radius: 50%;
89
+ display: inline-block;
90
+ flex-shrink: 0;
91
+ vertical-align: middle;
92
+ }
93
+
94
+ .mp-status-online {
95
+ background-color: var(--mp-status-online);
96
+ box-shadow: 0 0 5px var(--mp-status-online-glow);
97
+ }
98
+
99
+ .mp-status-offline {
100
+ background-color: var(--mp-status-offline);
101
+ }
102
+
103
+ .mp-status-checking {
104
+ background-color: var(--mp-status-checking);
105
+ animation: mp-pulse 1.5s ease-in-out infinite;
106
+ }
107
+
108
+ @keyframes mp-pulse {
109
+ 0%, 100% { opacity: 1; }
110
+ 50% { opacity: 0.35; }
111
+ }
112
+
113
+ /* ── Device card ────────────────────────────────────────── */
114
+
115
+ .mp-device-card {
116
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
117
+ cursor: pointer;
118
+ }
119
+
120
+ .mp-device-card:hover {
121
+ transform: translateY(-1px);
122
+ box-shadow: var(--mp-shadow-hover) !important;
123
+ }
124
+
125
+ /* ── Settings surface card ──────────────────────────────── */
126
+
127
+ .mp-settings-card {
128
+ background: var(--mp-surface);
129
+ border: 1px solid var(--mp-border) !important;
130
+ border-radius: var(--mp-radius);
131
+ }
132
+
133
+ /* ── Empty state ────────────────────────────────────────── */
134
+
135
+ .mp-empty-state {
136
+ display: flex;
137
+ flex-direction: column;
138
+ align-items: center;
139
+ justify-content: center;
140
+ padding: 3rem 1.5rem;
141
+ text-align: center;
142
+ }
143
+
144
+ .mp-empty-state-icon {
145
+ font-size: 2.5rem;
146
+ color: var(--bs-secondary-color);
147
+ margin-bottom: 0.75rem;
148
+ line-height: 1;
149
+ }
150
+
151
+ .mp-empty-state-title {
152
+ font-size: 0.9375rem;
153
+ font-weight: 500;
154
+ color: var(--bs-secondary-color);
155
+ margin: 0 0 0.25rem;
156
+ }
157
+
158
+ .mp-empty-state-hint {
159
+ font-size: 0.8125rem;
160
+ color: var(--bs-secondary-color);
161
+ margin: 0;
162
+ opacity: 0.7;
163
+ }
164
+
165
+ /* ── Loading state ──────────────────────────────────────── */
166
+
167
+ .mp-loading {
168
+ display: flex;
169
+ flex-direction: column;
170
+ align-items: center;
171
+ justify-content: center;
172
+ padding: 2.5rem 1.5rem;
173
+ gap: 0.75rem;
174
+ color: var(--bs-secondary-color);
175
+ font-size: 0.875rem;
176
+ }
177
+
178
+ /* ── Nav tabs ───────────────────────────────────────────── */
179
+
180
+ .mp-tabs {
181
+ border-bottom: 1px solid var(--mp-border);
182
+ }
183
+
184
+ .mp-tabs .nav-link {
185
+ color: var(--bs-secondary-color);
186
+ border: none;
187
+ padding: 0.75rem 1.25rem;
188
+ font-weight: 500;
189
+ }
190
+
191
+ .mp-tabs .nav-link:hover {
192
+ color: var(--bs-body-color);
193
+ border: none;
194
+ }
195
+
196
+ .mp-tabs .nav-link.active {
197
+ color: var(--mp-primary);
198
+ background: transparent;
199
+ border: none;
200
+ border-bottom: 2px solid var(--mp-primary);
201
+ }
202
+
203
+ /* ── Metadata label ─────────────────────────────────────── */
204
+
205
+ .mp-label {
206
+ font-size: 0.75rem;
207
+ text-transform: uppercase;
208
+ letter-spacing: 0.5px;
209
+ color: var(--bs-secondary-color);
210
+ }
211
+
212
+ /* ── View toggling ──────────────────────────────────────── */
213
+
214
+ .mp-view {
215
+ display: none;
216
+ }
217
+
218
+ .mp-view.active {
219
+ display: block;
220
+ }
221
+
222
+ /* ── Support footer ─────────────────────────────────────── */
223
+
224
+ .mp-footer {
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ flex-wrap: wrap;
229
+ gap: 0.75rem 1rem;
230
+ padding-top: 1.5rem;
231
+ margin-top: 2rem;
232
+ border-top: 1px solid var(--mp-border);
233
+ font-size: 0.8125rem;
234
+ color: var(--bs-secondary-color);
235
+ }
236
+
237
+ .mp-footer a {
238
+ color: var(--bs-secondary-color);
239
+ text-decoration: none;
240
+ display: inline-flex;
241
+ align-items: center;
242
+ gap: 0.3rem;
243
+ transition: color 0.15s ease;
244
+ }
245
+
246
+ .mp-footer a:hover {
247
+ color: var(--mp-primary);
248
+ }
249
+
250
+ .mp-footer-sep {
251
+ color: var(--mp-border);
252
+ user-select: none;
253
+ }
@@ -0,0 +1,133 @@
1
+ /* @mp-consulting/homebridge-ui-kit v1.0.0
2
+ Brand design system for Homebridge plugins
3
+ https://github.com/mp-consulting/homebridge-ui-kit */
4
+
5
+ (function (global) {
6
+ 'use strict';
7
+
8
+ var MpKit = {
9
+
10
+ /**
11
+ * StatusBadge — returns inline HTML for device status badges.
12
+ *
13
+ * MpKit.StatusBadge.online() → green dot + "Online"
14
+ * MpKit.StatusBadge.offline() → red dot + "Offline"
15
+ * MpKit.StatusBadge.checking() → animated dot + "Checking..."
16
+ * MpKit.StatusBadge.disabled() → grey badge + "Disabled"
17
+ */
18
+ StatusBadge: {
19
+ online: function (label) {
20
+ label = label || 'Online';
21
+ return '<span class="badge bg-success-subtle text-success">'
22
+ + '<span class="mp-status mp-status-online me-1"></span>'
23
+ + label + '</span>';
24
+ },
25
+ offline: function (label) {
26
+ label = label || 'Offline';
27
+ return '<span class="badge bg-danger-subtle text-danger">'
28
+ + '<span class="mp-status mp-status-offline me-1"></span>'
29
+ + label + '</span>';
30
+ },
31
+ checking: function (label) {
32
+ label = label || 'Checking...';
33
+ return '<span class="badge bg-secondary-subtle text-secondary">'
34
+ + '<span class="mp-status mp-status-checking me-1"></span>'
35
+ + label + '</span>';
36
+ },
37
+ disabled: function (label) {
38
+ label = label || 'Disabled';
39
+ return '<span class="badge bg-secondary">' + label + '</span>';
40
+ },
41
+ },
42
+
43
+ /**
44
+ * EmptyState — returns HTML for an empty list placeholder.
45
+ *
46
+ * MpKit.EmptyState.render({
47
+ * iconClass: 'bi bi-lightbulb',
48
+ * title: 'No devices found',
49
+ * hint: 'Click Discover to scan your network',
50
+ * })
51
+ */
52
+ EmptyState: {
53
+ render: function (opts) {
54
+ opts = opts || {};
55
+ var icon = opts.iconClass || 'bi bi-inbox';
56
+ var title = opts.title || 'No items';
57
+ var hint = opts.hint || '';
58
+ return '<div class="mp-empty-state">'
59
+ + '<i class="' + icon + ' mp-empty-state-icon"></i>'
60
+ + '<p class="mp-empty-state-title">' + title + '</p>'
61
+ + (hint ? '<p class="mp-empty-state-hint">' + hint + '</p>' : '')
62
+ + '</div>';
63
+ },
64
+ },
65
+
66
+ /**
67
+ * Loading — returns HTML for a centred loading spinner with message.
68
+ *
69
+ * MpKit.Loading.render('Loading device information...')
70
+ */
71
+ Loading: {
72
+ render: function (message) {
73
+ message = message || 'Loading...';
74
+ return '<div class="mp-loading">'
75
+ + '<div class="spinner-border spinner-border-sm text-secondary" role="status" aria-hidden="true"></div>'
76
+ + '<span>' + message + '</span>'
77
+ + '</div>';
78
+ },
79
+ },
80
+
81
+ /**
82
+ * View — controls which .mp-view element is visible.
83
+ *
84
+ * MpKit.View.show('listView') → shows #listView, hides all others
85
+ * MpKit.View.show('settingsView') → shows #settingsView, hides all others
86
+ */
87
+ View: {
88
+ show: function (id) {
89
+ document.querySelectorAll('.mp-view').forEach(function (v) {
90
+ v.classList.remove('active');
91
+ });
92
+ var el = document.getElementById(id);
93
+ if (el) { el.classList.add('active'); }
94
+ },
95
+ },
96
+
97
+ /**
98
+ * Footer — renders support links into a .mp-footer element.
99
+ *
100
+ * MpKit.Footer.render({
101
+ * github: 'https://github.com/mp-consulting/homebridge-...',
102
+ * npm: 'https://www.npmjs.com/package/@mp-consulting/...',
103
+ * changelog: 'https://github.com/.../blob/main/CHANGELOG.md',
104
+ * })
105
+ */
106
+ Footer: {
107
+ render: function (opts) {
108
+ opts = opts || {};
109
+ var target = opts.target || '.mp-footer';
110
+ var el = typeof target === 'string' ? document.querySelector(target) : target;
111
+ if (!el) { return; }
112
+ var links = [];
113
+ if (opts.github) {
114
+ links.push('<a href="' + opts.github + '" target="_blank" rel="noopener">'
115
+ + '<i class="bi bi-github"></i>GitHub</a>');
116
+ }
117
+ if (opts.npm) {
118
+ links.push('<a href="' + opts.npm + '" target="_blank" rel="noopener">'
119
+ + '<i class="bi bi-box-seam"></i>npm</a>');
120
+ }
121
+ if (opts.changelog) {
122
+ links.push('<a href="' + opts.changelog + '" target="_blank" rel="noopener">'
123
+ + '<i class="bi bi-clock-history"></i>Changelog</a>');
124
+ }
125
+ el.innerHTML = links.join('<span class="mp-footer-sep">|</span>');
126
+ },
127
+ },
128
+
129
+ };
130
+
131
+ global.MpKit = MpKit;
132
+
133
+ })(window);
@@ -2,13 +2,11 @@ export const SCREENS = [ 'discoveryScreen', 'setupScreen', 'controllersScreen',
2
2
  export const PLUGIN_NAME = 'UniFi Access';
3
3
 
4
4
  export const CATEGORY_ICONS = {
5
-
6
-
7
- AccessMethod: 'fa-fingerprint',
8
- Controller: 'fa-server',
9
- Device: 'fa-microchip',
10
- Hub: 'fa-door-open',
11
- Log: 'fa-file-alt',
5
+ AccessMethod: 'fingerprint',
6
+ Controller: 'server',
7
+ Device: 'cpu',
8
+ Hub: 'door-open',
9
+ Log: 'file-text',
12
10
  };
13
11
 
14
12
  export const CATEGORY_COLORS = {
@@ -33,15 +33,17 @@ export const renderControllers = () => {
33
33
  <div class="d-flex w-100 justify-content-between align-items-center">
34
34
  <div>
35
35
  <h6 class="mb-1">
36
- <i class="fas fa-server mr-2"></i> ${escapeHtml(ctrl.name || ctrl.address)}
37
- <span class="status-badge badge rounded-pill bg-secondary ms-2" style="font-size: 0.65rem;"><i class="fas fa-circle-notch fa-spin"></i></span>
36
+ <i class="bi bi-server me-2"></i> ${escapeHtml(ctrl.name || ctrl.address)}
37
+ <span class="status-badge badge rounded-pill bg-secondary ms-2" style="font-size: 0.65rem;">
38
+ <span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
39
+ </span>
38
40
  </h6>
39
- <small class="text-muted"><i class="fas fa-network-wired mr-1"></i> ${escapeHtml(ctrl.address)}</small>
41
+ <small class="text-muted"><i class="bi bi-hdd-network me-1"></i> ${escapeHtml(ctrl.address)}</small>
40
42
  </div>
41
43
  <div class="d-flex gap-1">
42
- <button class="btn btn-sm btn-primary feature-options-btn"><i class="fas fa-sliders-h"></i> Options</button>
43
- <button class="btn btn-sm btn-secondary edit-ctrl-btn"><i class="fas fa-edit"></i> Edit</button>
44
- <button class="btn btn-sm btn-danger delete-ctrl-btn"><i class="fas fa-trash"></i></button>
44
+ <button class="btn btn-sm btn-primary feature-options-btn"><i class="bi bi-sliders"></i> Options</button>
45
+ <button class="btn btn-sm btn-secondary edit-ctrl-btn"><i class="bi bi-pencil"></i> Edit</button>
46
+ <button class="btn btn-sm btn-danger delete-ctrl-btn"><i class="bi bi-trash"></i></button>
45
47
  </div>
46
48
  </div>
47
49
  `;
@@ -70,13 +72,13 @@ export const renderControllers = () => {
70
72
  badge.className = 'status-badge badge rounded-pill bg-' + colorClass + ' ms-2';
71
73
  badge.style.fontSize = '0.65rem';
72
74
  badge.textContent = '';
73
- badge.appendChild(el('i', { className: 'fas fa-' + icon }));
75
+ badge.appendChild(el('i', { className: 'bi bi-' + icon }));
74
76
  badge.appendChild(document.createTextNode(' ' + label));
75
77
  };
76
78
 
77
79
  homebridge.request('/checkStatus', { address: ctrl.address }).then((result) => {
78
80
 
79
- updateBadge(result?.online ? 'success' : 'danger', result?.online ? 'check-circle' : 'times-circle', result?.online ? 'Online' : 'Offline');
81
+ updateBadge(result?.online ? 'success' : 'danger', result?.online ? 'check-circle' : 'x-circle', result?.online ? 'Online' : 'Offline');
80
82
  }).catch(() => {
81
83
 
82
84
  updateBadge('warning', 'question-circle', 'Unknown');
@@ -53,7 +53,7 @@ export const handleDiscover = async () => {
53
53
 
54
54
 
55
55
  btn.disabled = false;
56
- btnText.innerHTML = '<i class="fas fa-search"></i> Discover Controllers';
56
+ btnText.innerHTML = '<i class="bi bi-search"></i> Discover Controllers';
57
57
  spinner.style.display = 'none';
58
58
  }
59
59
  };
@@ -69,10 +69,10 @@ const createDiscoveredDeviceItem = (device) => {
69
69
  li.innerHTML = `
70
70
  <div class="d-flex w-100 justify-content-between align-items-center">
71
71
  <div>
72
- <h6 class="mb-1"><i class="fas fa-server mr-2"></i> ${escapeHtml(device.name)}</h6>
73
- <small class="text-muted"><i class="fas fa-network-wired mr-1"></i> ${escapeHtml(device.ip)}</small>
72
+ <h6 class="mb-1"><i class="bi bi-server me-2"></i> ${escapeHtml(device.name)}</h6>
73
+ <small class="text-muted"><i class="bi bi-hdd-network me-1"></i> ${escapeHtml(device.ip)}</small>
74
74
  ${device.model ? `<span class="badge bg-secondary ms-2">${escapeHtml(device.model)}</span>` : ''}
75
- ${device.mac ? `<small class="text-muted ms-2"><i class="fas fa-barcode mr-1"></i> ${escapeHtml(device.mac)}</small>` : ''}
75
+ ${device.mac ? `<small class="text-muted ms-2"><i class="bi bi-upc me-1"></i> ${escapeHtml(device.mac)}</small>` : ''}
76
76
  </div>
77
77
  <span class="badge bg-primary rounded-pill">Select</span>
78
78
  </div>
@@ -21,7 +21,7 @@ export const setButtonLoading = (btn, loading, loadingText = 'Loading...') => {
21
21
  btn.dataset.originalContent = btn.innerHTML;
22
22
  btn.disabled = true;
23
23
 
24
- btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${loadingText}`;
24
+ btn.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> ${loadingText}`;
25
25
  } else {
26
26
 
27
27
 
@@ -188,7 +188,7 @@ export const renderOptions = () => {
188
188
  }
189
189
  }
190
190
 
191
- const icon = CATEGORY_ICONS[category.name] || 'fa-cog';
191
+ const icon = CATEGORY_ICONS[category.name] || 'gear';
192
192
  const color = CATEGORY_COLORS[category.name] || '#6c757d';
193
193
  const isOpen = state.openCategories.has(category.name) || ((modifiedCount > 0) && !state.openCategories.size) || !!searchTerm;
194
194
 
@@ -209,7 +209,7 @@ export const renderOptions = () => {
209
209
 
210
210
  header.innerHTML = `
211
211
  <div class="d-flex align-items-center">
212
- <span class="category-icon" style="background-color: ${color}"><i class="fas ${icon}"></i></span>
212
+ <span class="category-icon" style="background-color: ${color}"><i class="bi bi-${icon}"></i></span>
213
213
  <span class="ms-2">${escapeHtml(category.description.replace(/ feature options\.?/i, ''))}</span>
214
214
  </div>
215
215
  <div class="d-flex align-items-center gap-2">
@@ -218,7 +218,7 @@ export const renderOptions = () => {
218
218
  <span class="category-progress"><span class="category-progress-fill" style="width: ${progressPct}%"></span></span>
219
219
  </span>
220
220
  ${modifiedCount ? `<span class="badge bg-warning text-dark">${modifiedCount}</span>` : ''}
221
- <i class="fas fa-chevron-${isOpen ? 'up' : 'down'} toggle-icon"></i>
221
+ <i class="bi bi-chevron-${isOpen ? 'up' : 'down'} toggle-icon"></i>
222
222
  </div>
223
223
  `;
224
224
 
@@ -252,7 +252,7 @@ export const renderOptions = () => {
252
252
  const wasOpen = body.classList.contains('open');
253
253
 
254
254
  body.classList.toggle('open');
255
- header.querySelector('.toggle-icon').className = 'fas fa-chevron-' + (wasOpen ? 'down' : 'up') + ' toggle-icon';
255
+ header.querySelector('.toggle-icon').className = 'bi bi-chevron-' + (wasOpen ? 'down' : 'up') + ' toggle-icon';
256
256
 
257
257
  if(wasOpen) {
258
258
 
@@ -319,11 +319,11 @@ const createOptionItem = (optionKey, opt, scope, category) => {
319
319
  } else if(optState.scope === 'controller') {
320
320
 
321
321
 
322
- scopeIndicator = '<span class="badge bg-success bg-opacity-50 ms-1"><i class="fas fa-arrow-down" style="font-size:0.55rem"></i> controller</span>';
322
+ scopeIndicator = '<span class="badge bg-success bg-opacity-50 ms-1"><i class="bi bi-arrow-down" style="font-size:0.55rem"></i> controller</span>';
323
323
  } else if((optState.scope === 'global') && (scope.type !== 'global')) {
324
324
 
325
325
 
326
- scopeIndicator = '<span class="badge bg-secondary bg-opacity-50 ms-1"><i class="fas fa-arrow-down" style="font-size:0.55rem"></i> global</span>';
326
+ scopeIndicator = '<span class="badge bg-secondary bg-opacity-50 ms-1"><i class="bi bi-arrow-down" style="font-size:0.55rem"></i> global</span>';
327
327
  }
328
328
 
329
329
  // Display name: use option name or category name for the unnamed device option.
@@ -336,7 +336,7 @@ const createOptionItem = (optionKey, opt, scope, category) => {
336
336
 
337
337
  const resetButton = optState.explicit ?
338
338
  '<button class="btn btn-outline-secondary btn-sm reset-option-btn flex-shrink-0 mt-1" title="Reset to inherited value">' +
339
- '<i class="fas fa-undo"></i></button>' :
339
+ '<i class="bi bi-arrow-counterclockwise"></i></button>' :
340
340
  '';
341
341
 
342
342
  // Value input for options with configurable numeric values.
@@ -419,66 +419,3 @@
419
419
  font-size: 0.85rem;
420
420
  }
421
421
 
422
- /* ============================================================================
423
- DARK MODE SUPPORT
424
- ============================================================================ */
425
-
426
- @media (prefers-color-scheme: dark) {
427
- :root {
428
- --muted-color: #a0a0a0;
429
- --card-bg: rgba(255, 255, 255, 0.05);
430
- --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
431
- --card-hover-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
432
- --card-header-bg: rgba(255, 255, 255, 0.05);
433
- --card-header-border: rgba(255, 255, 255, 0.1);
434
- --card-header-hover-bg: rgba(255, 255, 255, 0.08);
435
- --list-item-border: rgba(255, 255, 255, 0.08);
436
- --progress-bg: rgba(255, 255, 255, 0.08);
437
- --scope-device-bg: rgba(13, 202, 240, 0.08);
438
- --scope-controller-bg: rgba(25, 135, 84, 0.08);
439
- --scope-global-bg: rgba(255, 193, 7, 0.08);
440
- --grouped-bar: rgba(255, 255, 255, 0.1);
441
- --default-on-bg: rgba(25, 135, 84, 0.2);
442
- --default-off-bg: rgba(108, 117, 125, 0.2);
443
- --scope-hint-color: #a0a0a0;
444
- --connector-line: rgba(255, 255, 255, 0.1);
445
- --connector-active: rgba(255, 255, 255, 0.3);
446
- }
447
-
448
- .text-muted {
449
- color: #a0a0a0 !important;
450
- }
451
- }
452
-
453
- [data-theme="dark"],
454
- .dark-mode {
455
- --muted-color: #a0a0a0;
456
- --card-bg: rgba(255, 255, 255, 0.05);
457
- --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
458
- --card-hover-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
459
- --card-header-bg: rgba(255, 255, 255, 0.05);
460
- --card-header-border: rgba(255, 255, 255, 0.1);
461
- --card-header-hover-bg: rgba(255, 255, 255, 0.08);
462
- --list-item-border: rgba(255, 255, 255, 0.08);
463
- --progress-bg: rgba(255, 255, 255, 0.08);
464
- --scope-device-bg: rgba(13, 202, 240, 0.08);
465
- --scope-controller-bg: rgba(25, 135, 84, 0.08);
466
- --scope-global-bg: rgba(255, 193, 7, 0.08);
467
- --grouped-bar: rgba(255, 255, 255, 0.1);
468
- --default-on-bg: rgba(25, 135, 84, 0.2);
469
- --default-off-bg: rgba(108, 117, 125, 0.2);
470
- --scope-hint-color: #a0a0a0;
471
- --connector-line: rgba(255, 255, 255, 0.1);
472
- --connector-active: rgba(255, 255, 255, 0.3);
473
- }
474
-
475
- [data-theme="dark"] .text-muted,
476
- .dark-mode .text-muted {
477
- color: #a0a0a0 !important;
478
- }
479
-
480
- [data-theme="dark"] .badge.bg-secondary,
481
- .dark-mode .badge.bg-secondary {
482
- background-color: #6c757d !important;
483
- color: #fff !important;
484
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mp-consulting/homebridge-unifi-access",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "displayName": "UniFi Access",
5
5
  "description": "Complete HomeKit support for UniFi Access devices — locks, doorbells, sensors, and readers — with automatic device discovery and realtime events.",
6
6
  "author": "Mickael Palma",
@@ -46,7 +46,8 @@
46
46
  ],
47
47
  "scripts": {
48
48
  "prebuild": "npm run clean",
49
- "build": "tsc",
49
+ "copy:ui-kit": "mkdir -p homebridge-ui/public/lib && cp node_modules/@mp-consulting/homebridge-ui-kit/dist/kit.css homebridge-ui/public/lib/ && cp node_modules/@mp-consulting/homebridge-ui-kit/dist/kit.js homebridge-ui/public/lib/",
50
+ "build": "npm run copy:ui-kit && tsc",
50
51
  "clean": "shx rm -rf dist",
51
52
  "lint": "eslint . --max-warnings=0",
52
53
  "postpublish": "npm run clean",
@@ -71,11 +72,12 @@
71
72
  },
72
73
  "devDependencies": {
73
74
  "@eslint/js": "^9.39.2",
75
+ "@mp-consulting/homebridge-ui-kit": "^1.0.0",
74
76
  "@types/node": "^24.0.0",
75
77
  "@vitest/coverage-v8": "^4.0.18",
76
78
  "eslint": "^9.39.2",
77
79
  "homebridge": "^2.0.0-beta.55",
78
- "homebridge-config-ui-x": "^5.15.0",
80
+ "homebridge-config-ui-x": "^5.19.0",
79
81
  "nodemon": "^3.1.11",
80
82
  "shx": "0.4.0",
81
83
  "typescript": "^5.9.3",