homebridge-plugin-utils 1.35.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 +94 -45
- package/dist/backpressure.js +229 -93
- package/dist/backpressure.js.map +1 -1
- 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 -262
- 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 +49 -2
- package/dist/ffmpeg/fmp4.js +47 -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 +142 -96
- package/dist/ffmpeg/process.js +406 -278
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +302 -188
- package/dist/ffmpeg/record.js +408 -567
- 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 -251
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -2
- package/dist/ffmpeg/settings.js +20 -5
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -150
- 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 -6
- package/dist/index.js +7 -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 +602 -45
- package/dist/util.js +783 -78
- 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,152 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* webUi-featureOptions/effects/tokens.mjs: Design-token stylesheet adoption for the feature options webUI.
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register the design-tokens effect. Adopts a constructable stylesheet declaring every `--fo-*` CSS custom property the rest of the webUI references (font family,
|
|
9
|
+
* font scale, spacing scale, radii, motion, accent colors, surfaces, borders, text, form controls, interactive states, specific colors). Cleanup is automatic via
|
|
10
|
+
* the supplied AbortSignal - aborting removes the stylesheet from `document.adoptedStyleSheets`.
|
|
11
|
+
*
|
|
12
|
+
* Stylistic invariants the tokens encode:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Tokens declare, theme overrides.** Accent tokens default to the CSS-standard `AccentColor` / `AccentColorText` keywords; the theme effect overrides them at
|
|
15
|
+
* runtime via `documentElement.style.setProperty` after probing Bootstrap's `.btn-primary`. The declared defaults remain in effect until (and if) the probe
|
|
16
|
+
* succeeds, so the user sees a sensible accent before Bootstrap's stylesheet is necessarily ready.
|
|
17
|
+
* 2. **Dark mode is token redefinition, not light-dark() at the consumer.** Every light/dark color pair is expressed as two declarations of the same token - one
|
|
18
|
+
* at `:root` for the light default, one at `:root.fo-dark` for the dark override. Consumers reference the token by name (`var(--fo-surface-bg)`) and get the
|
|
19
|
+
* right value automatically. A future third theme (high-contrast, brand variant) joins by adding a third selector block; no consumer rules change.
|
|
20
|
+
*
|
|
21
|
+
* Adopted synchronously - token declarations are static and have no I/O dependencies, so the stylesheet is ready by the time the call returns. Must run before any
|
|
22
|
+
* consumer (the theme effect or any inline style using `var(--fo-*)`) references a token, which the orchestrator's boot sequence guarantees by registering this
|
|
23
|
+
* effect first.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} args
|
|
26
|
+
* @param {AbortSignal} args.signal - Lifecycle signal. Aborting releases the stylesheet from the document.
|
|
27
|
+
*/
|
|
28
|
+
export const registerTokensEffect = ({ signal }) => {
|
|
29
|
+
|
|
30
|
+
if(signal.aborted) {
|
|
31
|
+
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const stylesheet = new CSSStyleSheet();
|
|
36
|
+
|
|
37
|
+
stylesheet.replaceSync(buildTokenCss());
|
|
38
|
+
document.adoptedStyleSheets = [ ...document.adoptedStyleSheets, stylesheet ];
|
|
39
|
+
|
|
40
|
+
signal.addEventListener("abort", () => {
|
|
41
|
+
|
|
42
|
+
// Release the stylesheet. Filter-rebuild rather than mutate to preserve the array's identity contract across consumers.
|
|
43
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((sheet) => sheet !== stylesheet);
|
|
44
|
+
}, { once: true });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Assemble the full token CSS. Grouped by category with a section comment per group so the file reads as the design vocabulary it represents. Within a scale
|
|
48
|
+
// group, tokens order smallest-to-largest by magnitude; within a flat group, tokens order alphabetically by token name; surfaces order by conceptual layering
|
|
49
|
+
// (base surface before elevated) rather than by scale or alphabet. The groups themselves are ordered by their position in the same conceptual hierarchy, from
|
|
50
|
+
// the broadest, foundational concerns down to the narrowest, single-purpose tokens.
|
|
51
|
+
const buildTokenCss = () => [
|
|
52
|
+
|
|
53
|
+
":root {",
|
|
54
|
+
|
|
55
|
+
// Font family. Provides explicit fallbacks beyond the `ui-monospace` keyword because Chrome's form-control font-resolution path can land on serif when only the
|
|
56
|
+
// keyword is specified. The stack walks the platform's canonical monospace files explicitly, terminating in the `monospace` generic.
|
|
57
|
+
" --fo-font-monospace: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", \"Courier New\", monospace;",
|
|
58
|
+
|
|
59
|
+
// Font scale.
|
|
60
|
+
" --fo-font-size-xs: 0.75rem;",
|
|
61
|
+
" --fo-font-size-sm: 0.8125rem;",
|
|
62
|
+
|
|
63
|
+
// Spacing scale. T-shirt sized, rem-based, distinct from Bootstrap's numbered `--bs-spacer * N` to avoid the half-aligned trap.
|
|
64
|
+
" --fo-space-xxs: 0.125rem;",
|
|
65
|
+
" --fo-space-xs: 0.25rem;",
|
|
66
|
+
" --fo-space-sm: 0.5rem;",
|
|
67
|
+
" --fo-space-md: 0.75rem;",
|
|
68
|
+
" --fo-space-lg: 1rem;",
|
|
69
|
+
|
|
70
|
+
// Border-radius scale.
|
|
71
|
+
" --fo-radius-sm: 0.375rem;",
|
|
72
|
+
" --fo-radius-md: 0.5rem;",
|
|
73
|
+
|
|
74
|
+
// Motion.
|
|
75
|
+
" --fo-transition-fast: 0.15s;",
|
|
76
|
+
" --fo-transition-base: 0.2s;",
|
|
77
|
+
|
|
78
|
+
// Accent colors. CSS-standard `AccentColor` / `AccentColorText` keywords as the declared defaults; the theme effect overrides at runtime via setProperty after
|
|
79
|
+
// probing Bootstrap's `.btn-primary`. The derivative tokens (-hover, -subtle) compose via `color-mix()` against whatever value the accent currently holds, so the
|
|
80
|
+
// derivatives stay in lockstep with both the default and the probed override without separate machinery.
|
|
81
|
+
" --fo-accent-bg: AccentColor;",
|
|
82
|
+
" --fo-accent-fg: AccentColorText;",
|
|
83
|
+
" --fo-accent-hover: color-mix(in srgb, var(--fo-accent-bg) 10%, transparent);",
|
|
84
|
+
" --fo-accent-subtle: color-mix(in srgb, var(--fo-accent-bg) 8%, transparent);",
|
|
85
|
+
|
|
86
|
+
// Surfaces. Light defaults; `:root.fo-dark` redeclares for dark mode. Tokens that defer to Bootstrap variables carry an explicit fallback inside the `var()` so
|
|
87
|
+
// the token resolves cleanly even when Bootstrap has not loaded yet (the orchestrator initializes tokens before Bootstrap's stylesheet is necessarily ready).
|
|
88
|
+
" --fo-surface-bg: #ffffff;",
|
|
89
|
+
" --fo-elevated-bg: var(--bs-gray-100, #f8f9fa);",
|
|
90
|
+
|
|
91
|
+
// Borders.
|
|
92
|
+
// Accent-derived container border - the probed theme accent at reduced opacity, shared by every outer container frame. One declaration for both themes: the
|
|
93
|
+
// accent is theme-probed, not light/dark-specific, so it needs no `:root.fo-dark` redeclaration. Kept a step lighter than the full-accent per-category frame so
|
|
94
|
+
// the two read as the same family at different weights.
|
|
95
|
+
" --fo-border-accent: color-mix(in srgb, var(--fo-accent-bg) 50%, transparent);",
|
|
96
|
+
" --fo-border-strong: var(--bs-border-color, #dee2e6);",
|
|
97
|
+
" --fo-border-subtle: rgba(0, 0, 0, 0.1);",
|
|
98
|
+
|
|
99
|
+
// Text.
|
|
100
|
+
" --fo-text-muted: var(--bs-gray-600, #6c757d);",
|
|
101
|
+
" --fo-text-on-elevated: var(--bs-body-color, #212529);",
|
|
102
|
+
|
|
103
|
+
// Form controls (light = Bootstrap defaults; dark overrides at `:root.fo-dark`).
|
|
104
|
+
" --fo-form-control-bg: var(--bs-body-bg, #ffffff);",
|
|
105
|
+
" --fo-form-control-border: var(--bs-border-color, #dee2e6);",
|
|
106
|
+
// A latent hook kept for symmetry with the dark override below: no light-mode rule consumes this token today (light-mode search focus uses Bootstrap's own focus
|
|
107
|
+
// styling), so this accent default takes effect only if a future light-mode focus rule references it.
|
|
108
|
+
" --fo-form-control-focus-border: var(--fo-accent-bg);",
|
|
109
|
+
" --fo-form-control-placeholder: var(--bs-secondary-color, rgba(33, 37, 41, 0.75));",
|
|
110
|
+
// The focus ring (box-shadow glow) shared by the search input's light and dark focus rules. Light matches the container border color (--fo-border-accent) so the
|
|
111
|
+
// ring reads as the same theme slate; dark redeclares a lighter accent tint below, because a translucent color that reads well on the dark surface washes out to
|
|
112
|
+
// near-gray on the light one - the ring is genuinely mode-dependent, like the surface tokens.
|
|
113
|
+
" --fo-focus-ring: 0 0 0 0.2rem var(--fo-border-accent);",
|
|
114
|
+
|
|
115
|
+
// Interactive-state colors.
|
|
116
|
+
" --fo-row-hover-bg: rgba(0, 0, 0, 0.03);",
|
|
117
|
+
|
|
118
|
+
// Specific colors. The grouped-option indicator (the Unicode `\\21B3` arrow) and the statusInfo's muted text want a distinct value from `--fo-text-muted` for
|
|
119
|
+
// visual hierarchy; they get their own tokens to express that.
|
|
120
|
+
" --fo-grouped-indicator: #666;",
|
|
121
|
+
" --fo-statusinfo-muted: var(--bs-secondary-color, rgba(33, 37, 41, 0.75));",
|
|
122
|
+
|
|
123
|
+
"}",
|
|
124
|
+
|
|
125
|
+
// Dark-mode redeclarations. Same tokens, dark values. The cascade applies these when `.fo-dark` is on `:root`, which the theme effect toggles based on the user's
|
|
126
|
+
// Homebridge lighting-mode preference.
|
|
127
|
+
":root.fo-dark {",
|
|
128
|
+
" --fo-surface-bg: #242424;",
|
|
129
|
+
" --fo-elevated-bg: #1A1A1A;",
|
|
130
|
+
" --fo-border-strong: #444;",
|
|
131
|
+
" --fo-border-subtle: rgba(255, 255, 255, 0.1);",
|
|
132
|
+
" --fo-text-muted: #999;",
|
|
133
|
+
" --fo-text-on-elevated: #F8F9FA;",
|
|
134
|
+
" --fo-form-control-bg: #1A1A1A;",
|
|
135
|
+
" --fo-form-control-border: #444;",
|
|
136
|
+
// A deliberately neutral border rather than the accent: the dark focus rule pairs this with an accent-tinted glow (the focus box-shadow), so the accent affordance
|
|
137
|
+
// shows through the glow while the border stays plain gray rather than doubling the accent on both the border and the ring.
|
|
138
|
+
" --fo-form-control-focus-border: #666;",
|
|
139
|
+
" --fo-form-control-placeholder: #999;",
|
|
140
|
+
// Dark focus ring: a lighter accent tint than light's border-matched value - 25% reads as a clear glow against the dark surface, where a stronger ring would be
|
|
141
|
+
// heavy. (Light uses the full border color; see the light declaration above.)
|
|
142
|
+
" --fo-focus-ring: 0 0 0 0.2rem color-mix(in srgb, var(--fo-accent-bg) 25%, transparent);",
|
|
143
|
+
// A white tint at a far higher alpha than the light block's 3% black: a faint dark overlay is invisible against the dark surface, so the dark hover needs a lighter,
|
|
144
|
+
// stronger tint to read as an equivalent highlight.
|
|
145
|
+
" --fo-row-hover-bg: rgba(255, 255, 255, 0.20);",
|
|
146
|
+
// Kept distinct from --fo-text-muted (#999 in dark mode) for the same visual-hierarchy reason as the light-mode block above; lighter than the muted text so the
|
|
147
|
+
// grouped-option arrow reads as slightly more prominent on a dark surface, mirroring the darker-than-muted emphasis it carries in light mode.
|
|
148
|
+
" --fo-grouped-indicator: #B0B0B0;",
|
|
149
|
+
" --fo-statusinfo-muted: #B8B8B8;",
|
|
150
|
+
"}"
|
|
151
|
+
|
|
152
|
+
].join("\n");
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* webUi-featureOptions/rendering.mjs: Pure DOM construction for the feature options webUI.
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
import { isValueOption, optionExists } from "../featureOptions.js";
|
|
8
|
+
import { createElement } from "./utils.mjs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pure DOM construction for the feature options webUI.
|
|
12
|
+
*
|
|
13
|
+
* Every export here is a pure function from data to DOM:
|
|
14
|
+
*
|
|
15
|
+
* - {@link categoryShell} - builds the `<details>` shell for one category (header + empty rows container). Lazy materialization: the rows container is intentionally
|
|
16
|
+
* empty; the view fills it on first expand to keep initial render bounded by category count, not option count.
|
|
17
|
+
* - {@link optionRow} - builds one option row's bare structure (checkbox, label, optional value-input) and applies its initial state via {@link applyRowState}.
|
|
18
|
+
* - {@link applyRowState} - the single writer for every state-dependent attribute of a row (tri-state, value-input state, label color, visibility, dependency
|
|
19
|
+
* badge), derived from the projection entry. The construction path and the per-mutation update walk both call it, so a freshly-built row and a re-derived row run
|
|
20
|
+
* identical code - no derived attribute can be set on one path and forgotten on the other. This is what makes the row DOM a pure function of the projection.
|
|
21
|
+
* - {@link triStateTransition} - the click-time state machine. Given the current DOM state of a checkbox plus the projection entry and configuration, computes only
|
|
22
|
+
* the action to dispatch. The resulting DOM state is not returned: the dispatch updates the store, and the view re-derives the row through {@link applyRowState}
|
|
23
|
+
* against the post-dispatch projection. Pure - takes data, returns data.
|
|
24
|
+
*
|
|
25
|
+
* The functions take projection entries (the view-relative records built by {@link projection}), the current view's scope kind, and any view-context identifiers
|
|
26
|
+
* they need (controllerId, deviceId). They do not read from a store, do not subscribe to events, do not call selectors. The view layer assembles the inputs and
|
|
27
|
+
* applies the outputs.
|
|
28
|
+
*
|
|
29
|
+
* Splitting the tri-state click logic out as a pure function ({@link triStateTransition}) means the inheritance state machine's action selection can be tested in
|
|
30
|
+
* isolation with plain DOM stubs - no store, no view layer, no event scaffolding. The view layer calls it on the change event, dispatches the returned action, and
|
|
31
|
+
* lets the reactive re-projection drive the DOM through {@link applyRowState}.
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build the `<details>` shell for a category. Returns the disclosure element with its header (`<summary>`) and an empty `<div class="fo-category-rows">` rows
|
|
38
|
+
* container. The rows container is intentionally empty; the view materializes its option rows lazily on first expand via {@link optionRow}.
|
|
39
|
+
*
|
|
40
|
+
* The category header carries a scope-suffix label - `(Global)` / `(Controller-specific)` / `(Device-specific)` - so the user always knows which scope they are
|
|
41
|
+
* editing at. Each variant maps directly from the view's scope kind; we do not consult validators here because the scope DU already encodes the distinction
|
|
42
|
+
* (controller view vs device view is the scope's kind, not a runtime predicate).
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} args
|
|
45
|
+
* @param {import("../featureOptions.js").FeatureCategoryEntry} args.category - The catalog entry for the category.
|
|
46
|
+
* @param {"controller" | "device" | "global"} args.scopeKind - The current view's scope kind.
|
|
47
|
+
* @returns {HTMLDetailsElement} The category shell, ready for insertion into the config table.
|
|
48
|
+
*/
|
|
49
|
+
export const categoryShell = ({ category, scopeKind }) => {
|
|
50
|
+
|
|
51
|
+
const arrow = createElement("span", { "aria-hidden": "true", classList: ["fo-category-arrow"] }, ["▶"]);
|
|
52
|
+
const title = createElement("span", { classList: ["fo-category-title"] }, [category.description + scopeLabel(scopeKind)]);
|
|
53
|
+
const summary = createElement("summary", { classList: ["fo-category-header"], title: "Expand or collapse this category." }, [ arrow, title ]);
|
|
54
|
+
const rows = createElement("div", { classList: ["fo-category-rows"] });
|
|
55
|
+
const details = createElement("details", { classList: ["fo-category"], "data-category": category.name });
|
|
56
|
+
|
|
57
|
+
details.appendChild(summary);
|
|
58
|
+
details.appendChild(rows);
|
|
59
|
+
|
|
60
|
+
return details;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build one option row: its bare structure plus the initial state applied through {@link applyRowState}.
|
|
65
|
+
*
|
|
66
|
+
* Every row has one uniform shape - a checkbox followed by a `<div class="fo-option-content">` cell - regardless of option kind:
|
|
67
|
+
*
|
|
68
|
+
* - **Boolean options**: the content cell holds only the `<label>`.
|
|
69
|
+
* - **Value-centric options**: the content cell stacks the `<label>` and an `<input type="text">` directly beneath it. The label always reads at full width and the
|
|
70
|
+
* field sits below at the width declared by the option's `inputSize` (5 ch when unspecified). The field never occupies a shared grid column, so its width cannot
|
|
71
|
+
* crush its own label or widen sibling rows. `inputSize` controls only the field's declared width.
|
|
72
|
+
*
|
|
73
|
+
* The row structure is uniform regardless of option kind: one row, one stacked content cell, so a long descriptive label and a compact value render through exactly
|
|
74
|
+
* the same path and differ only in the field's declared width.
|
|
75
|
+
*
|
|
76
|
+
* The element factories ({@link createCheckbox}, {@link createLabel}, {@link createValueInput}) build only the bare, state-independent shape. Every state-dependent
|
|
77
|
+
* attribute - the checkbox tri-state, the value-input's value / disabled state, the label color, row visibility, the dependency badge - is set by {@link applyRowState}
|
|
78
|
+
* once the structure exists. Sharing that single writer between construction here and the per-mutation update walk in the view is what keeps a freshly-built row and a
|
|
79
|
+
* re-derived row identical.
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} args
|
|
82
|
+
* @param {string | null} args.deviceId - The currently-selected device's serial, or null for the global view.
|
|
83
|
+
* @param {import("./selectors.mjs").ProjectionEntry} args.entry - The projection entry for this option.
|
|
84
|
+
* @param {"controller" | "device" | "global"} args.scopeKind - The current view's scope kind.
|
|
85
|
+
* @returns {HTMLDivElement} The constructed row element.
|
|
86
|
+
*/
|
|
87
|
+
export const optionRow = ({ deviceId, entry, scopeKind }) => {
|
|
88
|
+
|
|
89
|
+
const { expandedName, isGrouped, option } = entry;
|
|
90
|
+
const valueCentric = option.defaultValue !== undefined;
|
|
91
|
+
const classes = [ "fo-option-row", ...(isGrouped ? ["grouped-option"] : []) ];
|
|
92
|
+
|
|
93
|
+
const row = createElement("div", { classList: classes, id: "row-" + expandedName });
|
|
94
|
+
|
|
95
|
+
// The content cell stacks the label and, for a value-centric option, its value-input directly beneath it. The label always reads at full width and the field sits
|
|
96
|
+
// below at the declared inputSize width, so one layout serves both the compact and the descriptive case and no value-input ever occupies a shared right-hand column.
|
|
97
|
+
const content = [createLabel({ entry, expandedName })];
|
|
98
|
+
|
|
99
|
+
if(valueCentric) {
|
|
100
|
+
|
|
101
|
+
content.push(createValueInput({ option }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
row.appendChild(createCheckbox({ deviceId, expandedName, option }));
|
|
105
|
+
row.appendChild(createElement("div", { classList: ["fo-option-content"] }, content));
|
|
106
|
+
|
|
107
|
+
// The structure is in place; apply every state-dependent attribute from the projection entry. This is the same writer the view's per-mutation walk uses, so a
|
|
108
|
+
// freshly-materialized row arrives correct from its first render without a separate "set the initial state here" path that could drift from the update path.
|
|
109
|
+
applyRowState({ entry, row, scopeKind });
|
|
110
|
+
|
|
111
|
+
return row;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Apply every state-dependent attribute of an option row from its projection entry. The single source of truth for "what does this row look like given the model" -
|
|
116
|
+
* called once at construction (from {@link optionRow}) and again on every projection-affecting dispatch (from the view's update walk), so the row DOM is a pure
|
|
117
|
+
* function of the projection at all times.
|
|
118
|
+
*
|
|
119
|
+
* What it derives:
|
|
120
|
+
*
|
|
121
|
+
* - **Visibility** (`fo-hidden`) and the **dependency badge** (dimmed opacity + disabled checkbox) - a grouped option whose parent is currently disabled is shown
|
|
122
|
+
* dimmed and non-interactive when a filter or search keeps it visible; every other row carries no dim treatment.
|
|
123
|
+
* - **Checkbox tri-state** - inheriting from a higher scope reads as indeterminate + readOnly (the "borrowing from above" state the click machine cycles out of);
|
|
124
|
+
* otherwise the checkbox reflects the resolved enabled state directly.
|
|
125
|
+
* - **Label color** - the "where did this value come from / has it been modified" cue. Re-applying it here on every projection change is what makes a toggle that
|
|
126
|
+
* modifies (or reverts) an option re-color its label in place, rather than freezing the color at construction time.
|
|
127
|
+
* - **Value-input state** (value-centric options only) - readOnly / disabled track the inherit + enabled axes. The value text is re-derived from the projection
|
|
128
|
+
* EXCEPT while the user is actively editing it: uncommitted text exists only while the input holds focus (the `change` event commits on blur / Enter), so guarding
|
|
129
|
+
* on `document.activeElement` is exactly the condition under which a re-derive would clobber an in-progress edit.
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} args
|
|
132
|
+
* @param {import("./selectors.mjs").ProjectionEntry} args.entry - The projection entry for this option.
|
|
133
|
+
* @param {HTMLDivElement} args.row - The option row element to update in place.
|
|
134
|
+
* @param {"controller" | "device" | "global"} args.scopeKind - The current view's scope kind.
|
|
135
|
+
*/
|
|
136
|
+
export const applyRowState = ({ entry, row, scopeKind }) => {
|
|
137
|
+
|
|
138
|
+
const inheriting = isInheritingView(scopeKind, entry.scope);
|
|
139
|
+
|
|
140
|
+
row.classList.toggle("fo-hidden", !entry.visible);
|
|
141
|
+
row.style.opacity = entry.requiresParentBadge ? "0.5" : "";
|
|
142
|
+
|
|
143
|
+
const checkbox = row.querySelector(".fo-option-checkbox");
|
|
144
|
+
|
|
145
|
+
if(checkbox) {
|
|
146
|
+
|
|
147
|
+
// The inherit axis (indeterminate + readOnly) and the dependency-badge axis (disabled) are independent; a row can be both inheriting and parent-disabled.
|
|
148
|
+
checkbox.checked = entry.enabled;
|
|
149
|
+
checkbox.indeterminate = inheriting;
|
|
150
|
+
checkbox.readOnly = inheriting;
|
|
151
|
+
checkbox.disabled = entry.requiresParentBadge;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const label = row.querySelector(".fo-option-label");
|
|
155
|
+
|
|
156
|
+
if(label) {
|
|
157
|
+
|
|
158
|
+
applyLabelColor({ entry, inheriting, label });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const input = row.querySelector(".fo-option-value");
|
|
162
|
+
|
|
163
|
+
if(input) {
|
|
164
|
+
|
|
165
|
+
const locked = inheriting || !entry.enabled;
|
|
166
|
+
|
|
167
|
+
input.readOnly = locked;
|
|
168
|
+
input.disabled = locked;
|
|
169
|
+
|
|
170
|
+
if(locked) {
|
|
171
|
+
|
|
172
|
+
input.setAttribute("aria-disabled", "true");
|
|
173
|
+
} else {
|
|
174
|
+
|
|
175
|
+
input.removeAttribute("aria-disabled");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Never overwrite the value the user is currently editing. Outside an active edit the projection's resolved value is authoritative.
|
|
179
|
+
if(document.activeElement !== input) {
|
|
180
|
+
|
|
181
|
+
input.value = entry.value ?? defaultDisplay(entry.option);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The tri-state click-time state machine. Given the current DOM state of a clicked checkbox (post-browser-toggle) plus the projection entry, configuration index, and
|
|
188
|
+
* view context, compute the action to dispatch.
|
|
189
|
+
*
|
|
190
|
+
* Three transitions, distinguished by the checkbox's pre-call state:
|
|
191
|
+
*
|
|
192
|
+
* - **readOnly (was indeterminate)** -> the user clicked through to an explicit state at this scope. Action: set explicitly, or clear when the write rule says the
|
|
193
|
+
* entry-less resolution already matches the user's intent.
|
|
194
|
+
* - **just unchecked (was checked)** -> if an upstream entry exists, clear so resolution falls back to inheritance (the row returns to indeterminate); otherwise the
|
|
195
|
+
* explicit disable stays, recorded or normalized to a clear per the write rule.
|
|
196
|
+
* - **just checked (was unchecked)** -> set explicitly (or clear when the post-state matches default with no upstream).
|
|
197
|
+
*
|
|
198
|
+
* Write rule (parallels {@link FeatureOptions} semantics): a clearOption is correct when the resulting resolution equals the user's intent at this scope - that is,
|
|
199
|
+
* when the catalog default matches AND no upstream entry exists. In that case the entry-less lookup naturally produces the right value. A setOption is needed when
|
|
200
|
+
* any of those conditions break (default differs, value differs, upstream needs overriding).
|
|
201
|
+
*
|
|
202
|
+
* The function neither mutates nor returns DOM state: it returns only the action. The caller dispatches it, and the reactive re-projection drives the row's DOM
|
|
203
|
+
* through {@link applyRowState} against the post-dispatch projection - the resolved tri-state the projection produces is, by construction, exactly the state this
|
|
204
|
+
* transition intends, so there is no second DOM-writing path to keep in sync.
|
|
205
|
+
*
|
|
206
|
+
* @param {Object} args
|
|
207
|
+
* @param {import("./state.mjs").Catalog} args.catalog - The catalog index (for value-centric detection and upstream lookup).
|
|
208
|
+
* @param {HTMLInputElement} args.checkbox - The clicked checkbox, with its post-browser-toggle state.
|
|
209
|
+
* @param {import("../featureOptions.js").ConfigIndex} args.configIndex - The current config lookup index.
|
|
210
|
+
* @param {string | null} args.controllerId - The current controller serial, or null when no controller is in context.
|
|
211
|
+
* @param {string | null} args.deviceId - The current view's device serial, or null for global view.
|
|
212
|
+
* @param {import("./selectors.mjs").ProjectionEntry} args.entry - The projection entry for the option.
|
|
213
|
+
* @param {HTMLInputElement | null} args.inputValue - The value-input element, when the option is value-centric; null otherwise.
|
|
214
|
+
* @returns {{ action: Object }} The action to dispatch.
|
|
215
|
+
*/
|
|
216
|
+
export const triStateTransition = ({ catalog, checkbox, configIndex, controllerId, deviceId, entry, inputValue }) => {
|
|
217
|
+
|
|
218
|
+
const { expandedName, option } = entry;
|
|
219
|
+
const upstream = hasUpstreamOption({ configIndex, controllerId, deviceId, expandedName });
|
|
220
|
+
|
|
221
|
+
// Transition 1: was indeterminate (readOnly). The user clicked through to an explicit state at this scope.
|
|
222
|
+
if(checkbox.readOnly) {
|
|
223
|
+
|
|
224
|
+
return { action: writeAction({ deviceId, enabled: false, expandedName, inputValue, option, upstream, valueCentric: isValueOption(catalog, expandedName) }) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Transition 2: just transitioned to unchecked. With an upstream entry the clearOption returns the row to inheritance; without one the explicit disable stays
|
|
228
|
+
// (recorded or normalized to a clear per the write rule).
|
|
229
|
+
if(!checkbox.checked) {
|
|
230
|
+
|
|
231
|
+
if(upstream) {
|
|
232
|
+
|
|
233
|
+
return { action: { args: { id: deviceId ?? undefined, option: expandedName }, type: "option:cleared" } };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { action: writeAction({ deviceId, enabled: false, expandedName, inputValue, option, upstream, valueCentric: isValueOption(catalog, expandedName) }) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Transition 3: just transitioned to checked. Explicit enable at this scope.
|
|
240
|
+
return { action: writeAction({ deviceId, enabled: true, expandedName, inputValue, option, upstream, valueCentric: isValueOption(catalog, expandedName) }) };
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Map a view scope kind to the suffix label rendered on category headers. Switch on the discriminator; every scope kind maps to its own label.
|
|
244
|
+
const scopeLabel = (scopeKind) => {
|
|
245
|
+
|
|
246
|
+
switch(scopeKind) {
|
|
247
|
+
|
|
248
|
+
case "global":
|
|
249
|
+
|
|
250
|
+
return " (Global)";
|
|
251
|
+
|
|
252
|
+
case "controller":
|
|
253
|
+
|
|
254
|
+
return " (Controller-specific)";
|
|
255
|
+
|
|
256
|
+
case "device":
|
|
257
|
+
|
|
258
|
+
return " (Device-specific)";
|
|
259
|
+
|
|
260
|
+
default:
|
|
261
|
+
|
|
262
|
+
return "";
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Decide whether the entry is "inherited from above" relative to the current view. The view is at viewKind; the entry resolved at resolvedScope. An entry is
|
|
267
|
+
// inherited when the resolved scope is strictly higher than the view scope. The switch covers the three current scope kinds; an unrecognized kind silently resolves to
|
|
268
|
+
// "not inheriting" rather than throwing.
|
|
269
|
+
const isInheritingView = (viewKind, resolvedScope) => {
|
|
270
|
+
|
|
271
|
+
switch(viewKind) {
|
|
272
|
+
|
|
273
|
+
case "global":
|
|
274
|
+
|
|
275
|
+
// The view IS the highest scope - nothing is inherited from above.
|
|
276
|
+
return false;
|
|
277
|
+
|
|
278
|
+
case "controller":
|
|
279
|
+
|
|
280
|
+
// From a controller view, only global is higher.
|
|
281
|
+
return resolvedScope === "global";
|
|
282
|
+
|
|
283
|
+
case "device":
|
|
284
|
+
|
|
285
|
+
// From a device view, both global and controller are higher.
|
|
286
|
+
return (resolvedScope === "global") || (resolvedScope === "controller");
|
|
287
|
+
|
|
288
|
+
default:
|
|
289
|
+
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Build the checkbox element. Pure: returns a fresh element carrying only its bare, state-independent shape. The tri-state is applied by {@link applyRowState}.
|
|
295
|
+
const createCheckbox = ({ deviceId, expandedName, option }) => {
|
|
296
|
+
|
|
297
|
+
const checkbox = createElement("input", {
|
|
298
|
+
|
|
299
|
+
classList: ["fo-option-checkbox"],
|
|
300
|
+
"data-device-serial": deviceId ?? "",
|
|
301
|
+
id: expandedName,
|
|
302
|
+
name: expandedName,
|
|
303
|
+
type: "checkbox",
|
|
304
|
+
value: expandedName + (deviceId ? ("." + deviceId) : "")
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Record the option's default-on/off as the checkbox's default state, kept separate from the live `.checked` tri-state that applyRowState owns. Nothing reads this
|
|
308
|
+
// default today - there is no form reset, clone, or `:default` rule - but it keeps the element honest for any future consumer that relies on default-state semantics.
|
|
309
|
+
checkbox.defaultChecked = option.default;
|
|
310
|
+
|
|
311
|
+
return checkbox;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Build the description label for an option. The `for` attribute connects it to the checkbox so native label-for click semantics work. The color class is applied
|
|
315
|
+
// separately by {@link applyLabelColor} (via {@link applyRowState}) so the label's structure stays state-independent.
|
|
316
|
+
const createLabel = ({ entry, expandedName }) => createElement("label", {
|
|
317
|
+
|
|
318
|
+
classList: [ "fo-option-label", "user-select-none", "my-0", "py-0", "cursor-pointer" ],
|
|
319
|
+
for: expandedName
|
|
320
|
+
}, [entry.description]);
|
|
321
|
+
|
|
322
|
+
// Apply the label's scope-color class, replacing any color previously applied. The color classes are mutually exclusive, so we strip every one of them before adding
|
|
323
|
+
// the current one - this makes the function idempotent and safe to re-run on every projection change, which is what lets a toggle re-color a modified option's label
|
|
324
|
+
// in place. The construction path and the per-mutation update path share this one writer, so the initial color and every subsequent color come from the same map.
|
|
325
|
+
const applyLabelColor = ({ entry, inheriting, label }) => {
|
|
326
|
+
|
|
327
|
+
label.classList.remove("text-body", "text-info", "text-success", "text-warning");
|
|
328
|
+
label.classList.add(scopeColorClass({ entry, inheriting }));
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Map the row's display state to its Bootstrap utility class. Two cases by inheritance:
|
|
332
|
+
//
|
|
333
|
+
// - **Inheriting from a higher scope**: the row is showing a value sourced from an ancestor scope. Color identifies the source - text-success for controller,
|
|
334
|
+
// text-warning for global (warning because the global entry is the most distant source, the row is "borrowing" from far away).
|
|
335
|
+
// - **Explicit at this scope or below** (not inheriting): the entry IS set at the current view's scope (or unset entirely). When the explicit state differs
|
|
336
|
+
// from the catalog default, mark it text-info as a "this row has been modified" cue. Default-matching unset rows render text-body.
|
|
337
|
+
//
|
|
338
|
+
// The webUI's sole scope-to-class mapping. It consumes the `inheriting` boolean that {@link applyRowState} derives via {@link isInheritingView} from the view's
|
|
339
|
+
// scopeKind and the projection's `entry.scope`, rather than re-deriving that boolean itself; the switch below still reads `entry.scope` directly to pick the color.
|
|
340
|
+
const scopeColorClass = ({ entry, inheriting }) => {
|
|
341
|
+
|
|
342
|
+
if(inheriting) {
|
|
343
|
+
|
|
344
|
+
switch(entry.scope) {
|
|
345
|
+
|
|
346
|
+
case "controller":
|
|
347
|
+
|
|
348
|
+
return "text-success";
|
|
349
|
+
|
|
350
|
+
case "global":
|
|
351
|
+
|
|
352
|
+
return "text-warning";
|
|
353
|
+
|
|
354
|
+
default:
|
|
355
|
+
|
|
356
|
+
return "text-body";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Explicit at this scope or below. The modification highlight is boolean-deviation-only by design: a row lights up text-info only when its
|
|
361
|
+
// enabled-state differs from the catalog default. A value-centric option whose value was changed but whose enabled-state still matches the default is intentionally
|
|
362
|
+
// NOT highlighted - the cue tracks the boolean axis, not the value axis. A default-matching row renders plain body color.
|
|
363
|
+
if(entry.isModified && (entry.enabled !== entry.option.default)) {
|
|
364
|
+
|
|
365
|
+
return "text-info";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return "text-body";
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Build the value input for a value-centric option. Pure: returns a fresh element carrying only its bare, state-independent shape - the class set, the input type,
|
|
372
|
+
// and the width derived from the option's `inputSize` declaration (or 5 ch when unspecified), capped at the content cell's width so a wide field never overflows the
|
|
373
|
+
// row on a narrow panel. The value, readOnly, and disabled state are applied by {@link applyRowState}.
|
|
374
|
+
const createValueInput = ({ option }) => createElement("input", {
|
|
375
|
+
|
|
376
|
+
classList: [ "form-control", "shadow-none", "fo-option-value" ],
|
|
377
|
+
style: {
|
|
378
|
+
|
|
379
|
+
boxSizing: "content-box",
|
|
380
|
+
fontFamily: "var(--fo-font-monospace)",
|
|
381
|
+
maxWidth: "100%",
|
|
382
|
+
width: (option.inputSize ?? 5) + "ch"
|
|
383
|
+
},
|
|
384
|
+
type: "text"
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Render the catalog's `defaultValue` as the displayable string used by both the input element and the deviation comparison. Empty string is the consistent
|
|
388
|
+
// representation across every consumer so the input reads empty when disabled and the deviation check treats an empty input as "matches default."
|
|
389
|
+
const defaultDisplay = (option) => option.defaultValue?.toString() ?? "";
|
|
390
|
+
|
|
391
|
+
// Whether the option is set at a strictly higher scope than the current view. Drives the "fall back to inheritance" branch of the tri-state machine: a checked
|
|
392
|
+
// checkbox that goes unchecked returns to indeterminate when upstream is set, otherwise stays explicitly unchecked.
|
|
393
|
+
const hasUpstreamOption = ({ configIndex, controllerId, deviceId, expandedName }) => {
|
|
394
|
+
|
|
395
|
+
// Global view (no device) is the top - nothing above it. A device view where the device IS the controller-as-device collapses to "controller view" which only
|
|
396
|
+
// inherits from global; the controllerIsUpstream check below handles that case.
|
|
397
|
+
if(!deviceId) {
|
|
398
|
+
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const controllerIsUpstream = (controllerId !== null) && (deviceId !== controllerId);
|
|
403
|
+
|
|
404
|
+
if(controllerIsUpstream && optionExists({ configIndex, id: controllerId, option: expandedName })) {
|
|
405
|
+
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return optionExists({ configIndex, option: expandedName });
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Decide whether the post-transition state warrants writing a new entry, or whether clearing the option falls back to the default. We write when the user's intent
|
|
413
|
+
// differs from the catalog default (boolean axis OR value axis) OR when there is an upstream entry that the local state needs to override. Otherwise clearing the
|
|
414
|
+
// option is equivalent and keeps the configuredOptions array minimal.
|
|
415
|
+
const writeAction = ({ deviceId, enabled, expandedName, inputValue, option, upstream, valueCentric }) => {
|
|
416
|
+
|
|
417
|
+
const inputValueText = inputValue?.value ?? "";
|
|
418
|
+
const valueDeviates = (inputValue !== null) && (inputValueText !== defaultDisplay(option));
|
|
419
|
+
const booleanDeviates = enabled !== option.default;
|
|
420
|
+
const writeNeeded = booleanDeviates || valueDeviates || upstream;
|
|
421
|
+
const id = deviceId ?? undefined;
|
|
422
|
+
|
|
423
|
+
if(!writeNeeded) {
|
|
424
|
+
|
|
425
|
+
return { args: { id, option: expandedName }, type: "option:cleared" };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const value = (valueCentric && enabled && (inputValueText.length > 0)) ? inputValueText : undefined;
|
|
429
|
+
|
|
430
|
+
return { args: { enabled, id, option: expandedName, value }, type: "option:set" };
|
|
431
|
+
};
|