This commit is contained in:
Jason Wen
2026-03-25 04:45:24 -04:00
parent 5af3b9c1a1
commit 2563dca0eb
4 changed files with 27 additions and 23 deletions
+8 -8
View File
@@ -53,7 +53,7 @@ All metadata (titles, descriptions, options, min/max/step/unit) lives **inline o
**Visibility design**: Settings are always visible. When visibility rules fail, the setting is dimmed with an UNAVAILABLE badge, so users know it exists but is not applicable.
**Enablement rules**: Greyed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `enable_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
@@ -216,7 +216,7 @@ Individual options within `multiple_button` or `option` widgets can have their o
}
```
When an option's enablement fails, that option is greyed out (disabled) but still visible.
When an option's enablement fails, that option is grayed out (disabled) but still visible.
### Show only when another setting is on
@@ -246,16 +246,16 @@ Note: Due to the "dim instead of hide" design, this setting will be dimmed (not
```json
{
"key": "OptionA",
"key": "FeatureAlpha",
"widget": "toggle",
"title": "Option A",
"enablement": [{"type": "param", "key": "OptionB", "equals": false}]
"title": "Feature Alpha",
"enablement": [{"type": "param", "key": "FeatureBeta", "equals": false}]
},
{
"key": "OptionB",
"key": "FeatureBeta",
"widget": "toggle",
"title": "Option B",
"enablement": [{"type": "param", "key": "OptionA", "equals": false}]
"title": "Feature Beta",
"enablement": [{"type": "param", "key": "FeatureAlpha", "equals": false}]
}
```
+5 -5
View File
@@ -176,7 +176,7 @@ Root
| `unit` | No | Unit label. Static: `"seconds"`. Dynamic: `{"metric": "km/h", "imperial": "mph"}` (frontend resolves based on `IsMetric` param). See [Dynamic Units](#dynamic-units). |
| `value_map` | No | Maps stored values to display labels |
| `visibility` | No | Rules controlling visibility. Settings are **never hidden**, always dimmed with UNAVAILABLE badge when rules fail. See [Visibility vs Enablement](#visibility-vs-enablement). |
| `enablement` | No | Rules controlling enabled/disabled state. Greyed out with contextual badge when rules fail. |
| `enablement` | No | Rules controlling enabled/disabled state. Grayed out with contextual badge when rules fail. |
| `blocked` | No | When `true`, this param cannot be modified remotely (device-only). Frontend shows as read-only. |
| `title_param_suffix` | No | Dynamic title suffix. Object with `param` (param key) and `values` (mapping of param values to suffix strings). |
| `sub_items` | No | Child items that appear indented below this item |
@@ -235,13 +235,13 @@ Root
- **`visibility` rules** (NEW): Settings are **never hidden**. When rules fail, the setting is **dimmed with an UNAVAILABLE badge** so users know it exists but isn't applicable. This prevents confusion and preserves UI stability.
- **`enablement` rules**: When rules fail, the setting is **greyed out** with a contextual badge explaining why (e.g., "Requires longitudinal control"). User can still see it exists.
- **`enablement` rules**: When rules fail, the setting is **grayed out** with a contextual badge explaining why (e.g., "Requires longitudinal control"). User can still see it exists.
The "dim instead of hide" approach (visibility-based dimming instead of hiding) provides better UX: settings remain discoverable, and users understand why a setting is unavailable.
## Rules Reference
Rules control **visibility** (dimmed) and **enablement** (greyed out).
Rules control **visibility** (dimmed) and **enablement** (grayed out).
- All rules in an array use **AND** logic (all must pass for the rule to pass)
- If ANY rule fails, the condition is unsatisfied
@@ -660,7 +660,7 @@ Individual options within `multiple_button` or `option` widgets can have their o
}
```
When an option's enablement fails, that option is **greyed out** (disabled but still visible and selectable). This prevents users from changing to an unavailable option but keeps the UI stable.
When an option's enablement fails, that option is **grayed out** (disabled but still visible and selectable). This prevents users from changing to an unavailable option but keeps the UI stable.
---
@@ -1057,7 +1057,7 @@ At device boot, the generator reads `settings_ui.json`, compresses it, and write
**Q: What's the difference between `visibility` and `enablement`?**
- `visibility`: hidden entirely when rules fail (user doesn't know it exists)
- `enablement`: visible but greyed out when rules fail (user sees it but can't change it)
- `enablement`: visible but grayed out when rules fail (user sees it but can't change it)
**Q: How do I test my changes locally?**
Run the generator directly to see the full output:
View File
+14 -10
View File
@@ -284,8 +284,10 @@ def check_structural(data: dict, result: ValidationResult) -> None:
if "widget" not in item:
errors.append(f"{path}: item missing required field 'widget'")
elif item["widget"] not in VALID_WIDGETS:
errors.append(f"{path}: item '{item.get('key', '?')}' has invalid widget '{item['widget']}' "
f"(must be one of {VALID_WIDGETS})")
errors.append(
f"{path}: item '{item.get('key', '?')}' has invalid widget '{item['widget']}'"
+ f" (must be one of {VALID_WIDGETS})"
)
if errors:
result.error("structural", "; ".join(errors))
@@ -298,7 +300,7 @@ def check_item_completeness(data: dict, result: ValidationResult) -> None:
all_items = collect_all_items(data)
issues: list[str] = []
for path, item in all_items:
for _path, item in all_items:
key = item.get("key", "unknown")
if "title" not in item:
issues.append(f"{key}: missing 'title'")
@@ -350,7 +352,6 @@ def check_no_duplicate_keys(data: dict, result: ValidationResult) -> None:
def check_rule_wellformedness(data: dict, result: ValidationResult) -> None:
"""Check 5: All rules have valid structure."""
all_items = collect_all_items(data)
has_errors = False
# Save current error count to detect new errors
error_count_before = len(result.failed)
@@ -358,8 +359,7 @@ def check_rule_wellformedness(data: dict, result: ValidationResult) -> None:
for path, item in all_items:
for ctx, rules in collect_rules_from_item(item):
for i, rule in enumerate(rules):
if not validate_rule(rule, f"{path} > {ctx}[{i}]", result, CAPABILITY_FIELDS):
has_errors = True
validate_rule(rule, f"{path} > {ctx}[{i}]", result, CAPABILITY_FIELDS)
# Also validate trigger_condition rules on sub_panels
for panel in data.get("panels", []):
@@ -438,15 +438,19 @@ def check_sub_panel_triggers(data: dict, result: ValidationResult) -> None:
for sp in section.get("sub_panels", []):
trigger = sp.get("trigger_key")
if trigger and trigger not in panel_keys:
errors.append(f"sub_panel '{sp.get('id', '?')}' trigger_key '{trigger}' "
f"not found in panel '{pid}'")
errors.append(
f"sub_panel '{sp.get('id', '?')}' trigger_key '{trigger}'"
+ f" not found in panel '{pid}'"
)
# Check top-level sub_panels
for sp in panel.get("sub_panels", []):
trigger = sp.get("trigger_key")
if trigger and trigger not in panel_keys:
errors.append(f"sub_panel '{sp.get('id', '?')}' trigger_key '{trigger}' "
f"not found in panel '{pid}'")
errors.append(
f"sub_panel '{sp.get('id', '?')}' trigger_key '{trigger}'"
+ f" not found in panel '{pid}'"
)
if errors:
result.error("sub-panel triggers", "; ".join(errors))