Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 157 additions & 7 deletions .nikas-ui-standard.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
"version": "2.2",
"navigation_contract_version": "1.2",
"canonical_repository": "NikaSir/ha-contract-generated-ui",
"canonical_revision": "4d229f38e2f75009263e904b52462cc232eefaba",
"standard_path": "NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md",
"standard_sha256": "3b6cc750b08aa0d2a375d1430ea04ac68c90525a527d197b014abd96728d23d1",
"standard_sha256": "2a15e5c2483f0fa959faff54cd29144ddb8c392e6da4546bd4c5034bf652c2c7",
"navigation_contract_path": "docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md",
"navigation_contract_sha256": "d495eca80345b96976c168029a96146803f3d8195b6f6fc723827b601ffb578e",
"navigation_contract_sha256": "79923d2de82ef59ab76e37491f75ba7eb7e7c4c23e62d75142744159cae64229",
"role": "mirror",
"shell_contract": {
"version": "2.1",
Expand Down Expand Up @@ -55,19 +56,30 @@
"connection_plaque_reference": {
"implementation": "S8 OMNI",
"implementation_source": "NikaSir/ha-s8-omni",
"min_height_px": 58,
"padding_px": "12 14",
"padding_px": "11 12",
"radius_px": 18,
"lamp_px": 10,
"column_gap_px": 11,
"column_gap_px": 9,
"main_font": "16px/700",
"freshness_font": "13px/600",
"success_background_mix_percent": 11,
"status_background_mix_percent": 10,
"neutral_background_mix_percent": 8,
"status_border_mix_percent": 30,
"neutral_border_mix_percent": 28,
"shadow": "0 4px 14px rgba(0,0,0,.055)"
"shadow": "0 4px 14px rgba(0,0,0,.055)",
"contract_version": "1.1",
"width_px": 168,
"height_px": 58,
"box_sizing": "border-box",
"top_px": 13,
"right_px": 13,
"coordinate_origin": "card_inner_border_edge",
"font_family": "-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Arial,sans-serif",
"main_line_height_px": 17,
"freshness_line_height_px": 14,
"text_gap_px": 3,
"geometry_overrides_allowed": false
},
"peer_device_status_lamp_reference": {
"implementation": "Stark SolarPower",
Expand All @@ -90,5 +102,143 @@
"update_mode": "point-patch",
"accessible_status_required": true
},
"frontend_delivery_path": "SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md"
"refresh_action_feedback": {
"version": "1.1",
"status": "required",
"applies_when": "refresh_action_present",
"path": "docs/NIKAS_REFRESH_ACTION_CONTRACT.md",
"sha256": "69f73e9d5949a2564f2bcef3059039cc339c12c9e8dfe71cfa9f24a45b78d2a8",
"busy_animation": "rotate",
"minimum_visible_ms": 900,
"busy_until": "request_settled_and_minimum_elapsed",
"duplicate_activation": "blocked",
"aria_busy": true,
"failure_notification": true,
"reduced_motion": "static_busy_surface",
"execution_scope": "read_only_telemetry",
"freshness": "accepted_sample_only",
"update_mode": "point-patch",
"preserve": [
"header",
"active_tab",
"peer",
"work_viewport",
"scroll",
"zoom",
"editor_draft"
],
"test_source": "production_entrypoint",
"required_cases": [
"activation",
"fast_success",
"slow_success",
"duplicate_activation",
"failure_cleanup",
"unavailable_targets",
"render_stability",
"context_preservation",
"reduced_motion",
"truthful_freshness",
"result_presentation",
"retry_during_result",
"partial_failure",
"disconnect_cleanup"
],
"browser_acceptance_required": true,
"completion_feedback": {
"visible_ms": 1400,
"success_icon": "mdi:check",
"success_color": "#43a047",
"error_icon": "mdi:alert-circle-outline",
"error_color": "#e53935",
"idle_icon": "mdi:refresh",
"idle_color": "var(--primary-color)",
"success_semantics": "all_required_requests_explicitly_succeeded",
"retry_during_result": true,
"cancel_previous_timer_on_retry": true,
"preserve_result_deadline_on_render": true,
"disconnect_cleanup": true,
"geometry_unchanged": true,
"accessible_result_name": true
}
},
"hacs_publication": {
"version": "1.0",
"status": "required",
"applies_when": "hacs_distributed_integration",
"path": "docs/NIKAS_HACS_PUBLICATION_CONTRACT.md",
"sha256": "d9d42be0fa3fab5915cb76399a0a451a4fcf740ec6da6137ee560c7ec5f3774a",
"version_source": "custom_components/<domain>/manifest.json",
"delivery_boundary": "hacs_visible_release_when_release_driven",
"main_is_delivery": false,
"release_version": "exact_manifest_version",
"release_target": "merged_main_commit_with_manifest_version",
"idempotent_publish": true,
"default_custom_assets": false,
"publication_permission": "contents_write_publication_workflow_only",
"unsupported_branch_install_after_rejection": true,
"states": {
"merged_main": "code_accepted",
"matching_release": "published_to_hacs_channel",
"hacs_installable": "delivered",
"device_acceptance": "accepted_in_operation"
},
"required_cases": [
"manifest_version",
"release_exact_match",
"release_target",
"idempotent_publish",
"hacs_validation",
"no_release_assets_by_default",
"delivery_acceptance"
]
},
"knowledge_base_path": "docs/NIKAS_ENGINEERING_KNOWLEDGE_BASE.md",
"frontend_delivery_path": "SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md",
"panel_lifecycle": {
"version": "1.0",
"status": "required",
"path": "docs/NIKAS_PANEL_LIFECYCLE_CONTRACT.md",
"sha256": "939b58f7451d0819ad155975466162383e74c99c78040107f88cf667673ea32a",
"route_owner": "installed_configuration",
"registration_before_device_io": true,
"initial_failure": "panel_remains_registered",
"unavailable_rendering": "fail_closed",
"recovery": "coordinator_or_config_entry_retry",
"required_cases": [
"registration_before_refresh",
"initial_failure_preserves_route",
"offline_bootstrap",
"retry_recovery",
"route_collision",
"unload_ownership",
"generated_manifest_ownership"
]
},
"connection_decoration_contract": {
"version": "1.1",
"status": "required_when_present",
"path": "docs/NIKAS_CONNECTION_DECORATION_CONTRACT.md",
"sha256": "0cbc29e20fd811af3e14821ca59adbe737d2e22a9a8a6822472d52cc8ed83036",
"production_browser_acceptance_required": true,
"state_layout_delta_px": 0,
"measurement_noise_px": 0.1,
"geometry_tolerance_px": 1
},
"blue_corner_reference": {
"width_px": 205,
"height_px": 205,
"top_px": -92,
"right_px": -70,
"base_color": "#03A9D9",
"background": "rgba(3,169,217,0.07)",
"opacity": 1,
"radius": "50%",
"coordinate_origin": "card_inner_border_edge",
"clip": "persistent_card_decoration_layer",
"theme_primary_dependent": false,
"geometry_overrides_allowed": false,
"pointer_events": "none",
"aria_hidden": true
}
}
82 changes: 67 additions & 15 deletions NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,33 @@ The complete upper application menu copies the S8 OMNI Header, not merely its ti
- Primary title: `23px`, weight `800`, one line; `21px` on very narrow phones.
- Secondary/version line: `14px`, weight approximately `560`, `var(--secondary-text-color)`; `13px` on very narrow phones.
- The permanent left action is only Home Assistant system menu `mdi:menu`; it dispatches bubbling/composed `hass-toggle-menu`.
- At most one panel-global action occupies the right rail. Refresh uses `mdi:refresh`.
- At most one panel-global action occupies the right rail. Refresh uses `mdi:refresh` in idle/busy state and the completion glyphs defined below.
- Menu and refresh copy the S8 OMNI side plaques exactly: `44px × 44px`, `16px` radius, `1px solid color-mix(in srgb,var(--divider-color) 72%,transparent)` border, `var(--card-background-color)` background, `0 7px 20px rgba(23,45,76,.08)` shadow and a `25px` `ha-icon` glyph.
- Menu glyph uses `var(--primary-text-color)`; refresh uses `var(--primary-color)`.
- Menu glyph uses `var(--primary-text-color)`; refresh uses `var(--primary-color)` in idle/busy state.
- Disabled global action may reduce opacity, but its plaque geometry and reserved rail remain unchanged so title centering never moves.
- A transparent refresh rail, a borderless side action, a locally selected integration color or mismatched menu/refresh geometry is non-conforming.
- Back, an integration drawer, a device command or a decorative brand icon is prohibited in the permanent left rail.

### Refresh — busy, success and error

[NikaS Refresh Action Contract v1.1](NIKAS_REFRESH_ACTION_CONTRACT.md) is required
when a refresh action is present. The same mounted button has four states:
idle arrow → busy rotation → success check or error glyph → idle arrow.

- Start the permitted read-only refresh immediately; rotate for at least 900 ms
and until the request settles, with duplicate activation blocked throughout.
- After explicit success, stop rotation and show green `mdi:check` (`#43a047`)
for 1400 ms. After failure, show red `mdi:alert-circle-outline` (`#e53935`)
for 1400 ms with a visible error message. Then restore the idle arrow/color.
- Keep the `44px × 44px` plaque, `25px` glyph and Header coordinates unchanged.
Update accessible names for busy/success/error; color alone is insufficient.
- A deliberate retry is available during the result interval. Cancel the previous
timer; neither it nor a late callback may overwrite the new request.
- Telemetry and tab changes preserve the button, result and original deadline.
Reduced motion removes rotation but retains visible busy and result states.
- The check confirms the declared HA/API request result only. It never fabricates
a device acknowledgement, sample timestamp, healthy state or fresh telemetry.

### Center title plaque — return to the source NikaS base panel

- The geometrically centered two-line title is a persistent clickable plaque and the sole standard return control from a specialized panel to the NikaS base interface.
Expand Down Expand Up @@ -212,17 +232,30 @@ The two-level indicator is introduced only by an explicit product request. It is
- Transport and freshness are independent. For example, `Облако · Данные устарели` is valid; stale data must not be relabelled as a transport outage without evidence.
- A failed current poll makes preserved telemetry `Данные устарели`. Unless a domain documents another justified threshold, a sample also becomes stale after three normal polling intervals; it becomes current again only after a new successful sample is accepted.

### Placement and geometry

- The canonical placement copies S8 OMNI: upper-right of the first operational Hero/card, in the same heading row as the current-state title. The plaque belongs to the work viewport, not the fixed Header, and therefore scales with work content.
- Use a two-column heading row with the state/title in `minmax(0,1fr)` and the plaque in an intrinsic right column. On normal phone widths the plaque receives enough room for its longest label; the S8 OMNI reference reserves approximately `minmax(168px,44%)`.
- At extremely narrow widths where the title and plaque cannot remain readable, stack the row and align the plaque to the start. Shrinking required text below the typography envelope or allowing overlap is non-conforming.
- Surface layout: `display:grid`, columns `10px minmax(0,1fr)`, vertically centered, `11px` column gap.
- Minimum height: `58px`; padding: `12px 14px`; radius: `18px`; `white-space:nowrap`; `max-width:100%`.
- Default surface before state coloring: `var(--card-background-color)` background, `1px solid color-mix(in srgb,var(--divider-color) 72%,transparent)` border and `0 4px 14px rgba(0,0,0,.055)` shadow.
- Status lamp: `10px × 10px`, fully inside the plaque, circular, never moved outside the rounded surface.
- Text block is a stable vertical flex column with `3px` gap. Main line: `16px/700`, line-height approximately `1.05`. Freshness line: `13px/600`, line-height approximately `1.05`.
- The plaque is sized from the longest permitted transport/freshness pair. It must not change width, height or alignment when state changes.
### Placement and geometry — locked contract

The mandatory companion is [NikaS Connection Plaque and Blue Corner Contract
v1.1](NIKAS_CONNECTION_DECORATION_CONTRACT.md). It restores the compact S8
width and phone insets, with an exact `58px` height instead of the former
minimum-only height. It supersedes v1.0's larger `200px × 60px` plaque.

- The plaque is exactly `168px × 58px` border-box, radius `18px`, at `top:13px;
right:13px` from the operational card's inner border edge.
- Use `11px 12px` padding, a `1px` border, internal columns `10px minmax(0,1fr)`,
`9px` column gap and a circular `10px × 10px` lamp.
- Main text is `16px/700`, line-height `17px`; freshness is `13px/600`, line-height
`14px`; text gap is `3px`. Both use the companion's exact shared font stack.
- The title occupies its own reserved area. Below `360px` outer card width it
moves below the plaque; the plaque retains its top/right position and size.
- Both elements belong to the work viewport and scale with it. Status text,
image size/loading, peer switches and telemetry must not move their anchors.
- The separate upper-right background circle is exactly `205px × 205px`,
`top:-92px; right:-70px`, radius `50%`, fill `rgba(3,169,217,0.07)`.
It is clipped by a persistent card decoration layer and never recolored by
`--primary-color`, state or device mode. It does not represent Refresh.
- No per-panel or responsive override may change these tokens. Validate actual
production computed styles and rectangles using the companion's scenarios;
merely copying the latest S8 CSS is not evidence of conformance.

### State surfaces and colors

Expand Down Expand Up @@ -301,7 +334,7 @@ Repository tests or static checks must verify:
9. brand `icon.png` exists in the shipped integration package;
10. meaningful typography stays within `12–25px`, subject only to the documented schematic exception;
11. routine telemetry cannot replace the shell, viewport, canvas, background or Bottom Tab Bar;
12. an optional connection indicator, when requested, uses the canonical transport/freshness vocabulary, S8 OMNI geometry and exact state-tinted surface percentages;
12. a requested connection indicator and enabled blue corner satisfy `NIKAS_CONNECTION_DECORATION_CONTRACT.md` v1.1, including exact tokens, all label lengths, stable DOM and zero state-caused geometry movement; transport/freshness vocabulary and state-tinted surface percentages remain canonical;
13. the center title is a two-line, exactly `52px` high semantic button, contains no arrow or separate Back label and retains geometric centering;
14. every reset path normalizes and persists `{scale:1,x:0,y:0}` and native scroll origin;
15. source-route capture follows `NIKAS_PANEL_NAVIGATION_CONTRACT.md`, uses the four canonical base entry routes, writes the common session hand-off at outbound click/keyboard time, consumes it once, performs explicit HA navigation and contains no `history.back()`;
Expand All @@ -318,6 +351,8 @@ Repository tests or static checks must verify:
26. every Bottom Tab Bar label is fully visible, including Cyrillic descenders, with the sidebar expanded and collapsed and in every mandatory viewport.
27. the non-passive touch boundary guard blocks Home Assistant pull-to-refresh and outer scrolling at both work-viewport edges without replacing native interior scrolling or two-finger zoom.
28. a peer-device selector, when present, keeps one persistent accessible status lamp per device, preserves selection styling independently, applies the green/orange/red/gray fail-closed state contract and updates lamps without replacing selector DOM.
29. an owned panel route is registered before fallible device I/O, remains present after initial failure and is removed only by its exact owner, as required by `NIKAS_PANEL_LIFECYCLE_CONTRACT.md`.
30. a refresh action satisfies `NIKAS_REFRESH_ACTION_CONTRACT.md` v1.1, including the 1400 ms green check/red error state, truthful completion, retry/timer isolation and stable geometry.

Each repository also maintains `docs/NIKAS_SPECIALIZED_PANEL_COMPLIANCE.md` (or an equivalent explicit record). Unimplemented runtime behavior is recorded as `GAP`, never assumed to pass from documentation alone.

Expand Down Expand Up @@ -348,7 +383,7 @@ For every matrix entry, compare the measured Header, title plaque, work viewport
- the centered title plaque shows the panel name and exact `UI vX.Y.Z`, returns to each of the four originating NikaS base panels and uses the configured safe fallback after a direct open;
- Bottom icons and labels match the Stark SolarPower visual scale;
- integration/repository icon is present and recognizable in installed/distribution surfaces.
- a requested connection indicator visually matches S8 OMNI: `58px` minimum height, `18px` radius, internal `10px` lamp, stable two-line text and state-specific surface without geometry movement;
- a requested connection indicator has the exact `168px × 58px` box, `13px` top/right inset, `18px` radius, internal `10px` lamp and fixed `16/700` + `13/600` text; an enabled blue corner has the exact `205px` circle and fixed fill/anchor from `NIKAS_CONNECTION_DECORATION_CONTRACT.md`;
- repeated telemetry, indicator transitions, tab changes and upward/downward scroll produce no full-screen flash or white frame;
- scrolling the work area never moves Header, peer selector or Bottom Tab Bar;
- pulling downward at the top of any tab never displays the Home Assistant refresh spinner or splash screen; dragging upward at the bottom never moves the complete panel or leaves a blank field below it;
Expand All @@ -369,3 +404,20 @@ For every matrix entry, compare the measured Header, title plaque, work viewport
- GitHub Releases are not used.
- Automatic release tags are not used as a publication gate or update channel. An internal integration/UI version does not require a Git tag.
- A pull request remains draft until automated checks pass and the complete viewport matrix above is ready for user verification.

## 16. Panel lifecycle and availability

[NikaS Panel Lifecycle Contract v1.0](NIKAS_PANEL_LIFECYCLE_CONTRACT.md) is a
required companion to this standard.

- A configured panel route is application infrastructure, not telemetry state.
- The owner registers its route before fallible first device/cloud/coordinator I/O,
or uses integration-wide registration when the route is not entry-specific.
- `async_config_entry_first_refresh()` and successful device discovery must never
be prerequisites for route existence.
- Initial failure mounts explicit unavailable/no-data content and preserves Header,
navigation and sidebar entry.
- Recovery uses the normal coordinator/config-entry retry lifecycle and patches the
existing panel.
- Generated panel existence follows enabled manifest/configuration state; entity
availability affects content only.
Loading
Loading