homebridge-plugin-utils 1.34.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -2
- package/build/eslint-plugin/README.md +164 -0
- package/build/eslint-plugin/config.mjs +308 -0
- package/build/eslint-plugin/index.mjs +7 -0
- package/build/eslint-plugin/plugin.mjs +46 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
- package/build/eslint-plugin/rules/comment-style.mjs +190 -0
- package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
- package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
- package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
- package/build/eslint-plugin/test-setup.mjs +21 -0
- package/build/fs-ops.mjs +184 -0
- package/build/tsconfig.json +18 -3
- package/dist/backpressure.d.ts +128 -0
- package/dist/backpressure.js +273 -0
- package/dist/backpressure.js.map +1 -0
- package/dist/cli/index.d.ts +95 -0
- package/dist/cli/index.js +355 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clock-double.d.ts +93 -0
- package/dist/clock-double.js +141 -0
- package/dist/clock-double.js.map +1 -0
- package/dist/clock.d.ts +39 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/eslint-plugin/config.d.mts +193 -0
- package/dist/eslint-plugin/index.d.mts +2 -0
- package/dist/eslint-plugin/plugin.d.mts +87 -0
- package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
- package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
- package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
- package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
- package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
- package/dist/featureOptions-docs.d.ts +96 -0
- package/dist/featureOptions-docs.js +193 -0
- package/dist/featureOptions-docs.js.map +1 -0
- package/dist/featureOptions.d.ts +674 -0
- package/dist/featureOptions.js +870 -0
- package/dist/featureOptions.js.map +1 -0
- package/dist/ffmpeg/codecs.d.ts +256 -72
- package/dist/ffmpeg/codecs.js +477 -257
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/dgram-util.d.ts +46 -0
- package/dist/ffmpeg/dgram-util.js +38 -0
- package/dist/ffmpeg/dgram-util.js.map +1 -0
- package/dist/ffmpeg/exec.d.ts +83 -64
- package/dist/ffmpeg/exec.js +77 -86
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +70 -4
- package/dist/ffmpeg/fmp4.js +100 -11
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/hap-enums.d.ts +214 -0
- package/dist/ffmpeg/hap-enums.js +92 -0
- package/dist/ffmpeg/hap-enums.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +16 -9
- package/dist/ffmpeg/index.js +6 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
- package/dist/ffmpeg/mp4-assembler.js +387 -0
- package/dist/ffmpeg/mp4-assembler.js.map +1 -0
- package/dist/ffmpeg/mp4-parser.d.ts +94 -0
- package/dist/ffmpeg/mp4-parser.js +130 -0
- package/dist/ffmpeg/mp4-parser.js.map +1 -0
- package/dist/ffmpeg/options.d.ts +62 -149
- package/dist/ffmpeg/options.js +608 -499
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +143 -91
- package/dist/ffmpeg/process.js +406 -265
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +302 -188
- package/dist/ffmpeg/record.js +408 -560
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +144 -0
- package/dist/ffmpeg/recording-process-double.js +168 -0
- package/dist/ffmpeg/recording-process-double.js.map +1 -0
- package/dist/ffmpeg/rtp-parser.d.ts +70 -0
- package/dist/ffmpeg/rtp-parser.js +77 -0
- package/dist/ffmpeg/rtp-parser.js.map +1 -0
- package/dist/ffmpeg/rtp.d.ts +198 -141
- package/dist/ffmpeg/rtp.js +473 -244
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -1
- package/dist/ffmpeg/settings.js +20 -2
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -148
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/formatters.d.ts +106 -0
- package/dist/formatters.js +174 -0
- package/dist/formatters.js.map +1 -0
- package/dist/homebridge-enums.d.ts +30 -0
- package/dist/homebridge-enums.js +17 -0
- package/dist/homebridge-enums.js.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/logclient/auth.d.ts +114 -0
- package/dist/logclient/auth.js +199 -0
- package/dist/logclient/auth.js.map +1 -0
- package/dist/logclient/cli-run.d.ts +76 -0
- package/dist/logclient/cli-run.js +639 -0
- package/dist/logclient/cli-run.js.map +1 -0
- package/dist/logclient/cli.d.ts +3 -0
- package/dist/logclient/cli.js +97 -0
- package/dist/logclient/cli.js.map +1 -0
- package/dist/logclient/client.d.ts +145 -0
- package/dist/logclient/client.js +600 -0
- package/dist/logclient/client.js.map +1 -0
- package/dist/logclient/config.d.ts +173 -0
- package/dist/logclient/config.js +199 -0
- package/dist/logclient/config.js.map +1 -0
- package/dist/logclient/endpoints.d.ts +54 -0
- package/dist/logclient/endpoints.js +73 -0
- package/dist/logclient/endpoints.js.map +1 -0
- package/dist/logclient/filter.d.ts +45 -0
- package/dist/logclient/filter.js +51 -0
- package/dist/logclient/filter.js.map +1 -0
- package/dist/logclient/frame.d.ts +93 -0
- package/dist/logclient/frame.js +203 -0
- package/dist/logclient/frame.js.map +1 -0
- package/dist/logclient/index.d.ts +31 -0
- package/dist/logclient/index.js +12 -0
- package/dist/logclient/index.js.map +1 -0
- package/dist/logclient/parser.d.ts +211 -0
- package/dist/logclient/parser.js +393 -0
- package/dist/logclient/parser.js.map +1 -0
- package/dist/logclient/rest.d.ts +41 -0
- package/dist/logclient/rest.js +111 -0
- package/dist/logclient/rest.js.map +1 -0
- package/dist/logclient/settings.d.ts +15 -0
- package/dist/logclient/settings.js +64 -0
- package/dist/logclient/settings.js.map +1 -0
- package/dist/logclient/socket-double.d.ts +201 -0
- package/dist/logclient/socket-double.js +384 -0
- package/dist/logclient/socket-double.js.map +1 -0
- package/dist/logclient/socket.d.ts +257 -0
- package/dist/logclient/socket.js +620 -0
- package/dist/logclient/socket.js.map +1 -0
- package/dist/logclient/stitch.d.ts +83 -0
- package/dist/logclient/stitch.js +146 -0
- package/dist/logclient/stitch.js.map +1 -0
- package/dist/logclient/time-expression.d.ts +42 -0
- package/dist/logclient/time-expression.js +181 -0
- package/dist/logclient/time-expression.js.map +1 -0
- package/dist/logclient/time-window.d.ts +38 -0
- package/dist/logclient/time-window.js +53 -0
- package/dist/logclient/time-window.js.map +1 -0
- package/dist/logclient/types.d.ts +107 -0
- package/dist/logclient/types.js +6 -0
- package/dist/logclient/types.js.map +1 -0
- package/dist/mqttClient.d.ts +287 -0
- package/dist/mqttClient.js +433 -0
- package/dist/mqttClient.js.map +1 -0
- package/dist/service.d.ts +64 -15
- package/dist/service.js +93 -66
- package/dist/service.js.map +1 -1
- package/dist/ui/featureOptions.js +870 -0
- package/dist/ui/featureOptions.js.map +1 -0
- package/dist/ui/formatters.js +174 -0
- package/dist/ui/formatters.js.map +1 -0
- package/dist/ui/pluginConfigSession.mjs +141 -0
- package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
- package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
- package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
- package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
- package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
- package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
- package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
- package/dist/ui/webUi-featureOptions/state.mjs +319 -0
- package/dist/ui/webUi-featureOptions/store.mjs +181 -0
- package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
- package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
- package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
- package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
- package/dist/ui/webUi-featureOptions.mjs +677 -0
- package/dist/ui/webUi.mjs +192 -87
- package/dist/util.d.ts +597 -45
- package/dist/util.js +787 -68
- package/dist/util.js.map +1 -1
- package/package.json +33 -15
- package/build/eslint-rules.mjs +0 -511
- package/dist/featureoptions.d.ts +0 -264
- package/dist/featureoptions.js +0 -480
- package/dist/featureoptions.js.map +0 -1
- package/dist/mqttclient.d.ts +0 -178
- package/dist/mqttclient.js +0 -310
- package/dist/mqttclient.js.map +0 -1
- package/dist/ui/featureoptions.js +0 -480
- package/dist/ui/featureoptions.js.map +0 -1
- package/dist/ui/webUi-featureoptions.mjs +0 -3765
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* webUi-featureOptions/views/search.mjs: The search panel - search input + filter pills + toggle-all + status counts + reset button group.
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
import { createElement, setCategoryExpanded } from "../utils.mjs";
|
|
8
|
+
import { effect } from "../store.mjs";
|
|
9
|
+
import { projection } from "../selectors.mjs";
|
|
10
|
+
|
|
11
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mount the search panel view.
|
|
15
|
+
*
|
|
16
|
+
* The panel hosts the following interactive surfaces:
|
|
17
|
+
*
|
|
18
|
+
* - **Search input** - debounced 300ms; dispatches `filter:changed` with the trimmed query.
|
|
19
|
+
* - **Filter pills** (All / Modified) - dispatch `filter:changed` with the mode.
|
|
20
|
+
* - **Toggle-all categories** - imperative DOM mutation; sets `<details open>` on every category in the config table.
|
|
21
|
+
* - **Status bar counters** (total / modified / grouped / visible) - read from the projection, updated on any state change that touches it.
|
|
22
|
+
* - **Reset button group** (Reset... -> Reset to Defaults / Revert to Saved) - dispatches `options:reset` or `model:reverted`.
|
|
23
|
+
*
|
|
24
|
+
* The panel re-builds on `model:loaded` (and only then). Subsequent dispatches update individual elements (counts, pill active-state, toggle-all label) without
|
|
25
|
+
* rebuilding the DOM. The view's footprint is small because the heavy work - the projection walk - is shared with view-options through the memoized selector.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} args
|
|
28
|
+
* @param {HTMLElement} args.configTable - The `#configTable` element. The toggle-all handler queries it directly to set the open-state on category disclosures.
|
|
29
|
+
* @param {HTMLElement} args.root - The `#search` container.
|
|
30
|
+
* @param {AbortSignal} args.signal - Lifecycle signal.
|
|
31
|
+
* @param {import("../store.mjs").FeatureOptionsStore} args.store - The store.
|
|
32
|
+
*/
|
|
33
|
+
export const mountSearchView = ({ configTable, root, signal, store }) => {
|
|
34
|
+
|
|
35
|
+
// Element refs filled in by buildPanel(); update functions read these to mutate text or active-state.
|
|
36
|
+
const refs = {
|
|
37
|
+
|
|
38
|
+
filterAll: null,
|
|
39
|
+
filterModified: null,
|
|
40
|
+
grouped: null,
|
|
41
|
+
modified: null,
|
|
42
|
+
resetDefaults: null,
|
|
43
|
+
resetRevert: null,
|
|
44
|
+
resetToggle: null,
|
|
45
|
+
search: null,
|
|
46
|
+
toggleAll: null,
|
|
47
|
+
total: null,
|
|
48
|
+
visible: null
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let debounceTimer = null;
|
|
52
|
+
|
|
53
|
+
signal.addEventListener("abort", () => {
|
|
54
|
+
|
|
55
|
+
clearTimeout(debounceTimer);
|
|
56
|
+
}, { once: true });
|
|
57
|
+
|
|
58
|
+
// Build the panel once at model:loaded. Subsequent rebuilds would invalidate refs and re-bind handlers; for a single-instance panel, one-time build is correct.
|
|
59
|
+
effect({
|
|
60
|
+
|
|
61
|
+
events: ["model:loaded"],
|
|
62
|
+
fn: () => {
|
|
63
|
+
|
|
64
|
+
if(store.state.status.kind === "loading") {
|
|
65
|
+
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Build the panel content. The view never reveals its own region; the orchestrator owns region visibility and reveals the search panel via revealRegions once the
|
|
70
|
+
// populated UI is ready, so the search box and its metrics do not flash in before the rest of the page.
|
|
71
|
+
buildPanel({ debounce: scheduleSearchDispatch, refs, root });
|
|
72
|
+
},
|
|
73
|
+
signal,
|
|
74
|
+
store
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Counts and visible-toggle pill highlight update on any state change that touches the projection. Reading the projection here hits the memoized cache when
|
|
78
|
+
// nothing relevant changed (e.g., a scope:changed that resolves to the same view returns the cached projection in O(1)).
|
|
79
|
+
effect({
|
|
80
|
+
|
|
81
|
+
events: [ "model:loaded", "option:set", "option:cleared", "options:reset", "model:reverted", "filter:changed", "scope:changed", "devices:loaded" ],
|
|
82
|
+
fn: () => {
|
|
83
|
+
|
|
84
|
+
if(!refs.total) {
|
|
85
|
+
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const p = projection(store.state);
|
|
90
|
+
|
|
91
|
+
refs.total.textContent = String(p.counts.total);
|
|
92
|
+
refs.modified.textContent = String(p.counts.modified);
|
|
93
|
+
refs.grouped.textContent = String(p.counts.grouped);
|
|
94
|
+
refs.visible.textContent = String(p.counts.visible);
|
|
95
|
+
|
|
96
|
+
updateToggleAllLabel({ configTable, toggleAll: refs.toggleAll });
|
|
97
|
+
updateFilterPillState({ filterAll: refs.filterAll, filterModified: refs.filterModified, mode: store.state.filter.mode });
|
|
98
|
+
},
|
|
99
|
+
signal,
|
|
100
|
+
store
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Wire the search input event listener. Reads the current value, dispatches debounced filter:changed.
|
|
104
|
+
root.addEventListener("input", (event) => {
|
|
105
|
+
|
|
106
|
+
if(event.target !== refs.search) {
|
|
107
|
+
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
scheduleSearchDispatch(event.target.value.trim());
|
|
112
|
+
}, { signal });
|
|
113
|
+
|
|
114
|
+
// Wire button click handlers via delegation on the panel root.
|
|
115
|
+
root.addEventListener("click", (event) => handleClick({ configTable, event, refs, store }), { signal });
|
|
116
|
+
|
|
117
|
+
// Keep the toggle-all control in sync with the live expand/collapse ratio. A category's open-state change is a DOM-only `details.open` mutation, not a store
|
|
118
|
+
// dispatch, so it does not flow through the projection effect above - the control would otherwise only re-derive its label on option / filter / scope changes and go
|
|
119
|
+
// stale the moment the user expanded or collapsed a single category. We observe the same capture-phase `toggle` the options view listens for (toggle does not bubble)
|
|
120
|
+
// and re-derive the label, covering individual summary clicks, the bulk toggle-all, and saved-state restoration alike, in every engine.
|
|
121
|
+
configTable.addEventListener("toggle", (event) => {
|
|
122
|
+
|
|
123
|
+
if(event.target?.matches?.("details.fo-category")) {
|
|
124
|
+
|
|
125
|
+
updateToggleAllLabel({ configTable, toggleAll: refs.toggleAll });
|
|
126
|
+
}
|
|
127
|
+
}, { capture: true, signal });
|
|
128
|
+
|
|
129
|
+
// Debounce helper; closes over `debounceTimer` and `store`. setTimeout is intentional here over the abort-driven equivalent: clearTimeout is the simplest
|
|
130
|
+
// expression of "cancel the previous timer," with no abort-controller allocation per keystroke.
|
|
131
|
+
function scheduleSearchDispatch(query) {
|
|
132
|
+
|
|
133
|
+
clearTimeout(debounceTimer);
|
|
134
|
+
debounceTimer = setTimeout(() => {
|
|
135
|
+
|
|
136
|
+
store.dispatch({ query, type: "filter:changed" });
|
|
137
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Build the panel DOM and fill the `refs` record with element pointers that update functions consult later. One-time call; idempotent only insofar as it reassigns
|
|
142
|
+
// `refs` fields (the panel itself is rebuilt fresh, replacing any prior content).
|
|
143
|
+
const buildPanel = ({ refs, root }) => {
|
|
144
|
+
|
|
145
|
+
root.textContent = "";
|
|
146
|
+
root.className = "";
|
|
147
|
+
|
|
148
|
+
refs.total = createElement("strong", {}, ["0"]);
|
|
149
|
+
refs.modified = createElement("strong", { classList: ["text-warning"] }, ["0"]);
|
|
150
|
+
refs.grouped = createElement("strong", { classList: ["text-info"] }, ["0"]);
|
|
151
|
+
refs.visible = createElement("strong", { classList: ["text-success"] }, ["0"]);
|
|
152
|
+
|
|
153
|
+
const statusInfo = createElement("div", {
|
|
154
|
+
|
|
155
|
+
id: "statusInfo",
|
|
156
|
+
role: "status",
|
|
157
|
+
style: { flex: "1 1 auto", minWidth: "0", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }
|
|
158
|
+
}, [
|
|
159
|
+
createElement("span", { classList: ["text-muted"] }, [
|
|
160
|
+
|
|
161
|
+
refs.total, " total options · ",
|
|
162
|
+
refs.modified, " modified · ",
|
|
163
|
+
refs.grouped, " grouped · ",
|
|
164
|
+
refs.visible, " visible"
|
|
165
|
+
])
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
refs.resetToggle = createElement("button", {
|
|
169
|
+
|
|
170
|
+
classList: [ "btn", "btn-xs", "btn-outline-danger", "cursor-pointer", "text-truncate", "user-select-none" ],
|
|
171
|
+
"data-action": "reset-toggle",
|
|
172
|
+
style: { fontSize: "var(--fo-font-size-xs)", marginLeft: "auto", padding: "var(--fo-space-xs) var(--fo-space-sm)" },
|
|
173
|
+
textContent: "Reset...",
|
|
174
|
+
title: "Configuration reset options.",
|
|
175
|
+
type: "button"
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
refs.resetDefaults = createElement("button", {
|
|
179
|
+
|
|
180
|
+
classList: [ "btn", "btn-xs", "btn-outline-danger", "cursor-pointer", "d-none", "text-truncate", "user-select-none" ],
|
|
181
|
+
"data-action": "reset-defaults",
|
|
182
|
+
style: { fontSize: "var(--fo-font-size-xs)", marginLeft: "auto", padding: "var(--fo-space-xs) var(--fo-space-sm)" },
|
|
183
|
+
textContent: "Reset to Defaults",
|
|
184
|
+
title: "Reset all options to default values.",
|
|
185
|
+
type: "button"
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
refs.resetRevert = createElement("button", {
|
|
189
|
+
|
|
190
|
+
classList: [ "btn", "btn-xs", "btn-outline-danger", "cursor-pointer", "d-none", "text-truncate", "user-select-none" ],
|
|
191
|
+
"data-action": "reset-revert",
|
|
192
|
+
style: { fontSize: "var(--fo-font-size-xs)", marginLeft: "auto", padding: "var(--fo-space-xs) var(--fo-space-sm)" },
|
|
193
|
+
textContent: "Revert to Saved",
|
|
194
|
+
title: "Revert options to the last saved configuration.",
|
|
195
|
+
type: "button"
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const resetGroup = createElement("div", {
|
|
199
|
+
|
|
200
|
+
classList: [ "d-flex", "align-items-center", "gap-1" ],
|
|
201
|
+
role: "group"
|
|
202
|
+
}, [ refs.resetToggle, refs.resetDefaults, refs.resetRevert ]);
|
|
203
|
+
|
|
204
|
+
const statusBar = createElement("div", {
|
|
205
|
+
|
|
206
|
+
classList: [ "d-flex", "justify-content-between", "align-items-center", "px-2", "py-1", "mb-1", "alert-info", "rounded" ],
|
|
207
|
+
id: "featureStatusBar",
|
|
208
|
+
style: { alignItems: "center", display: "flex", fontSize: "var(--fo-font-size-sm)", gap: "var(--fo-space-sm)" }
|
|
209
|
+
}, [ statusInfo, resetGroup ]);
|
|
210
|
+
|
|
211
|
+
// Search input.
|
|
212
|
+
refs.search = createElement("input", {
|
|
213
|
+
|
|
214
|
+
autocomplete: "off",
|
|
215
|
+
classList: ["form-control"],
|
|
216
|
+
id: "searchInput",
|
|
217
|
+
placeholder: "Search options...",
|
|
218
|
+
type: "search"
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const searchWrapper = createElement("div", {
|
|
222
|
+
|
|
223
|
+
classList: [ "search-input-wrapper", "flex-grow-1" ],
|
|
224
|
+
style: { maxWidth: "400px" }
|
|
225
|
+
}, [createElement("div", { classList: ["input-group"] }, [refs.search])]);
|
|
226
|
+
|
|
227
|
+
// Filter pills.
|
|
228
|
+
refs.filterAll = createElement("button", {
|
|
229
|
+
|
|
230
|
+
classList: [ "btn", "btn-xs", "btn-primary", "cursor-pointer", "user-select-none" ],
|
|
231
|
+
"data-filter": "all",
|
|
232
|
+
id: "filter-all",
|
|
233
|
+
style: { fontSize: "var(--fo-font-size-xs)", padding: "var(--fo-space-xxs) var(--fo-space-sm)" },
|
|
234
|
+
textContent: "All",
|
|
235
|
+
title: "Show all options.",
|
|
236
|
+
type: "button"
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
refs.filterModified = createElement("button", {
|
|
240
|
+
|
|
241
|
+
classList: [ "btn", "btn-xs", "btn-outline-secondary", "cursor-pointer", "user-select-none" ],
|
|
242
|
+
"data-filter": "modified",
|
|
243
|
+
id: "filter-modified",
|
|
244
|
+
style: { fontSize: "var(--fo-font-size-xs)", padding: "var(--fo-space-xxs) var(--fo-space-sm)" },
|
|
245
|
+
textContent: "Modified",
|
|
246
|
+
title: "Show only modified options.",
|
|
247
|
+
type: "button"
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const pills = createElement("div", { classList: [ "filter-pills", "d-flex", "gap-1" ] }, [ refs.filterAll, refs.filterModified ]);
|
|
251
|
+
|
|
252
|
+
// Expand-all toggle.
|
|
253
|
+
refs.toggleAll = createElement("button", {
|
|
254
|
+
|
|
255
|
+
classList: [ "btn", "btn-xs", "btn-outline-secondary" ],
|
|
256
|
+
id: "toggleAllCategories",
|
|
257
|
+
style: {
|
|
258
|
+
|
|
259
|
+
display: "inline-block",
|
|
260
|
+
fontFamily: "var(--fo-font-monospace)",
|
|
261
|
+
fontSize: "var(--fo-font-size-xs)",
|
|
262
|
+
padding: "var(--fo-space-xxs) var(--fo-space-sm)",
|
|
263
|
+
textAlign: "center"
|
|
264
|
+
},
|
|
265
|
+
type: "button"
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const controlBar = createElement("div", { classList: ["search-toolbar"] }, [
|
|
269
|
+
|
|
270
|
+
createElement("div", { classList: [ "d-flex", "flex-wrap", "gap-2", "align-items-center" ] }, [
|
|
271
|
+
|
|
272
|
+
searchWrapper,
|
|
273
|
+
pills,
|
|
274
|
+
createElement("div", { classList: [ "ms-auto", "d-flex", "gap-2" ] }, [refs.toggleAll])
|
|
275
|
+
])
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
root.appendChild(statusBar);
|
|
279
|
+
root.appendChild(controlBar);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Update the toggle-all button's glyph, title, and data-action attribute based on the current expand/collapse ratio. When more than half the categories are
|
|
283
|
+
// expanded, the button's data-action and title switch to collapse (and its glyph flips); otherwise they switch to expand. Single read pass over the config table;
|
|
284
|
+
// no state stored.
|
|
285
|
+
const updateToggleAllLabel = ({ configTable, toggleAll }) => {
|
|
286
|
+
|
|
287
|
+
if(!toggleAll) {
|
|
288
|
+
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const categories = configTable.querySelectorAll("details[data-category]");
|
|
293
|
+
const expandedCount = [...categories].filter((details) => details.open).length;
|
|
294
|
+
const shouldShowCollapse = expandedCount > (categories.length / 2);
|
|
295
|
+
|
|
296
|
+
toggleAll.textContent = shouldShowCollapse ? "▶" : "▼";
|
|
297
|
+
toggleAll.title = shouldShowCollapse ? "Collapse all categories" : "Expand all categories";
|
|
298
|
+
toggleAll.setAttribute("data-action", shouldShowCollapse ? "collapse" : "expand");
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Apply the active-state class swap to the filter pills based on the current filter mode. The active pill carries its semantic color class; the inactive pill
|
|
302
|
+
// falls back to the outline style. The single source of truth is `state.filter.mode`; the DOM follows.
|
|
303
|
+
const updateFilterPillState = ({ filterAll, filterModified, mode }) => {
|
|
304
|
+
|
|
305
|
+
if(!filterAll || !filterModified) {
|
|
306
|
+
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const allClasses = (mode === "all") ? [ "btn-primary", "cursor-pointer", "user-select-none" ] : [ "btn-outline-secondary", "cursor-pointer", "user-select-none" ];
|
|
311
|
+
const modClasses = (mode === "modified") ?
|
|
312
|
+
[ "btn-warning", "text-dark", "cursor-pointer", "user-select-none" ] :
|
|
313
|
+
[ "btn-outline-secondary", "cursor-pointer", "user-select-none" ];
|
|
314
|
+
|
|
315
|
+
filterAll.className = "";
|
|
316
|
+
filterAll.classList.add("btn", "btn-xs", ...allClasses);
|
|
317
|
+
|
|
318
|
+
filterModified.className = "";
|
|
319
|
+
filterModified.classList.add("btn", "btn-xs", ...modClasses);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Handle a click on the search panel. Dispatches by comparing the click target's identity against the ref-held elements. Reset and revert buttons collapse the
|
|
323
|
+
// action set back to "Reset..." after dispatching; the reset-toggle button reveals the action set without dispatching.
|
|
324
|
+
const handleClick = ({ configTable, event, refs, store }) => {
|
|
325
|
+
|
|
326
|
+
const target = event.target;
|
|
327
|
+
|
|
328
|
+
if(target === refs.filterAll) {
|
|
329
|
+
|
|
330
|
+
store.dispatch({ mode: "all", type: "filter:changed" });
|
|
331
|
+
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if(target === refs.filterModified) {
|
|
336
|
+
|
|
337
|
+
store.dispatch({ mode: "modified", type: "filter:changed" });
|
|
338
|
+
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if(target === refs.toggleAll) {
|
|
343
|
+
|
|
344
|
+
const shouldExpand = refs.toggleAll.getAttribute("data-action") === "expand";
|
|
345
|
+
|
|
346
|
+
for(const details of configTable.querySelectorAll("details[data-category]")) {
|
|
347
|
+
|
|
348
|
+
setCategoryExpanded(details, shouldExpand);
|
|
349
|
+
|
|
350
|
+
// On bulk expand, synchronously drive the options view's capture-phase `toggle` handler so each category's rows materialize in the SAME layout pass as the open,
|
|
351
|
+
// rather than the open reflowing first and the async native `toggle` task materializing in a second pass. Collapsing the opens and materializations into one
|
|
352
|
+
// settled height change keeps the host's iframe-resize ResizeObserver from cascading ("ResizeObserver loop completed with undelivered notifications"). Row
|
|
353
|
+
// materialization is idempotent, so the native async toggle that follows is a no-op. Collapse needs no synthetic toggle - hiding rows is already one reflow.
|
|
354
|
+
if(shouldExpand) {
|
|
355
|
+
|
|
356
|
+
details.dispatchEvent(new Event("toggle"));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
updateToggleAllLabel({ configTable, toggleAll: refs.toggleAll });
|
|
361
|
+
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if(target === refs.resetToggle) {
|
|
366
|
+
|
|
367
|
+
refs.resetDefaults.classList.toggle("d-none");
|
|
368
|
+
refs.resetRevert.classList.toggle("d-none");
|
|
369
|
+
refs.resetToggle.textContent = refs.resetDefaults.classList.contains("d-none") ? "Reset..." : "▶";
|
|
370
|
+
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if(target === refs.resetDefaults) {
|
|
375
|
+
|
|
376
|
+
store.dispatch({ type: "options:reset" });
|
|
377
|
+
collapseResetGroup(refs);
|
|
378
|
+
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if(target === refs.resetRevert) {
|
|
383
|
+
|
|
384
|
+
store.dispatch({ type: "model:reverted" });
|
|
385
|
+
collapseResetGroup(refs);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Collapse the reset button group back to the "Reset..." affordance after a destructive action fires.
|
|
390
|
+
const collapseResetGroup = (refs) => {
|
|
391
|
+
|
|
392
|
+
refs.resetDefaults.classList.add("d-none");
|
|
393
|
+
refs.resetRevert.classList.add("d-none");
|
|
394
|
+
refs.resetToggle.textContent = "Reset...";
|
|
395
|
+
};
|