homebridge-plugin-utils 1.27.0 → 1.27.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/dist/ui/webUi-featureoptions.mjs +284 -194
- package/package.json +2 -2
|
@@ -809,6 +809,9 @@ export class webUiFeatureOptions {
|
|
|
809
809
|
// Ensure the DOM is ready before we render our UI. We wait for Bootstrap styles to be applied before proceeding.
|
|
810
810
|
await this.#waitForBootstrap();
|
|
811
811
|
|
|
812
|
+
// Initialize theme sync before injecting styles so CSS variables are defined and current.
|
|
813
|
+
await this.#setupThemeAutoUpdate();
|
|
814
|
+
|
|
812
815
|
// Add our custom styles for hover effects, dark mode support, and modern layouts. These enhance the visual experience and ensure consistency with the
|
|
813
816
|
// Homebridge UI theme.
|
|
814
817
|
this.#injectCustomStyles();
|
|
@@ -1061,198 +1064,6 @@ export class webUiFeatureOptions {
|
|
|
1061
1064
|
}
|
|
1062
1065
|
}
|
|
1063
1066
|
|
|
1064
|
-
/**
|
|
1065
|
-
* Inject custom styles for hover effects, dark mode support, and modern layouts.
|
|
1066
|
-
*
|
|
1067
|
-
* These styles enhance the visual experience and ensure our UI integrates well with both light and dark modes. We use media queries to automatically adapt
|
|
1068
|
-
* to the user's system preferences. The styles include support for flexbox layouts, responsive design, and theme-aware coloring.
|
|
1069
|
-
*
|
|
1070
|
-
* @private
|
|
1071
|
-
*/
|
|
1072
|
-
#injectCustomStyles() {
|
|
1073
|
-
|
|
1074
|
-
// Ensure we do not inject duplicate styles when re-entering this view. We make this idempotent for stability across navigations.
|
|
1075
|
-
if(document.getElementById("feature-options-styles")) {
|
|
1076
|
-
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Extract our theme color from .btn-primary to ensure consistency with the Homebridge theme.
|
|
1081
|
-
const probeBtn = document.createElement("button");
|
|
1082
|
-
|
|
1083
|
-
probeBtn.className = "btn btn-primary";
|
|
1084
|
-
probeBtn.style.display = "none";
|
|
1085
|
-
document.body.appendChild(probeBtn);
|
|
1086
|
-
|
|
1087
|
-
this.#themeColor.background = getComputedStyle(probeBtn).backgroundColor;
|
|
1088
|
-
this.#themeColor.text = getComputedStyle(probeBtn).color;
|
|
1089
|
-
|
|
1090
|
-
document.body.removeChild(probeBtn);
|
|
1091
|
-
|
|
1092
|
-
// Quick utility to help us convert RGB values to RGBA for use in CSS.
|
|
1093
|
-
const rgba = (rgb, alpha) => {
|
|
1094
|
-
|
|
1095
|
-
const match = rgb.match(/\d+/g);
|
|
1096
|
-
|
|
1097
|
-
if(!match || (match.length < 3)) {
|
|
1098
|
-
|
|
1099
|
-
return rgb;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
return "rgba(" + match[0] + ", " + match[1] + ", " + match[2] + ", " + alpha + ")";
|
|
1103
|
-
};
|
|
1104
|
-
|
|
1105
|
-
const styles = [
|
|
1106
|
-
|
|
1107
|
-
/* eslint-disable @stylistic/max-len */
|
|
1108
|
-
// Remove margin collapse and enable clean layout flow.
|
|
1109
|
-
"html, body { margin: 0; padding: 0; }",
|
|
1110
|
-
|
|
1111
|
-
// Compensate for misbehavior in Homebridge Config UI X when switching to or from dark mode.
|
|
1112
|
-
"body { background-color: #fff !important; }",
|
|
1113
|
-
|
|
1114
|
-
// Page root uses a column layout with full width.
|
|
1115
|
-
"#pageFeatureOptions { display: flex !important; flex-direction: column; width: 100%; }",
|
|
1116
|
-
|
|
1117
|
-
// Sidebar + content layout is horizontal (row).
|
|
1118
|
-
".feature-main-content { display: flex !important; flex-direction: row !important; width: 100%; }",
|
|
1119
|
-
|
|
1120
|
-
// Sidebar layout and appearance.
|
|
1121
|
-
"#sidebar { display: block; width: 200px; min-width: 200px; max-width: 200px; background-color: var(--bs-gray-100); position: relative; }",
|
|
1122
|
-
|
|
1123
|
-
// Remove internal scrolling from sidebar content.
|
|
1124
|
-
"#sidebar .sidebar-content { padding: 0rem; overflow: unset; }",
|
|
1125
|
-
|
|
1126
|
-
// Sidebar containers.
|
|
1127
|
-
"#controllersContainer { padding: 0; margin-bottom: 0; }",
|
|
1128
|
-
"#devicesContainer { padding: 0; margin-top: 0; padding-top: 0 !important; }",
|
|
1129
|
-
|
|
1130
|
-
// Feature content (right-hand pane).
|
|
1131
|
-
".feature-content { display: flex !important; flex-direction: column !important; flex: 1 1 auto; min-width: 0; }",
|
|
1132
|
-
".category-border { border: 1px solid " + this.#themeColor.background + " !important; box-shadow: 0 0 0 1px " + rgba(this.#themeColor.background, 0.1) + "; }",
|
|
1133
|
-
|
|
1134
|
-
// Ensure the table itself uses separate borders when we have rounded tbody elements. This is necessary for border-radius to work properly.
|
|
1135
|
-
"table[data-category] { border-collapse: separate !important; border-spacing: 0; }",
|
|
1136
|
-
|
|
1137
|
-
// How we define row visibility for feature options. We need this complexity because we hide or make visible rows depending on what the user has chosen to expose.
|
|
1138
|
-
//
|
|
1139
|
-
// "table[data-category] tbody tr.fo-visible,"
|
|
1140
|
-
// "table[data-category] tbody tr:not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none']){}",
|
|
1141
|
-
|
|
1142
|
-
// Create the outer border of the table on the left and right sides.
|
|
1143
|
-
"table[data-category] tbody tr:not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none']) td:first-child{",
|
|
1144
|
-
" border-left:1px solid " + this.#themeColor.background + ";",
|
|
1145
|
-
"}",
|
|
1146
|
-
"table[data-category] tbody tr:not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none']) td:last-child{",
|
|
1147
|
-
" border-right:1px solid " + this.#themeColor.background + ";",
|
|
1148
|
-
"}",
|
|
1149
|
-
|
|
1150
|
-
// Provide the top border on the first visible row.
|
|
1151
|
-
"table[data-category] tbody tr:nth-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td{",
|
|
1152
|
-
" border-top:1px solid " + this.#themeColor.background + ";",
|
|
1153
|
-
"}",
|
|
1154
|
-
|
|
1155
|
-
// Provide the bottom border on the last visible row.
|
|
1156
|
-
"table[data-category] tbody tr:nth-last-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td{",
|
|
1157
|
-
" border-bottom:1px solid " + this.#themeColor.background + ";",
|
|
1158
|
-
"}",
|
|
1159
|
-
|
|
1160
|
-
// Create rounded corners at the top and bottom rows.
|
|
1161
|
-
"table[data-category] tbody tr:nth-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td:first-child{",
|
|
1162
|
-
" border-top-left-radius:.5rem;",
|
|
1163
|
-
"}",
|
|
1164
|
-
"table[data-category] tbody tr:nth-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td:last-child{",
|
|
1165
|
-
" border-top-right-radius:.5rem;",
|
|
1166
|
-
"}",
|
|
1167
|
-
"table[data-category] tbody tr:nth-last-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td:first-child{",
|
|
1168
|
-
" border-bottom-left-radius:.5rem;",
|
|
1169
|
-
"}",
|
|
1170
|
-
"table[data-category] tbody tr:nth-last-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td:last-child{",
|
|
1171
|
-
" border-bottom-right-radius:.5rem;",
|
|
1172
|
-
"}",
|
|
1173
|
-
|
|
1174
|
-
// Main options area - remove scroll behavior, just layout styling.
|
|
1175
|
-
".options-content { padding: 1rem; margin: 0; }",
|
|
1176
|
-
|
|
1177
|
-
// Info header styling.
|
|
1178
|
-
"#headerInfo { flex-shrink: 0; padding: 0.5rem !important; margin-bottom: 0.5rem !important; }",
|
|
1179
|
-
|
|
1180
|
-
// Device stats grid layout.
|
|
1181
|
-
".device-stats-grid { display: flex; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.5rem; padding: 0 0.75rem; flex-wrap: nowrap; overflow: hidden; }",
|
|
1182
|
-
".device-stats-grid .stat-item:first-child { flex: 0 0 25% }",
|
|
1183
|
-
".device-stats-grid .stat-item:not(:first-child) { flex-grow: 1; min-width: 0; }",
|
|
1184
|
-
|
|
1185
|
-
".stat-item { display: flex; flex-direction: column; gap: 0.125rem; }",
|
|
1186
|
-
".stat-label { font-weight: 600; color: var(--bs-gray-600); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
|
|
1187
|
-
".stat-value { font-size: 0.875rem; color: var(--bs-body-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
|
|
1188
|
-
|
|
1189
|
-
// Responsive hiding for our device stats grid.
|
|
1190
|
-
"@media (max-width: 700px) { .device-stats-grid .stat-item:nth-last-of-type(1) { display: none !important; } }",
|
|
1191
|
-
"@media (max-width: 500px) { .device-stats-grid .stat-item:nth-last-of-type(2) { display: none !important; } }",
|
|
1192
|
-
"@media (max-width: 300px) { .device-stats-grid .stat-item:nth-last-of-type(3) { display: none !important; } }",
|
|
1193
|
-
|
|
1194
|
-
// Responsive hiding for feature option status information.
|
|
1195
|
-
"@media (max-width: 400px) { #statusInfo { display: none !important; } }",
|
|
1196
|
-
|
|
1197
|
-
// Navigation styles.
|
|
1198
|
-
".nav-link { border-radius: 0.375rem; transition: all 0.2s; position: relative; padding: 0.25rem 0.75rem !important; line-height: 1.2; font-size: 0.8125rem; }",
|
|
1199
|
-
".nav-link:hover { background-color: " + rgba(this.#themeColor.background, 0.1) + "; color: " + this.#themeColor.background + " !important; }",
|
|
1200
|
-
".nav-link.active { background-color: " + this.#themeColor.background + "; color: " + this.#themeColor.text + " !important; }",
|
|
1201
|
-
".nav-header { border-bottom: 1px solid rgba(0, 0, 0, 0.1); margin-bottom: 0.125rem; padding: 0.25rem 0.75rem !important; font-size: 0.75rem !important; line-height: 1.2; }",
|
|
1202
|
-
"#devicesContainer .nav-header { font-weight: 600; margin-top: 0 !important; padding-top: 0.5rem !important; }",
|
|
1203
|
-
"#controllersContainer .nav-header { font-weight: 600; margin-top: 0 !important; padding-top: 0.5rem !important; }",
|
|
1204
|
-
|
|
1205
|
-
// Search bar.
|
|
1206
|
-
".search-toolbar { border-radius: 0.5rem; padding: 0 0 0.5rem 0; }",
|
|
1207
|
-
".search-input-wrapper { min-width: 0; }",
|
|
1208
|
-
".filter-pills { display: flex; gap: 0.5rem; flex-wrap: wrap; }",
|
|
1209
|
-
|
|
1210
|
-
// Grouped option visual indicator.
|
|
1211
|
-
".grouped-option { background-color: " + rgba(this.#themeColor.background, 0.08) + "; }",
|
|
1212
|
-
".grouped-option td:nth-child(2) label { padding-left: 20px; position: relative; }",
|
|
1213
|
-
".grouped-option td:nth-child(2) label::before { content: \"\\21B3\"; position: absolute; left: 4px; color: #666; }",
|
|
1214
|
-
|
|
1215
|
-
// Dark mode refinements.
|
|
1216
|
-
"@media (prefers-color-scheme: dark) {",
|
|
1217
|
-
|
|
1218
|
-
// Compensate for misbehavior in Homebridge Config UI X when switching to or from dark mode.
|
|
1219
|
-
" body { background-color: #242424 !important; }",
|
|
1220
|
-
" #sidebar { background-color: #1A1A1A !important; }",
|
|
1221
|
-
" .nav-header { border-bottom-color: rgba(255, 255, 255, 0.1); }",
|
|
1222
|
-
" .text-body { color: #999 !important; }",
|
|
1223
|
-
" .text-muted { color: #999 !important; }",
|
|
1224
|
-
" .device-stats-grid { background-color: #1A1A1A; border-color: #444; }",
|
|
1225
|
-
" .stat-label { color: #999; }",
|
|
1226
|
-
" .stat-value { color: #999; }",
|
|
1227
|
-
" #search .form-control { background-color: #1A1A1A; border-color: #444; color: #F8F9FA; }",
|
|
1228
|
-
" #search .form-control:focus { background-color: #1A1A1A; border-color: #666; color: #F8F9FA; box-shadow: 0 0 0 0.2rem rgba(255, 160, 0, 0.25); }",
|
|
1229
|
-
" #search .form-control::placeholder { color: #999; }",
|
|
1230
|
-
" #statusInfo .text-muted { color: #B8B8B8 !important; }",
|
|
1231
|
-
"}",
|
|
1232
|
-
|
|
1233
|
-
// Table hover styling.
|
|
1234
|
-
".table-hover tbody tr { transition: background-color 0.15s; }",
|
|
1235
|
-
".table-hover tbody tr:hover { background-color: rgba(0, 0, 0, 0.03); }",
|
|
1236
|
-
"@media (prefers-color-scheme: dark) { .table-hover tbody tr:hover { background-color: rgba(255, 255, 255, 0.20); } }",
|
|
1237
|
-
|
|
1238
|
-
// Utility styles.
|
|
1239
|
-
".btn-xs { font-size: 0.75rem !important; padding: 0.125rem 0.5rem !important; line-height: 1.5; touch-action: manipulation; }",
|
|
1240
|
-
".cursor-pointer { cursor: pointer; }",
|
|
1241
|
-
".user-select-none { user-select: none; -webkit-user-select: none; }",
|
|
1242
|
-
|
|
1243
|
-
// Use CSS for the category header hover emphasis to avoid JS event handlers for simple hover effects.
|
|
1244
|
-
"table[data-category] thead th[role='button']:hover { color: " + this.#themeColor.background + " !important; }",
|
|
1245
|
-
|
|
1246
|
-
// Respect reduced motion settings for accessibility.
|
|
1247
|
-
"@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }"
|
|
1248
|
-
/* eslint-enable @stylistic/max-len */
|
|
1249
|
-
];
|
|
1250
|
-
|
|
1251
|
-
const styleElement = this.#createElement("style", { id: "feature-options-styles" }, [styles.join("\n")]);
|
|
1252
|
-
|
|
1253
|
-
document.head.appendChild(styleElement);
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
1067
|
/**
|
|
1257
1068
|
* Show the device list taking the controller context into account.
|
|
1258
1069
|
*
|
|
@@ -3109,10 +2920,11 @@ export class webUiFeatureOptions {
|
|
|
3109
2920
|
|
|
3110
2921
|
row.style.display = this.#rowMatchesFilter(row, filterType) ? "" : "none";
|
|
3111
2922
|
}
|
|
3112
|
-
} else {
|
|
3113
2923
|
|
|
3114
|
-
|
|
2924
|
+
continue;
|
|
3115
2925
|
}
|
|
2926
|
+
|
|
2927
|
+
row.style.display = this.#rowMatchesFilter(row, filterType) ? "" : "none";
|
|
3116
2928
|
}
|
|
3117
2929
|
|
|
3118
2930
|
// Update counts and visibility. We need to update various UI elements to reflect the filtered view.
|
|
@@ -3145,11 +2957,13 @@ export class webUiFeatureOptions {
|
|
|
3145
2957
|
switch(filterType) {
|
|
3146
2958
|
|
|
3147
2959
|
case "modified":
|
|
2960
|
+
|
|
3148
2961
|
// Modified options have the text-info class to indicate they differ from defaults.
|
|
3149
2962
|
return row.querySelector("label")?.classList.contains("text-info") ?? false;
|
|
3150
2963
|
|
|
3151
2964
|
case "all":
|
|
3152
2965
|
default:
|
|
2966
|
+
|
|
3153
2967
|
return true;
|
|
3154
2968
|
}
|
|
3155
2969
|
}
|
|
@@ -3403,6 +3217,282 @@ export class webUiFeatureOptions {
|
|
|
3403
3217
|
}
|
|
3404
3218
|
}
|
|
3405
3219
|
|
|
3220
|
+
/**
|
|
3221
|
+
* Inject custom styles for hover effects, dark mode support, and modern layouts.
|
|
3222
|
+
*
|
|
3223
|
+
* These styles enhance the visual experience and ensure our UI integrates well with both light and dark modes. We use media queries to automatically adapt
|
|
3224
|
+
* to the user's system preferences. The styles include support for flexbox layouts, responsive design, and theme-aware coloring.
|
|
3225
|
+
*
|
|
3226
|
+
* @private
|
|
3227
|
+
*/
|
|
3228
|
+
#injectCustomStyles() {
|
|
3229
|
+
|
|
3230
|
+
// Ensure we do not inject duplicate styles when re-entering this view. We make this idempotent for stability across navigations.
|
|
3231
|
+
if(document.getElementById("feature-options-styles")) {
|
|
3232
|
+
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
const styles = [
|
|
3237
|
+
|
|
3238
|
+
/* eslint-disable @stylistic/max-len */
|
|
3239
|
+
// Define CSS variables used throughout our webUI. We update these when the theme changes so the UI can respond according to the visual environment.
|
|
3240
|
+
":root {",
|
|
3241
|
+
" --plugin-primary-bg: " + this.#themeColor.background + ";",
|
|
3242
|
+
" --plugin-primary-fg: " + this.#themeColor.text + ";",
|
|
3243
|
+
" --plugin-primary-hover: rgba(0,0,0,0.05); /* placeholder, JS will override with a color-specific value */",
|
|
3244
|
+
" --plugin-primary-subtle: rgba(0,0,0,0.03); /* placeholder, JS will override with a color-specific value */",
|
|
3245
|
+
" --plugin-body-bg-light: #ffffff;",
|
|
3246
|
+
" --plugin-body-bg-dark: #242424;",
|
|
3247
|
+
" --plugin-sidebar-bg-light: var(--bs-gray-100);",
|
|
3248
|
+
" --plugin-sidebar-bg-dark: #1A1A1A;",
|
|
3249
|
+
"}",
|
|
3250
|
+
|
|
3251
|
+
// We start with a base layout reset - remove margin collapse and enable clean layout flow.
|
|
3252
|
+
"html, body { margin: 0; padding: 0; }",
|
|
3253
|
+
|
|
3254
|
+
// Theme-scoped body and sidebar backgrounds. We scope to app-driven theme first.
|
|
3255
|
+
":root[data-plugin-theme='light'] body { background-color: var(--plugin-body-bg-light) !important; }",
|
|
3256
|
+
":root[data-plugin-theme='dark'] body { background-color: var(--plugin-body-bg-dark) !important; }",
|
|
3257
|
+
":root[data-plugin-theme='light'] #sidebar { background-color: var(--plugin-sidebar-bg-light) !important; }",
|
|
3258
|
+
":root[data-plugin-theme='dark'] #sidebar { background-color: var(--plugin-sidebar-bg-dark) !important; }",
|
|
3259
|
+
|
|
3260
|
+
// Page root uses a column layout with full width.
|
|
3261
|
+
"#pageFeatureOptions { display: flex !important; flex-direction: column; width: 100%; }",
|
|
3262
|
+
|
|
3263
|
+
// Sidebar + content layout is horizontal (row).
|
|
3264
|
+
".feature-main-content { display: flex !important; flex-direction: row !important; width: 100%; }",
|
|
3265
|
+
|
|
3266
|
+
// Sidebar layout and appearance.
|
|
3267
|
+
"#sidebar { display: block; width: 200px; min-width: 200px; max-width: 200px; position: relative; }",
|
|
3268
|
+
|
|
3269
|
+
// Remove internal scrolling from sidebar content.
|
|
3270
|
+
"#sidebar .sidebar-content { padding: 0rem; overflow: unset; }",
|
|
3271
|
+
|
|
3272
|
+
// Sidebar containers.
|
|
3273
|
+
"#controllersContainer { padding: 0; margin-bottom: 0; }",
|
|
3274
|
+
"#devicesContainer { padding: 0; margin-top: 0; padding-top: 0 !important; }",
|
|
3275
|
+
|
|
3276
|
+
// Feature content (right-hand pane).
|
|
3277
|
+
".feature-content { display: flex !important; flex-direction: column !important; flex: 1 1 auto; min-width: 0; }",
|
|
3278
|
+
// ".category-border { border: 1px solid " + this.#themeColor.background + " !important; box-shadow: 0 0 0 1px " + rgba(this.#themeColor.background, 0.1) + "; }",
|
|
3279
|
+
".category-border { border: 1px solid var(--plugin-primary-bg) !important; box-shadow: 0 0 0 1px var(--plugin-primary-hover); }",
|
|
3280
|
+
|
|
3281
|
+
// Ensure the table itself uses separate borders when we have rounded tbody elements. This is necessary for border-radius to work properly.
|
|
3282
|
+
"table[data-category] { border-collapse: separate !important; border-spacing: 0; }",
|
|
3283
|
+
|
|
3284
|
+
// How we define row visibility for feature options. We need this complexity because we hide or make visible rows depending on what the user has chosen to expose.
|
|
3285
|
+
//
|
|
3286
|
+
// "table[data-category] tbody tr.fo-visible,"
|
|
3287
|
+
// "table[data-category] tbody tr:not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none']){}",
|
|
3288
|
+
|
|
3289
|
+
// Create the outer border of the table on the left and right sides.
|
|
3290
|
+
"table[data-category] tbody tr:not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none']) td:first-child{",
|
|
3291
|
+
" border-left:1px solid var(--plugin-primary-bg);",
|
|
3292
|
+
"}",
|
|
3293
|
+
"table[data-category] tbody tr:not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none']) td:last-child{",
|
|
3294
|
+
" border-right:1px solid var(--plugin-primary-bg);",
|
|
3295
|
+
"}",
|
|
3296
|
+
|
|
3297
|
+
// Provide the top border on the first visible row.
|
|
3298
|
+
"table[data-category] tbody tr:nth-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td{",
|
|
3299
|
+
" border-top:1px solid var(--plugin-primary-bg);",
|
|
3300
|
+
"}",
|
|
3301
|
+
|
|
3302
|
+
// Provide the bottom border on the last visible row.
|
|
3303
|
+
"table[data-category] tbody tr:nth-last-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td{",
|
|
3304
|
+
" border-bottom:1px solid var(--plugin-primary-bg);",
|
|
3305
|
+
"}",
|
|
3306
|
+
|
|
3307
|
+
// Create rounded corners at the top and bottom rows.
|
|
3308
|
+
"table[data-category] tbody tr:nth-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td:first-child{",
|
|
3309
|
+
" border-top-left-radius:.5rem;",
|
|
3310
|
+
"}",
|
|
3311
|
+
"table[data-category] tbody tr:nth-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td:last-child{",
|
|
3312
|
+
" border-top-right-radius:.5rem;",
|
|
3313
|
+
"}",
|
|
3314
|
+
"table[data-category] tbody tr:nth-last-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td:first-child{",
|
|
3315
|
+
" border-bottom-left-radius:.5rem;",
|
|
3316
|
+
"}",
|
|
3317
|
+
"table[data-category] tbody tr:nth-last-child(1 of :not([hidden]):not(.d-none):not(.is-hidden):not([style*='display: none'])) td:last-child{",
|
|
3318
|
+
" border-bottom-right-radius:.5rem;",
|
|
3319
|
+
"}",
|
|
3320
|
+
|
|
3321
|
+
// Main options area - remove scroll behavior, just layout styling.
|
|
3322
|
+
".options-content { padding: 1rem; margin: 0; }",
|
|
3323
|
+
|
|
3324
|
+
// Info header styling.
|
|
3325
|
+
"#headerInfo { flex-shrink: 0; padding: 0.5rem !important; margin-bottom: 0.5rem !important; }",
|
|
3326
|
+
|
|
3327
|
+
// Device stats grid layout.
|
|
3328
|
+
".device-stats-grid { display: flex; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.5rem; padding: 0 0.75rem; flex-wrap: nowrap; overflow: hidden; }",
|
|
3329
|
+
".device-stats-grid .stat-item:first-child { flex: 0 0 25% }",
|
|
3330
|
+
".device-stats-grid .stat-item:not(:first-child) { flex-grow: 1; min-width: 0; }",
|
|
3331
|
+
|
|
3332
|
+
".stat-item { display: flex; flex-direction: column; gap: 0.125rem; }",
|
|
3333
|
+
".stat-label { font-weight: 600; color: var(--bs-gray-600); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
|
|
3334
|
+
".stat-value { font-size: 0.875rem; color: var(--bs-body-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
|
|
3335
|
+
|
|
3336
|
+
// Responsive hiding for our device stats grid.
|
|
3337
|
+
"@media (max-width: 700px) { .device-stats-grid .stat-item:nth-last-of-type(1) { display: none !important; } }",
|
|
3338
|
+
"@media (max-width: 500px) { .device-stats-grid .stat-item:nth-last-of-type(2) { display: none !important; } }",
|
|
3339
|
+
"@media (max-width: 300px) { .device-stats-grid .stat-item:nth-last-of-type(3) { display: none !important; } }",
|
|
3340
|
+
|
|
3341
|
+
// Responsive hiding for feature option status information.
|
|
3342
|
+
"@media (max-width: 400px) { #statusInfo { display: none !important; } }",
|
|
3343
|
+
|
|
3344
|
+
// Navigation styles.
|
|
3345
|
+
".nav-link { border-radius: 0.375rem; transition: all 0.2s; position: relative; padding: 0.25rem 0.75rem !important; line-height: 1.2; font-size: 0.8125rem; }",
|
|
3346
|
+
".nav-link:hover { background-color: var(--plugin-primary-hover); color: var(--plugin-primary-bg) !important; }",
|
|
3347
|
+
".nav-link.active { background-color: var(--plugin-primary-bg); color: var(--plugin-primary-fg) !important; }",
|
|
3348
|
+
".nav-header { border-bottom: 1px solid rgba(0, 0, 0, 0.1); margin-bottom: 0.125rem; padding: 0.25rem 0.75rem !important; font-size: 0.75rem !important; line-height: 1.2; }",
|
|
3349
|
+
"#devicesContainer .nav-header { font-weight: 600; margin-top: 0 !important; padding-top: 0.5rem !important; }",
|
|
3350
|
+
"#controllersContainer .nav-header { font-weight: 600; margin-top: 0 !important; padding-top: 0.5rem !important; }",
|
|
3351
|
+
|
|
3352
|
+
// Search bar.
|
|
3353
|
+
".search-toolbar { border-radius: 0.5rem; padding: 0 0 0.5rem 0; }",
|
|
3354
|
+
".search-input-wrapper { min-width: 0; }",
|
|
3355
|
+
".filter-pills { display: flex; gap: 0.5rem; flex-wrap: wrap; }",
|
|
3356
|
+
|
|
3357
|
+
// Grouped option visual indicator.
|
|
3358
|
+
".grouped-option { background-color: var(--plugin-primary-subtle); }",
|
|
3359
|
+
".grouped-option td:nth-child(2) label { padding-left: 20px; position: relative; }",
|
|
3360
|
+
".grouped-option td:nth-child(2) label::before { content: \"\\21B3\"; position: absolute; left: 4px; color: #666; }",
|
|
3361
|
+
|
|
3362
|
+
// Dark-mode refinements.
|
|
3363
|
+
":root[data-plugin-theme='dark'] .nav-header { border-bottom-color: rgba(255, 255, 255, 0.1); }",
|
|
3364
|
+
":root[data-plugin-theme='dark'] .text-body { color: #999 !important; }",
|
|
3365
|
+
":root[data-plugin-theme='dark'] .text-muted { color: #999 !important; }",
|
|
3366
|
+
":root[data-plugin-theme='dark'] .device-stats-grid { background-color: #1A1A1A; border-color: #444; }",
|
|
3367
|
+
":root[data-plugin-theme='dark'] .stat-label { color: #999; }",
|
|
3368
|
+
":root[data-plugin-theme='dark'] .stat-value { color: #999; }",
|
|
3369
|
+
":root[data-plugin-theme='dark'] #search .form-control { background-color: #1A1A1A; border-color: #444; color: #F8F9FA; }",
|
|
3370
|
+
":root[data-plugin-theme='dark'] #search .form-control:focus { background-color: #1A1A1A; border-color: #666; color: #F8F9FA; box-shadow: 0 0 0 0.2rem rgba(255, 160, 0, 0.25); }",
|
|
3371
|
+
":root[data-plugin-theme='dark'] #search .form-control::placeholder { color: #999; }",
|
|
3372
|
+
":root[data-plugin-theme='dark'] #statusInfo .text-muted { color: #B8B8B8 !important; }",
|
|
3373
|
+
|
|
3374
|
+
// Table hover styling.
|
|
3375
|
+
".table-hover tbody tr { transition: background-color 0.15s; }",
|
|
3376
|
+
":root[data-plugin-theme='light'] .table-hover tbody tr:hover { background-color: rgba(0, 0, 0, 0.03); }",
|
|
3377
|
+
":root[data-plugin-theme='dark'] .table-hover tbody tr:hover { background-color: rgba(255, 255, 255, 0.20); }",
|
|
3378
|
+
|
|
3379
|
+
|
|
3380
|
+
// Utility styles.
|
|
3381
|
+
".btn-xs { font-size: 0.75rem !important; padding: 0.125rem 0.5rem !important; line-height: 1.5; touch-action: manipulation; }",
|
|
3382
|
+
".cursor-pointer { cursor: pointer; }",
|
|
3383
|
+
".user-select-none { user-select: none; -webkit-user-select: none; }",
|
|
3384
|
+
|
|
3385
|
+
// Use CSS for the category header hover emphasis to avoid JS event handlers for simple hover effects.
|
|
3386
|
+
"table[data-category] thead th[role='button']:hover { color: var(--plugin-primary-bg) !important; }",
|
|
3387
|
+
|
|
3388
|
+
// Respect reduced motion settings for accessibility.
|
|
3389
|
+
"@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }"
|
|
3390
|
+
/* eslint-enable @stylistic/max-len */
|
|
3391
|
+
];
|
|
3392
|
+
|
|
3393
|
+
const styleElement = this.#createElement("style", { id: "feature-options-styles" }, [styles.join("\n")]);
|
|
3394
|
+
|
|
3395
|
+
document.head.appendChild(styleElement);
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
/**
|
|
3399
|
+
* Set up automatic theme detection and live updates.
|
|
3400
|
+
*
|
|
3401
|
+
* We rely on Config UI X to tell us what the preferred color scheme is. Once we have it, we recompute our color variables derived from .btn-primary whenever the theme
|
|
3402
|
+
* changes before mirroring all that to :root[data-plugin-theme="light|dark"] so our injected CSS is instantly reflected in our webUI.
|
|
3403
|
+
*
|
|
3404
|
+
* @private
|
|
3405
|
+
*/
|
|
3406
|
+
async #setupThemeAutoUpdate() {
|
|
3407
|
+
|
|
3408
|
+
// Apply the current theme lighting mode and compute color variables.
|
|
3409
|
+
this.#setPluginTheme(await homebridge.userCurrentLightingMode());
|
|
3410
|
+
|
|
3411
|
+
// Finally, we listen for system and browser changes to the current dark mode setting.
|
|
3412
|
+
this.#addEventListener(window.matchMedia("(prefers-color-scheme: dark)"), "change", async () => this.#setPluginTheme(await homebridge.userCurrentLightingMode()));
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
/**
|
|
3416
|
+
* Apply the current theme to :root and recompute JS-derived CSS variables.
|
|
3417
|
+
*
|
|
3418
|
+
* @param {"light"|"dark"} mode
|
|
3419
|
+
* @private
|
|
3420
|
+
*/
|
|
3421
|
+
#setPluginTheme(mode) {
|
|
3422
|
+
|
|
3423
|
+
// Sanity check.
|
|
3424
|
+
if(![ "dark", "light" ].includes(mode)) {
|
|
3425
|
+
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
// See if we're already set appropriately. If so, we're done.
|
|
3430
|
+
const current = document.documentElement.getAttribute("data-plugin-theme");
|
|
3431
|
+
|
|
3432
|
+
if(current === mode) {
|
|
3433
|
+
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// Update our theme.
|
|
3438
|
+
document.documentElement.setAttribute("data-plugin-theme", mode);
|
|
3439
|
+
|
|
3440
|
+
this.#computeThemeColors();
|
|
3441
|
+
this.#updateCssVariablesFromTheme();
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
/**
|
|
3445
|
+
* Compute current primary background and foreground from Bootstrap's .btn-primary.
|
|
3446
|
+
*
|
|
3447
|
+
* This gives us a theme-correct color pair regardless of the configured palette in Config UI X.
|
|
3448
|
+
*
|
|
3449
|
+
* @private
|
|
3450
|
+
*/
|
|
3451
|
+
#computeThemeColors() {
|
|
3452
|
+
|
|
3453
|
+
const probeBtn = document.createElement("button");
|
|
3454
|
+
|
|
3455
|
+
probeBtn.className = "btn btn-primary";
|
|
3456
|
+
probeBtn.style.display = "none";
|
|
3457
|
+
document.body.appendChild(probeBtn);
|
|
3458
|
+
|
|
3459
|
+
this.#themeColor.background = getComputedStyle(probeBtn).backgroundColor;
|
|
3460
|
+
this.#themeColor.text = getComputedStyle(probeBtn).color;
|
|
3461
|
+
|
|
3462
|
+
document.body.removeChild(probeBtn);
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
/**
|
|
3466
|
+
* Update CSS custom properties used by our injected styles so they immediately reflect the current theme.
|
|
3467
|
+
*
|
|
3468
|
+
* @private
|
|
3469
|
+
*/
|
|
3470
|
+
#updateCssVariablesFromTheme() {
|
|
3471
|
+
|
|
3472
|
+
const rootStyle = document.documentElement.style;
|
|
3473
|
+
|
|
3474
|
+
const rgba = (rgb, alpha) => {
|
|
3475
|
+
|
|
3476
|
+
const match = rgb.match(/\d+/g);
|
|
3477
|
+
|
|
3478
|
+
if(!match || (match.length < 3)) {
|
|
3479
|
+
|
|
3480
|
+
return rgb;
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
return "rgba(" + match[0] + ", " + match[1] + ", " + match[2] + ", " + alpha + ")";
|
|
3484
|
+
};
|
|
3485
|
+
|
|
3486
|
+
|
|
3487
|
+
// These variables are consumed by our injected CSS.
|
|
3488
|
+
rootStyle.setProperty("--plugin-primary-bg", this.#themeColor.background);
|
|
3489
|
+
rootStyle.setProperty("--plugin-primary-fg", this.#themeColor.text);
|
|
3490
|
+
|
|
3491
|
+
// Derivatives used for hover/subtle surfaces.
|
|
3492
|
+
rootStyle.setProperty("--plugin-primary-hover", rgba(this.#themeColor.background, 0.10));
|
|
3493
|
+
rootStyle.setProperty("--plugin-primary-subtle", rgba(this.#themeColor.background, 0.08));
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3406
3496
|
/**
|
|
3407
3497
|
* Clean up all resources when the instance is no longer needed.
|
|
3408
3498
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-plugin-utils",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.1",
|
|
4
4
|
"displayName": "Homebridge Plugin Utilities",
|
|
5
5
|
"description": "Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.",
|
|
6
6
|
"author": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@stylistic/eslint-plugin": "5.2.3",
|
|
44
44
|
"@types/node": "24.2.1",
|
|
45
|
-
"eslint": "9.
|
|
45
|
+
"eslint": "9.33.0",
|
|
46
46
|
"homebridge": "1.11.0",
|
|
47
47
|
"shx": "0.4.0",
|
|
48
48
|
"typedoc": "0.28.9",
|