diff --git a/.github/workflows/standards-checks.yml b/.github/workflows/standards-checks.yml index 463f466..2658d3f 100644 --- a/.github/workflows/standards-checks.yml +++ b/.github/workflows/standards-checks.yml @@ -13,58 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-python@v5 with: - node-version: "22" - - name: Validate copy/adapt JavaScript - run: | - node --check reference/integration-panel-template/zoom-controller-reference.js - node --check reference/integration-panel-template/panel-shell.js - - name: Enforce NikaS UI standard v1.9 + python-version: "3.13" + - name: Enforce synchronized NikaS UI v2.2 mirror run: python scripts/check_nikas_ui_standard.py - - name: Validate v1.9 contract - run: | - python - <<'PY' - import json - from pathlib import Path - - root = Path(".") - standard = (root / "NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md").read_text(encoding="utf-8") - shell = (root / "reference/integration-panel-template/panel-shell.js").read_text(encoding="utf-8") - readme = (root / "reference/integration-panel-template/README.md").read_text(encoding="utf-8") - zoom = (root / "reference/integration-panel-template/zoom-controller-reference.js").read_text(encoding="utf-8") - navigation = (root / "docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md").read_text(encoding="utf-8") - contract = json.loads((root / "reference/integration-panel-template/panel-contract.example.json").read_text(encoding="utf-8")) - - for token in ("v1.9", "hass-toggle-menu", "12–25px", "Локально", "Данные актуальны", "GitHub Releases are not used", "exact form `UI vX.Y.Z`", "Focus-visible", "same click/keyboard handler"): - assert token in standard, token - for token in ('icon="mdi:menu"', "font-size:23px", "font-size:14px", "--mdc-icon-size:28px", 'class="canvas-viewport"', ".header-title:focus-visible", ".header-title:active", 'uiVersion: "1.0.0"', "uiVersionLine", "nikas.specialized.source_route_at.v1"): - assert token in shell, token - assert 'subtitle: "Device model · UI v' not in shell - assert "history.back(" not in shell - assert "second line is only `UI vX.Y.Z`" in readme - assert " · UI" not in readme - assert "mdi:arrow-left" not in shell - assert shell.count('class="canvas-viewport"') == 1 - assert "const DEFAULT_MIN = 0.75" in zoom - assert "const DEFAULT_MAX = 2.0" in zoom - assert "this.host?._selectedDeviceId || this.host?._selectedDevice" in zoom - assert contract["header"]["left_event"] == "hass-toggle-menu" - assert contract["header"]["title_action"] == "return_to_source_base_panel" - assert contract["header"]["browser_history_back_allowed"] is False - assert contract["header"]["title_second_line"] == "UI vX.Y.Z" - assert contract["header"]["title_plaque"]["reference"] == "LIDER" - assert contract["header"]["title_plaque"]["pressed_background_primary_mix_percent"] == 13 - assert contract["header"]["title_plaque"]["pressed_border_primary_mix_percent"] == 42 - assert contract["header"]["source_route_timestamp_key"] == "nikas.specialized.source_route_at.v1" - assert contract["header"]["allowed_source_routes"] == [ - "/dashboard-house-v11/home", - "/dashboard-actions/home", - "/dashboard-infrastructure/overview", - ] - assert contract["zoom"]["range_percent"] == [75, 200] - assert contract["rendering"]["routine_shadow_root_replacement"] is False - assert contract["connection_indicator"]["enabled"] is False - for token in ("/starline", "nikas.specialized.source_route_at.v1", "same click/keyboard handler", "blocking defect"): - assert token in navigation, token - PY diff --git a/.nikas-ui-standard.json b/.nikas-ui-standard.json index 8a8deaa..9e2aa05 100644 --- a/.nikas-ui-standard.json +++ b/.nikas-ui-standard.json @@ -1,23 +1,34 @@ { - "version": "1.9", - "navigation_contract_version": "1.1", + "version": "2.2", + "navigation_contract_version": "1.2", + "canonical_repository": "NikaSir/ha-contract-generated-ui", "standard_path": "NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md", - "standard_sha256": "a31dc45499443f8fa43eaea1906fe3a793dffcb411b10cebcd3a3a205fbcef27", + "standard_sha256": "3b6cc750b08aa0d2a375d1430ea04ac68c90525a527d197b014abd96728d23d1", "navigation_contract_path": "docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md", - "navigation_contract_sha256": "c87dc760c0900701f6a807a35aba40e790f68f52c39f4579583598f697997e52", - "role": "specialized", - "artifact_kind": "reference", - "runtime_files": ["reference/integration-panel-template/panel-shell.js"], - "header_return": { - "button_marker": "id=\"return-source\"", - "version_marker": "`UI v${normalized}`", - "focus_marker": ".header-title:focus-visible", - "pressed_marker": ".header-title:active", - "explicit_precedence_marker": "safeReturnRoute(current.searchParams.get(\"return_to\")) || safeReturnRoute(current.searchParams.get(\"from\"))", - "capture_once_marker": "this._returnRoute = resolveReturnRoute(this)", - "timestamp_required_marker": "handedOffRaw !== null && handedOffAtRaw !== null", - "future_timestamp_rejection_marker": "age >= 0" + "navigation_contract_sha256": "d495eca80345b96976c168029a96146803f3d8195b6f6fc723827b601ffb578e", + "role": "mirror", + "shell_contract": { + "version": "2.1", + "host_boundary": "ha-panel", + "header_body_px": 60, + "peer_selector_px": 52, + "bottom_nav_body_px": 64, + "content_max_width_px": 1280, + "coordinate_tolerance_px": 2, + "scroll_boundary_guard": "capture-non-passive-touchmove", + "specialized_tab_range": [ + 3, + 5 + ], + "viewport_matrix": { + "phone_portrait": "430x932", + "phone_landscape": "932x430", + "tablet_portrait": "768x1024", + "tablet_landscape": "1024x768", + "desktop": "1440x900" + } }, + "runtime_files": [], "header_reference": { "implementation": "S8 OMNI", "implementation_source": "NikaSir/ha-s8-omni", @@ -33,8 +44,8 @@ "title_plaque_reference": { "implementation": "S8 OMNI", "implementation_source": "NikaSir/ha-s8-omni", - "min_width": "min(290px,100%)", - "min_height_px": 44, + "width": "min(360px,100%)", + "height_px": 52, "padding_px": "5 14", "radius_px": 16, "primary_background_mix_percent": 5, @@ -57,5 +68,27 @@ "status_border_mix_percent": 30, "neutral_border_mix_percent": 28, "shadow": "0 4px 14px rgba(0,0,0,.055)" - } + }, + "peer_device_status_lamp_reference": { + "implementation": "Stark SolarPower", + "visual_lineage": "StarLine", + "diameter_px": 9, + "halo_px": 3, + "states": { + "good": "green", + "warning": "orange", + "fault": "red", + "unknown": "gray" + }, + "state_priority": [ + "fault", + "warning", + "good", + "unknown" + ], + "selection_is_independent": true, + "update_mode": "point-patch", + "accessible_status_required": true + }, + "frontend_delivery_path": "SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md" } diff --git a/NIKAS_INTEGRATION_PANEL_TEMPLATE.md b/NIKAS_INTEGRATION_PANEL_TEMPLATE.md index 5a63e7d..16ceca1 100644 --- a/NIKAS_INTEGRATION_PANEL_TEMPLATE.md +++ b/NIKAS_INTEGRATION_PANEL_TEMPLATE.md @@ -1,6 +1,6 @@ -# NikaS Integration Panel Template v1.9 +# NikaS Integration Panel Template v2.2 -> **SUPERSEDED FOR SHELL GEOMETRY, HEADER NAVIGATION AND GESTURES:** use `NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md` v1.9 and `NIKAS_PANEL_NAVIGATION_CONTRACT.md`. Domain composition guidance remains valid only when compatible with those documents. +> **SUPERSEDED FOR SHELL GEOMETRY, HEADER NAVIGATION AND GESTURES:** use `NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md` v2.2 and `NIKAS_PANEL_NAVIGATION_CONTRACT.md`. Domain composition guidance remains valid only when compatible with those documents. **Status:** Required reference implementation **Primary target:** iPhone Pro Max, portrait @@ -55,7 +55,7 @@ Rules: - title is geometrically centered on the viewport; - first line is the human application name; - second line contains only `UI vX.Y.Z`; type, model and context text are prohibited there; -- the whole title is the sole standard return button and copies the exact LIDER plaque geometry, surface, pressed and focus-visible states from the v1.9 standard; +- the whole title is the sole standard return button and copies the exact LIDER plaque geometry, surface, pressed and focus-visible states from the v2.2 standard; - first/second lines are `23/14px`; narrow fallback is `21/13px`; - decorative brand/device icon is not placed next to the title; - right zone contains at most one primary global action, normally Refresh, plus overflow only when genuinely needed. @@ -64,8 +64,9 @@ Rules: | Application | Parent | | --- | --- | -| ZONT | `/dashboard-house-v11/home` | -| StarLine | `/dashboard-house-v11/home` | +| Access | `/dashboard-house-v13/home` | +| ZONT | `/dashboard-house-v13/home` | +| StarLine | `/dashboard-house-v13/home` | | HO-SC-8W irrigation | `/dashboard-actions/home` | | S8 OMNI | `/dashboard-actions/home` | | Keenetic Hero 4G+ | `/dashboard-infrastructure/overview` | @@ -181,7 +182,7 @@ Primary navigation is always: - outside the content scroll region; - 3–5 primary destinations; - icon + short label; -- `ha-icon` MDI glyph at 28 px and label at 12 px/700; +- `ha-icon` MDI glyph at 26 px and label at 12 px/700; - active state highlighted inside its shared cell. Floating/pill navigation with side or bottom gaps is non-conforming. @@ -240,16 +241,15 @@ No specialized panel may import this repository or another integration at runtim ## 14. Reference implementation -The code template lives under: +The canonical build-time source kit lives in `NikaSir/ha-contract-generated-ui` under: ```text -templates/integration-panel-v1/ +templates/shell_v2/nikas-specialized-shell.js ``` It provides: -- `panel-shell-reference.js` — stable shell/reference component designed to be concatenated with the copied v1.9 zoom controller into one autonomous bundle; -- `zoom-controller-reference.js` — the copy/adapt v1.9 gesture controller; concatenate it into the integration-owned production bundle; +- `nikas-specialized-shell.js` — canonical host-bound Shell v2.2 source kit, vendored at build time into the integration-owned production bundle; - `panel-contract.example.json` — machine-readable metadata example; - `README.md` — adoption checklist. diff --git a/NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md b/NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md index f6f1518..e60fcb2 100644 --- a/NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md +++ b/NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md @@ -1,16 +1,18 @@ -# NikaS Specialized Panel UI Standard v1.9 +# NikaS Specialized Panel UI Standard v2.2 **Status:** REQUIRED **Canonical source:** `NikaSir/ha-contract-generated-ui` **Applies to:** every integration-owned specialized Home Assistant panel -**Primary acceptance viewport:** iPhone Pro Max, portrait -**Reference shell implementation:** Stark SolarPower / UPS +**Mandatory acceptance matrix:** phone portrait/landscape, tablet portrait/landscape, desktop with Home Assistant sidebar expanded/collapsed +**Normative shell reference:** the numeric host-bound geometry in this document; screenshots and individual panels are not normative **Reference Header surface and controls:** S8 OMNI **Reference connection/freshness plaque:** S8 OMNI +**Reference peer-device status lamps:** Stark SolarPower / StarLine lineage **Reference typography and domain status treatment:** LIDER -**Required navigation companion:** `docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md` +**Required navigation companion:** `docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md` v1.2 +**Canonical build-time source kit:** `templates/shell_v2/nikas-specialized-shell.js` -This document supersedes every earlier shell, Header, zoom, scrolling and Bottom Tab Bar rule. Historical documents remain useful only where they do not conflict with this standard. +This document supersedes every earlier shell, Header, zoom, scrolling and Bottom Tab Bar rule. Historical documents and named panel implementations remain useful only as visual lineage where they do not conflict with this standard. Version 2.2 keeps the v2 geometry and adds the proven peer-device selector status-lamp contract. It retains the vendored build-time shell source and current four-panel base route topology. ## 1. Ownership and topology @@ -28,15 +30,62 @@ BOTTOM TAB BAR native scale - The effective iOS safe area is consumed exactly once. - On phones the panel owns a height-locked application shell. The outer Home Assistant page must not become the scrolling surface: only the work viewport scrolls, so Header and Bottom Tab Bar stay at fixed screen coordinates. - Prevent scroll chaining from the work viewport into Home Assistant's outer document. A sticky element inside the scrolling/transformed subtree does not satisfy the fixed-chrome requirement. +- On iOS, CSS overscroll containment alone is not sufficient. The shell installs a capture-phase, non-passive `touchmove` boundary guard: a downward finger move at the top edge, an upward finger move at the bottom edge, and either vertical direction on a short view are consumed before Home Assistant can start pull-to-refresh or scroll its outer container. Interior movement on a long view remains native. - Short views fill the available work row rather than shrinking the application shell and pulling either menu toward their content. -## 2. Header — S8 OMNI reference surface and geometry +### 1.1 Home Assistant host boundary + +- The panel root is bound to the actual Home Assistant panel host, not to the browser window. Its border box must equal the current `ha-panel` content box after Home Assistant chrome and an expanded or collapsed sidebar have taken their space. +- Responsive breakpoints are evaluated from the current panel-host inline size, for example with a shell container query; the browser-window width is not a substitute when the Home Assistant sidebar consumes part of the window. +- The shell uses the host's local coordinate system: logical `inset:0`, `inline-size:100%`, `block-size:100%`, `min-inline-size:0` and `min-block-size:0`. `100vw`, a hard-coded sidebar offset and viewport-fixed positioning derived from `window.innerWidth` are prohibited. +- Opening, closing or resizing the Home Assistant sidebar must resize the shell naturally. A panel must not render below, behind or across the sidebar, and must not reserve a sidebar-sized blank strip when the sidebar is collapsed. +- If a Home Assistant version does not provide a stable percentage height, a `fixed_root` or `boundary_guard` may be used only when all four coordinates are derived from the current host rectangle. A browser-window fixed root is non-conforming. +- The outer Home Assistant document remains at scroll origin. Neither native scroll nor overscroll from the work viewport may transfer to it. +- A vertical gesture that starts on Header or Bottom Tab Bar is also consumed by the shell boundary guard; fixed chrome is never a drag handle for the outer Home Assistant page. Two-finger gestures remain available to the work-area zoom engine. + +### 1.2 Canonical shell rows + +The shell is one persistent grid with the following rows, in this order: + +```text +Header 60px + effective top safe area +Peer selector 0px, or exactly 52px when required +Work viewport minmax(0, 1fr) +Bottom Tab Bar 64px + effective bottom safe area +``` + +- Header and Bottom Tab Bar always span the complete host width on phone, tablet and desktop. +- Safe left/right insets are consumed by the corresponding shell row exactly once. +- Domain content cannot add an outer Header, footer, application margin or second safe-area reserve. +- A short page fills the work row. A long page scrolls only inside that row. Content height must never determine the vertical position of either menu. + +### 1.3 Canonical work-content frame + +- The work viewport always occupies the full middle shell row and has `min-width:0`, `min-height:0`, `overflow-y:auto`, `overflow-x:hidden` and contained overscroll. +- Its immediate content frame is centered, uses `width:100%`, `max-width:1280px`, `box-sizing:border-box`, and exactly `12px` horizontal padding below `600px`, `16px` from `600px` through `1023px`, and `24px` from `1024px` upward. +- A domain scene, diagram or card collection may choose its own internal composition, but may not change the shell width, shell rows, outer content frame or chrome coordinates. +- A narrower domain card is allowed inside the canonical content frame. Applying a domain `max-width` to the shell, Header, work viewport or Bottom Tab Bar is prohibited. + +### 1.4 Navigation levels + +- A NikaS base panel uses the global four-destination navigation: `Дом / Помещения / Действия / Инфра`. +- A specialized integration panel uses only its own 3–5 primary destinations. Five destinations, as used by Keenetic, conform to this limit. +- Global base navigation and specialized integration navigation are different levels and must never be mixed in one Bottom Tab Bar. + +### 1.5 Build-time shell source + +- Every changed panel vendors the exact canonical `templates/shell_v2/nikas-specialized-shell.js` source into its own repository and verifies its SHA-256 in CI. +- The vendored source is concatenated at build time into the panel's one autonomous production bundle. Loading Contract Generated UI or any shared remote module at runtime is prohibited. +- The kit owns only host geometry, fixed chrome, canonical work frame and navigation hand-off. Domain cards, entities, commands, routes, state models and assets remain owned by the panel repository. +- A repository may add domain CSS inside the canonical work frame, but may not override the kit's host, shell rows, Header, outer work frame or Bottom Tab Bar geometry. + +## 2. Header — canonical geometry with S8 OMNI surface The complete upper application menu copies the S8 OMNI Header, not merely its title typography. The Header surface, menu plaque, center return plaque and optional right action are one visual system. -- Grid: `52px minmax(0,1fr) 52px`; on very narrow screens: `48px minmax(0,1fr) 48px`. -- Minimum height: `62px`; phone target: `60px`, plus the effective top safe area. -- Header side padding is at least `12px` and consumes the effective left/right safe area once. The top inset is consumed by the Header once; no child plaque adds a second safe-area offset. +- The Header body is exactly `60px` high, plus the effective top safe area owned by the Header. +- Its body grid is exactly `52px minmax(0,1fr) 52px` with `12px` horizontal padding plus the effective left/right safe area. On widths below `360px`, only the two rails reduce to `48px`; the body height does not change. +- Both side rails remain reserved even when the right action is disabled or absent. The title therefore never moves when action state changes. - The Header strip uses the S8 OMNI surface: background `color-mix(in srgb,var(--primary-background-color) 97%,transparent)`, bottom border `1px solid color-mix(in srgb,var(--divider-color) 70%,transparent)`, and `backdrop-filter:blur(18px) saturate(130%)`. The iOS-prefixed equivalent may be added. - The Header strip remains a persistent mounted layer. Blur, border and background must not be toggled or remounted during scroll, pinch, telemetry changes or tab switches. - A solid integration-brand color, a transparent strip that exposes moving work content, or a card-white strip disconnected from `var(--primary-background-color)` is non-conforming. @@ -56,15 +105,15 @@ The complete upper application menu copies the S8 OMNI Header, not merely its ti - 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. - The first line is the current specialized-panel name. The second line is the interface version in the exact form `UI vX.Y.Z`. - The whole plaque is one semantic `button` and copies the S8 OMNI reference geometry and tone exactly; it retains geometric centering between the side rails. -- Default geometry: `justify-self:center`, `min-width:min(290px,100%)`, `max-width:100%`, `min-height:44px`, `padding:5px 14px`. On narrow phones it uses `min-width:0; width:100%; padding-inline:8px` so the plaque fills the available center grid column without moving the side rails. +- Default geometry: `justify-self:center`, `width:min(360px,100%)`, `height:52px`, `padding:5px 14px`. Below `360px` it uses `width:100%; padding-inline:8px` so the plaque fills the available center grid column without moving the side rails. - Reference surface: `1px` border `color-mix(in srgb,var(--primary-color,#03a9d9) 24%,var(--divider-color,#dfe3e8))`, `16px` radius, background `color-mix(in srgb,var(--primary-color,#03a9d9) 5%,var(--card-background-color,#fff))`, and shadow `0 5px 16px rgba(23,45,76,.06)`. - Pressed state: background primary mix `13%`, border primary mix `42%`, shadow `0 2px 7px rgba(23,45,76,.05)`; an optional subtle `scale(.985)` response is allowed. Focus-visible uses a `2px` primary-color outline with `2px` offset. - The focus state and pressed response are mandatory and remain visibly distinct from the default state. - A transparent title, a plain text label without the S8 OMNI surface, a white-only card surface, a wider `460px` desktop plaque forced into the phone Header, or a locally chosen integration color is non-conforming. - An arrow, chevron, a separate `Назад` label and `history.back()` are prohibited. -- When a specialized panel is opened from `/dashboard-house-v11/home`, `/dashboard-actions/home` or `/dashboard-infrastructure/overview`, it returns to that same base panel. Permitted sub-routes are normalized according to the required navigation contract. +- When a specialized panel is opened from `/dashboard-house-v13/home`, `/dashboard-rooms-v11/rooms`, `/dashboard-actions/home` or `/dashboard-infrastructure/overview`, it returns to that same base panel. Permitted sub-routes are normalized according to the required navigation contract. - The base shell records the source route in the same click/keyboard handler, immediately before changing location to the specialized panel. Ambient shell synchronization and telemetry updates must not refresh the hand-off timestamp. The common one-shot hand-off key is `sessionStorage["nikas.specialized.source_route.v1"]`; `return_to` or `from` query parameters may be used as an explicit hand-off. -- The specialized panel captures and validates the route once, persists its accepted route for that panel/client, and does not recalculate it during telemetry updates. Only same-origin routes rooted at `/dashboard-house-v11`, `/dashboard-actions` and `/dashboard-infrastructure` are accepted. +- The specialized panel captures and validates the route once, persists its accepted route for that panel/client, and does not recalculate it during telemetry updates. Only same-origin routes rooted at `/dashboard-house-v13`, `/dashboard-rooms-v11`, `/dashboard-actions` and `/dashboard-infrastructure` are accepted. - Capture precedence is: explicit `return_to`/`from`, one-shot session hand-off, saved route for that specialized panel, safe same-origin referrer, configured `parent_route`, then the repository-defined safe base-panel fallback. - Navigation is explicit Home Assistant navigation: `history.pushState()` followed by a `location-changed` event. Browser history is never the routing contract. - The title plaque, its accepted route and its click handler are mounted with the fixed Header and survive tab switches, polling, loss/recovery and every state-only patch. @@ -76,6 +125,21 @@ The complete upper application menu copies the S8 OMNI Header, not merely its ti - Selected peer persists across tabs and owns its own locally persisted scale/position state. - Hiding a selector on an aggregate view is allowed when all peers are already visible, but the same selector subtree is retained for single-peer views. +### 3.1 Peer-device status lamps — Stark SolarPower reference + +- When a peer-device selector is present, every device button carries one compact status lamp, including the selected peer. The lamp reports that device's current health while the button surface reports selection; these two meanings remain independent. +- The selected button keeps the canonical primary-color selection background, border and text. Device health must never recolor the complete selector button or replace the selection treatment. +- The reference lamp is a persistent `9px × 9px` circle inside the button, with a subtle `3px` halo mixed from the same state color. It must not change button height, width, order or label alignment. +- State priority is fail-closed: red overrides orange, orange overrides green, and an untrusted or incomplete result is gray. A lower-priority healthy fact must not conceal a higher-priority fault. +- Green means the device is reachable, its required telemetry is current and no domain fault or warning is active. +- Orange means the device remains usable but is in a documented warning state, such as battery operation, degraded/reserve operation, stale telemetry or a non-critical domain alert. +- Red means a confirmed fault, unavailable device or lost required connection. +- Gray means unknown, missing, incomplete or otherwise unreliable status. Gray is mandatory until enough factual inputs exist to classify the device safely. +- The lamp state is derived only from Home Assistant or integration-owned facts for that specific peer. Selection, last-clicked state and optimistic command results are not health inputs. +- Color is not the sole accessible signal: the button has an `aria-label` containing device name and status, and the lamp exposes the same status as a title or equivalent accessible description. +- Lamps do not blink, pulse or animate between states. State changes patch only the existing lamp class/color and accessible text; they never rebuild the selector, Header, work viewport or panel shell. +- The complete peer selector remains mounted during tab switches, telemetry polling, peer changes and loss/recovery. Unchanged lamp state produces no DOM write. + ## 4. Work viewport at 100% At exactly `scale = 1`: @@ -107,19 +171,20 @@ The former rule that one-finger transform panning also provides vertical movemen - A second finger cancels pending more-info; post-gesture synthetic clicks are briefly suppressed, while intentional stationary hold still opens native more-info. - Pinch and two-finger reset must not open history, a graph or more-info. -## 6. Bottom Tab Bar — UPS reference geometry +## 6. Bottom Tab Bar — canonical geometry with UPS visual lineage -- One fixed, edge-attached, full-width bar; never a floating card or pill. +- One persistent, edge-attached bar spanning the complete host width on every supported viewport; never a floating card or pill. - It remains outside the work viewport and above the Home Indicator. - Background: `var(--card-background-color)`; top divider and a subtle upward shadow. -- Insets: approximately `6px` top and `calc(6px + env(safe-area-inset-bottom))` bottom, with safe left/right padding. +- The native bar body is exactly `64px`: `6px` top padding, a `52px` tab target row and `6px` bottom padding. The effective bottom safe area is added below that body exactly once; effective left/right safe areas are added once. +- The complete icon/label stack must fit inside the `52px` target row: icon no larger than `26px`, stack gap `1px`, label `12px/14px`, and at least `6px` internal clearance below the label. Browser-default button typography and an implicit `normal` line-height are prohibited because they can clip Cyrillic descenders at the host edge. - All destinations have equal-width columns; 3–5 primary tabs are supported. -- Tab touch target minimum height: `52px`. -- Tab radius: approximately `16px`; compact internal padding and approximately `3px` icon/label gap. -- Icons are MDI through `ha-icon`, never text characters; canonical glyph size is `28px`. +- Tab touch target height: exactly `52px`. +- Tab radius: `16px`; compact internal padding and `1px` icon/label gap. +- Icons are MDI through `ha-icon`, never text characters; canonical glyph size is `26px`. - Labels are one line, approximately `12px`, weight `700`, readable and ellipsized only when necessary. - Inactive content uses `var(--secondary-text-color)`. -- Active icon/label use `var(--primary-color)` and an approximately 11% primary-color background, without a second shadow. +- Active icon/label use `var(--primary-color)` and an `11%` primary-color background, without a second shadow and without changing the tab's size, column width or bar height. - Navigating between tabs restores the work area to the page start before interaction resumes. - A short view must not move the bar. A long view must scroll its final control/card fully clear of the bar and Home Indicator. @@ -237,18 +302,38 @@ Repository tests or static checks must verify: 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; -13. the center title is a two-line `44px`+ semantic button, contains no arrow or separate Back label and retains geometric centering; +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 three 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()`; +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()`; 16. the hand-off route and timestamp are a required pair, reject missing, invalid, expired and future timestamps, and are both removed before candidate selection; 17. the production entrypoint is the only runtime file, is autonomous and is reproducible from its declared build inputs; 18. UI version, manifest/contract, component registration and cache key stay coherent; 19. unknown/unavailable data and product command policy are explicit and fail closed; 20. JavaScript syntax, package validation, HACS and Hassfest pass. +21. the shell border box equals the Home Assistant panel-host border box within `2px` on every edge and uses no `100vw` or hard-coded sidebar offset; +22. Header and Bottom Tab Bar span the host width and keep their coordinates within `2px` before and after work scrolling, overscroll, tab changes and sidebar toggles; +23. the title-plaque horizontal center equals the panel-host horizontal center within `2px` with the right action present, absent, enabled and disabled; +24. the outer Home Assistant scrolling element remains at origin while the work viewport scrolls; +25. the canonical shell row sizes, work-content frame, breakpoint gutters and 3–5 specialized-tab limit are machine-checked. +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. 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. -## 14. Mandatory phone acceptance +## 14. Mandatory viewport acceptance + +Every changed shell is accepted at all of these CSS viewport sizes: + +| Class | Viewport | Home Assistant state | +|---|---:|---| +| Phone portrait | `430 × 932` | mobile menu closed and opened | +| Phone landscape | `932 × 430` | mobile menu closed and opened | +| Tablet portrait | `768 × 1024` | sidebar expanded and collapsed | +| Tablet landscape | `1024 × 768` | sidebar expanded and collapsed | +| Desktop | `1440 × 900` | sidebar expanded and collapsed | + +For every matrix entry, compare the measured Header, title plaque, work viewport and Bottom Tab Bar rectangles with the canonical reference. A coordinate or size deviation greater than `2px` is a blocking shell defect. Domain content is compared only against its own approved composition. - long diagnostics pages scroll vertically at 100%; - 100% cannot move horizontally or be pulled away from the top origin; @@ -257,14 +342,17 @@ Each repository also maintains `docs/NIKAS_SPECIALIZED_PANEL_COMPLIANCE.md` (or - pinch never causes content snap-back; - card activation does not become accidental pan; - Header, selector and Bottom Tab Bar remain stationary at every scale; +- every peer selector shows one correctly classified lamp per device; selected styling remains unchanged while green/orange/red/gray health states update independently and without geometry shift; - the upper menu visually matches S8 OMNI: persistent 97% primary-background strip, divider, blur and three aligned plaques below Dynamic Island; - both Header side buttons are visible matching `44px × 44px` plaques; -- the centered title plaque shows the panel name and exact `UI vX.Y.Z`, returns to each of the three originating NikaS base panels and uses the configured safe fallback after a direct open; +- 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; - 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; +- a short tab consumes vertical edge gestures in both directions, while a long tab still scrolls naturally between Header and Bottom Tab Bar; - short views do not pull either menu inward, and long views expose their last control above the Bottom Tab Bar; - at least ten consecutive tab switches produce no blank frame, lost map/image or duplicated viewport; - loss/recovery changes telemetry and any enabled indicator in place; preserved samples are visibly stale; @@ -272,10 +360,12 @@ Each repository also maintains `docs/NIKAS_SPECIALIZED_PANEL_COMPLIANCE.md` (or - repeated telemetry and tab changes do not change the captured Header return destination or replace its click handler. - an unavailable target cannot be commanded and never flashes an optimistic success state; - a missing or stale hand-off timestamp falls back safely instead of reusing an old source route. +- expanding or collapsing the Home Assistant sidebar changes only the available host width; it does not overlap, offset twice or leave a blank sidebar reserve in the NikaS shell; +- rotating between portrait and landscape preserves one shell, one work viewport, the selected tab and valid scroll/zoom bounds. ## 15. Publication - NikaS panel and integration work is published through traceable commits, branches and pull requests. - 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 real-phone acceptance items above are ready for user verification. +- A pull request remains draft until automated checks pass and the complete viewport matrix above is ready for user verification. diff --git a/README.md b/README.md index 81dcfae..6c24b0c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository contains the default community health and engineering-standard files used across public repositories owned by `NikaSir`. -The current required specialized-panel contract is `NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md` v1.9 together with `docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md`. It includes the exact LIDER center return plaque, deterministic source-route hand-off, UPS-reference fixed Header actions, native scrolling at 100%, overflow-axis pan only above 100%, stable-DOM anti-flicker rules, the opt-in two-level connection/freshness indicator, the 12–25px typography range, the 28px Bottom Tab Bar icon rule and mandatory integration brand assets. +The current required specialized-panel contract is `NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md` v2.2 together with `docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md`. It includes the exact LIDER center return plaque, deterministic source-route hand-off, UPS-reference fixed Header actions, native scrolling at 100%, overflow-axis pan only above 100%, stable-DOM anti-flicker rules, the opt-in two-level connection/freshness indicator, the 12–25px typography range, the 26px Bottom Tab Bar icon rule and mandatory integration brand assets. ## Purpose diff --git a/SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md b/SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md index bceffc1..200dfb4 100644 --- a/SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md +++ b/SPECIALIZED_PANEL_FRONTEND_RELEASE_STANDARD.md @@ -1,7 +1,7 @@ -# NikaS Specialized Panel Frontend Delivery Standard v1.9 +# NikaS Specialized Panel Frontend Delivery Standard v1.7 **Status:** required for every integration-owned Home Assistant specialized panel -**UI authority:** [`NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md`](NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md) v1.9 and [`NIKAS_PANEL_NAVIGATION_CONTRACT.md`](docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md) +**UI authority:** [`NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md`](NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md) v2.2 ## Production artifact @@ -11,15 +11,15 @@ Modular development is allowed, but the build must produce one deterministic art ## Fixed shell acceptance -The production bundle must implement the v1.9 application shell: +The production bundle must implement the NikaS UI v2.2 application shell: - fixed Home Assistant menu Header, optional fixed peer-device selector, exactly one work viewport/canvas and fixed safe-area-aware Bottom Tab Bar; - permanent left `mdi:menu` action dispatching bubbling/composed `hass-toggle-menu`; no permanent Header Back; - centered two-line semantic title button with panel name plus version-only `UI vX.Y.Z`, visible focus/pressed states and validated return to the originating NikaS base panel; - native vertical scrolling with `x = y = 0` at 100%, focal pinch at 75–200%, bounded one-finger pan only above 100%, 97–103% snap and stationary two-finger reset; - shell mounted once, telemetry point-patched, visited views lazily cached and no full-screen flash; -- meaningful text at 12–25px and Bottom Tab Bar MDI icons/labels at 28px and 12px/700; -- optional connection/freshness indicator only when explicitly requested, using the canonical v1.9 vocabulary and status-tinted plaque; +- meaningful text at 12–25px and Bottom Tab Bar MDI icons/labels at 26px and 12px/700; +- optional connection/freshness indicator only when explicitly requested, using the canonical NikaS UI v2.2 vocabulary and status-tinted plaque; - packaged repository/integration identity including `custom_components//brand/icon.png`, minimum 256×256 RGBA. ## Required verification diff --git a/SPECIALIZED_PANEL_UI_STANDARD.md b/SPECIALIZED_PANEL_UI_STANDARD.md index 00201b0..5c67326 100644 --- a/SPECIALIZED_PANEL_UI_STANDARD.md +++ b/SPECIALIZED_PANEL_UI_STANDARD.md @@ -2,6 +2,6 @@ This filename is retained for existing repository links only. -The sole current authority is [`NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md`](NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md), version 1.9, together with its required [navigation contract](docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md). It replaces every earlier rule for Header navigation, Back, safe areas, zoom, scrolling, panning, rendering, typography, connection/freshness indicators, Bottom Tab Bar geometry and repository/integration icons. +The sole current authority is [`NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md`](NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md), version 2.2, together with its required [navigation contract](docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md) v1.2. The authoritative owner is [`NikaSir/ha-contract-generated-ui`](https://github.com/NikaSir/ha-contract-generated-ui). -Implementations must not combine historical rules with v1.9. In particular, the permanent left Header action is the Home Assistant system menu, the exact LIDER center plaque is the sole return control, and GitHub Releases and automatic release tags are not part of the NikaS panel publication workflow. +Implementations must not combine historical rules with v2.2. The permanent left Header action is the Home Assistant system menu, the centered title plaque is the sole return control, Bottom Tab Bar icons are 26px, and GitHub Releases or automatic release tags are not part of the NikaS panel publication workflow. diff --git a/docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md b/docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md index a835158..d030c8e 100644 --- a/docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md +++ b/docs/NIKAS_PANEL_NAVIGATION_CONTRACT.md @@ -1,4 +1,4 @@ -# NikaS Panel Navigation and Return Contract v1.1 +# NikaS Panel Navigation and Return Contract v1.2 **Status:** REQUIRED **Canonical owner:** `NikaSir/ha-contract-generated-ui` @@ -14,11 +14,12 @@ This contract defines public panel routes, source hand-off and deterministic ret - `safe_return_route` is the repository-defined base-panel fallback used after a direct open. - Identifiers such as `house.vehicles` are registry keys, not URL paths, and must never be supplied to `history.pushState()`. -The three base panels are: +The four base panels are: | Panel | `panel_root` | canonical `entry_route` | |---|---|---| -| House now | `/dashboard-house-v11` | `/dashboard-house-v11/home` | +| House now | `/dashboard-house-v13` | `/dashboard-house-v13/home` | +| Rooms | `/dashboard-rooms-v11` | `/dashboard-rooms-v11/rooms` | | Actions | `/dashboard-actions` | `/dashboard-actions/home` | | Infrastructure | `/dashboard-infrastructure` | `/dashboard-infrastructure/overview` | @@ -26,13 +27,15 @@ The specialized-panel registry is: | Panel | canonical route | `safe_return_route` | |---|---|---| -| ZONT | `/dashboard-zont` | `/dashboard-house-v11/home` | -| StarLine | `/starline` | `/dashboard-house-v11/home` | +| Access | `/dashboard-access-v1/home` | `/dashboard-house-v13/home` | +| ZONT | `/dashboard-zont` | `/dashboard-house-v13/home` | +| StarLine | `/starline` | `/dashboard-house-v13/home` | | S8 OMNI | `/dashboard-s8-omni` | `/dashboard-actions/home` | | HO-SC-8W | `/dashboard-irrigation` | `/dashboard-actions/home` | | Stark SolarPower | `/dashboard-ups` | `/dashboard-infrastructure/overview` | | Keenetic Hero 4G+ | `/dashboard-keenetic` | `/dashboard-infrastructure/overview` | | LIDER | `/dashboard-lider` | `/dashboard-infrastructure/overview` | +| Water Accounting | `/dashboard-water-accounting` | `/dashboard-infrastructure/overview` | `/dashboard-starline` is invalid. The registered StarLine route is `/starline`. @@ -42,7 +45,7 @@ The specialized-panel registry is: - Every integration repository exposes its registered root, canonical route, safe fallback and contract version in a machine-readable panel contract. - One active route has exactly one owner. - Every installed specialized panel has at least one visible entry link from the NikaS base interface; a sidebar-only or direct-URL-only panel is non-conforming. -- Required owners are: House now → ZONT and StarLine; Actions → S8 OMNI and HO-SC-8W; Infrastructure → Stark SolarPower and Keenetic; the Infrastructure power section → LIDER. +- Required owners are: House now → Access, ZONT and StarLine; Actions → S8 OMNI and HO-SC-8W; Infrastructure → Stark SolarPower, Keenetic and Water Accounting; the Infrastructure power section → LIDER. - A `more-info` action does not count as the required entry link. Legacy `/dashboard-house/*` pages may remain declared detail routes during migration. They are not base-panel source routes and must not be used as a return fallback. @@ -59,12 +62,14 @@ sessionStorage.setItem("nikas.specialized.source_route_at.v1", String(Date.now() Only these values are valid: ```text -/dashboard-house-v11/home +/dashboard-house-v13/home +/dashboard-rooms-v11/rooms /dashboard-actions/home /dashboard-infrastructure/overview ``` -- `/dashboard-house-v11/*` normalizes to `/dashboard-house-v11/home`. +- `/dashboard-house-v13/*` normalizes to `/dashboard-house-v13/home`. +- `/dashboard-rooms-v11/*` normalizes to `/dashboard-rooms-v11/rooms`. - `/dashboard-actions/*` normalizes to `/dashboard-actions/home`. - `/dashboard-infrastructure/*` normalizes to `/dashboard-infrastructure/overview`. - `/dashboard-house` and `/dashboard-house/*` are never stored as the NikaS base source. @@ -93,7 +98,7 @@ An invalid `return_to` must not suppress a valid `from`. The accepted route is s ## 5. Validation - Accept same-origin URL paths only. -- Accept only the three base roots listed in section 1, then normalize them to their canonical entry routes. +- Accept only the four base roots listed in section 1, then normalize them to their canonical entry routes. - Reject external origins, protocol-relative URLs, `javascript:`, `data:`, specialized-panel routes, arbitrary dashboards and legacy `/dashboard-house` routes. - Drop query and hash components unless a base-panel contract explicitly declares them canonical. - A malformed explicit parameter does not invalidate lower-precedence valid candidates. @@ -124,7 +129,7 @@ CI must fail when any of the following is false: 2. every outbound target exists and has exactly one owner; 3. every specialized panel has a declared visible base entry; 4. no source or fallback uses `/dashboard-house`, `/dashboard-starline` or a registry identifier; -5. all three base source routes normalize exactly as specified; +5. all four base source routes normalize exactly as specified; 6. both hand-off values are required, validated as a pair and consumed once; 7. direct open uses the declared safe fallback; 8. invalid or external routes are rejected; @@ -143,7 +148,7 @@ On the primary iPhone viewport verify: - each declared base card opens its actual specialized route; - the title plaque returns to the same originating base panel; -- House now returns to `/dashboard-house-v11/home`, never `/dashboard-house`; +- House now returns to `/dashboard-house-v13/home`, never `/dashboard-house`; - a direct open returns to the declared safe fallback; - refresh, polling, tab changes and peer switches do not alter the destination; - Header does not flash, rebuild or lose its click handler. diff --git a/reference/integration-panel-template/README.md b/reference/integration-panel-template/README.md index 6adcbba..8725ae7 100644 --- a/reference/integration-panel-template/README.md +++ b/reference/integration-panel-template/README.md @@ -1,150 +1,13 @@ -# NikaS Integration Panel Template v1.9 — reference implementation +# Archived integration-panel reference -This directory is the **canonical copy/adapt reference** for integration-owned specialized panels in Home Assistant NikaS. +**Status:** ARCHIVED — DO NOT ADOPT -It is **not** a shared runtime dependency. Every integration copies/adapts the source into its own repository and ships one autonomous production frontend bundle. +This directory preserves an obsolete pre-Shell-v2 implementation only for source-history review. It is not a canonical template, runtime dependency or permitted starting point for a new panel. -Applies to Stark SolarPower, HO-SC-8W, S8 OMNI, Keenetic Hero 4G+, VLESS Gateway and future specialized panels. +Current requirements: -## Canonical shell +- NikaS Specialized Panel UI Standard v2.2; +- NikaS Panel Navigation and Return Contract v1.2; +- canonical build-time source kit: [`NikaSir/ha-contract-generated-ui/templates/shell_v2/nikas-specialized-shell.js`](https://github.com/NikaSir/ha-contract-generated-ui/blob/main/templates/shell_v2/nikas-specialized-shell.js). -Every panel keeps this order: - -```text -Header -Optional Device Context Selector (native scale) -One Work Viewport / Canvas -Bottom Tab Bar -``` - -Developers change domain content, tabs and optional device context. Header geometry, card rhythm, state tones, safe-area behavior, loading shell and Bottom Tab Bar mechanics are common infrastructure. - -## Header contract - -Mobile header grid: - -```text -52 px | minmax(0,1fr) | 52 px -``` - -Narrow mobile may use: - -```text -48 px | minmax(0,1fr) | 48 px -``` - -Rules: - -- left control is only `mdi:menu` and dispatches bubbling/composed `hass-toggle-menu`; -- Back, browser history, an integration menu and device actions are prohibited in this rail; -- center is one clickable two-line title plaque: the first line is the human panel name and the second line is only `UI vX.Y.Z`; -- the plaque returns to the validated originating NikaS base panel, has visible focus/pressed states and never uses browser Back; -- the plaque copies LIDER exactly: `min-width:min(290px,100%)`, `min-height:44px`, `padding:5px 14px`, `16px` radius, primary mixes `5%/24%` and shadow `0 5px 16px rgba(23,45,76,.06)`; pressed mixes are `13%/42%` with `0 2px 7px rgba(23,45,76,.05)`; -- title is `23px/800`, subtitle `14px/560`; narrow fallback is `21/13px`; -- right side contains at most one global action, normally Refresh; -- both actions use matching `44 × 44px`, radius `16px` plaques with `25px` `ha-icon` glyphs; -- equal side rails keep the title geometrically centered. - -## Bottom Tab Bar contract - -- 3–5 primary tabs only; -- full-width, edge-attached, safe-area aware; -- outside the vertical scroll region; -- never a floating pill; -- dynamic equal-width columns from actual tab count; -- active cell uses primary icon/text plus light primary surface; -- MDI glyphs use `ha-icon` at `28px`; labels use `12px/700`; target height is at least `52px`; -- short labels; `Диагн.` is allowed only when `Диагностика` does not physically fit. - -## Mobile / desktop contract - -Primary target: **iPhone Pro Max portrait**. - -- mobile primary content is one column; -- no horizontal scrolling; -- primary content uses 100% available width with system side padding; -- card radius 20–24 px, target 22 px; -- card padding 16–20 px, target 18 px in the reference; -- vertical gap 12–16 px, target 14 px; -- desktop is an adaptation of the same hierarchy, capped at 1280 px; -- desktop is not a separate design. - -Meaningful text stays within `12–25px`. Only redundant non-interactive schematic annotations may use `9–10px`. - -## Zoom, scroll and stable rendering - -- concatenate/copy the v1.9 zoom controller into the production bundle; never import it from another repository at runtime; -- exactly one `.canvas-viewport` owns native vertical scrolling at 100%; `x = y = 0` and horizontal movement are fixed; -- pinch range is 75–200%; one-finger transform pan starts only above 100% and is axis-clamped; -- stationary two-finger double tap resets scale/position and shows `Масштаб 100%`; 97–103% snaps to 100%; -- Header, selector and Bottom Tab Bar stay outside the work canvas and at fixed phone coordinates; -- mount the application shell once; telemetry point-patches existing nodes, while visited tab/device views are lazily cached; -- routine `hass` updates must not recreate the background, viewport, Header or navigation. - -The two-level connection/freshness indicator is opt-in only. When requested, use `Локально / Облако / Резерв / Нет связи / Нет данных` and `Данные актуальны / Данные устарели / Нет данных`, with `16/13px` typography and a plaque tinted by the main status color. Do not use `Онлайн` as the transport label. - -## Shared primitives - -`panel-shell-reference.js` includes reference implementations for: - -- `PanelShell`; -- `AppHeader`; -- optional Device Context Selector; -- Hero Status; -- Metric Card; -- State Row; -- Action Card; -- Alert; -- Diagram placeholder; -- Loading skeleton; -- dynamic 3–5 item Bottom Tab Bar; -- long-press -> native Home Assistant more-info for entity-backed elements. - -## Adoption sequence - -1. Copy `zoom-controller-reference.js` and `panel-shell-reference.js` into the integration frontend source tree. -2. Rename the custom element and constants for the integration. -3. Set the panel title and numeric `X.Y.Z` UI version. Put any parent-section navigation inside the work area. -4. Define 3–5 primary tabs. -5. Enable Device Selector only for multiple peer physical devices. -6. Replace `_renderHeroStatus()` and `_renderViewContent()` with domain content. -7. Preserve Header, one viewport/canvas, stable-DOM rendering, safe-area geometry, common primitives and Bottom Tab Bar semantics. -8. Bind factual entity-backed blocks to native HA more-info on hold where practical. -9. Bind only integration/registry-provided entities, render `unknown`/`unavailable` explicitly and never invent a fallback value or entity ID. -10. Add only verified integration actions; read-only panels stay read-only and command-capable panels fail closed when their target is unavailable. -11. Build the panel plus copied v1.9 zoom controller into one autonomous production JS bundle and register it through `module_url` with query-string UI-version cache busting. -12. CI must regenerate the bundle and fail on a diff (or use an equivalent `--check` mode), reject runtime imports of previous frontend versions and validate JavaScript syntax. -13. Validate iPhone Pro Max portrait, fixed Header/Bottom during long native scroll, bounded pinch/pan/reset, no telemetry/tab flash, cold cache, full HA restart and Home Assistant Cloud/Nabu Casa before acceptance. - -## Multi-device rule - -Use Device Context Selector only for peer physical devices owned by one panel, for example two UPS units. - -```text -Header -Device Selector -Hero / selected-device content -Bottom Tab Bar -``` - -Device order is fixed. Selection persists across views. The selected device never moves to the first slot. - -Ethernet/LTE, modes, zones or workflow steps are domain context, not automatically Device Selector items. - -## Production frontend rule - -```text -Specialized Panel = self-contained production frontend bundle -``` - -Do not create runtime chains such as `v033 -> v032 -> v031 -> base`. Historical versions belong in Git, not in the browser loading path. - -## Non-goals - -The reference does not provide: - -- direct device API calls; -- raw Tuya/RCI/SNMP writes; -- fake Home Assistant entities; -- shared CDN/runtime module dependencies; -- domain-specific business logic. +New and changed panels vendor the canonical source kit at build time and ship one autonomous integration-owned production bundle. diff --git a/reference/integration-panel-template/panel-contract.example.json b/reference/integration-panel-template/panel-contract.example.json index 4038833..3253651 100644 --- a/reference/integration-panel-template/panel-contract.example.json +++ b/reference/integration-panel-template/panel-contract.example.json @@ -1,181 +1,8 @@ { - "api_version": "nikas.home-assistant/integration-panel/v1", - "template": "NikaS Integration Panel Template v1.9", - "navigation_contract_version": "1.1", - "panel": { - "id": "example", - "title": "Example Panel", - "path": "/dashboard-example", - "owner": "example_integration", - "preferred_view": "overview", - "ui_version": "1.0.0" - }, - "target": { - "primary": "iPhone Pro Max portrait", - "control_viewport_css_px": [430, 932], - "mobile_first": true, - "desktop_is_secondary": true, - "horizontal_scroll_allowed": false, - "desktop_max_content_width_px": 1280 - }, - "header": { - "grid": "52px minmax(0,1fr) 52px", - "narrow_grid": "48px minmax(0,1fr) 48px", - "left_action": "home_assistant_system_menu", - "left_icon": "mdi:menu", - "left_event": "hass-toggle-menu", - "action_plaque_px": 44, - "action_radius_px": 16, - "action_icon_px": 25, - "title_px": 23, - "subtitle_px": 14, - "narrow_title_px": 21, - "narrow_subtitle_px": 13, - "min_touch_target_px": 44, - "title_alignment": "viewport_center", - "title_is_button": true, - "title_second_line": "UI vX.Y.Z", - "title_action": "return_to_source_base_panel", - "title_plaque": { - "reference": "LIDER", - "min_width": "min(290px,100%)", - "min_height_px": 44, - "padding": "5px 14px", - "radius_px": 16, - "background_primary_mix_percent": 5, - "border_primary_mix_percent": 24, - "shadow": "0 5px 16px rgba(23,45,76,.06)", - "pressed_background_primary_mix_percent": 13, - "pressed_border_primary_mix_percent": 42, - "pressed_shadow": "0 2px 7px rgba(23,45,76,.05)", - "focus_outline_px": 2, - "focus_outline_offset_px": 2 - }, - "source_route_handoff_key": "nikas.specialized.source_route.v1", - "source_route_timestamp_key": "nikas.specialized.source_route_at.v1", - "source_route_pair_required": true, - "source_route_valid_age_ms": [0, 30000], - "source_route_partial_write_rollback": true, - "allowed_source_routes": ["/dashboard-house-v11/home", "/dashboard-actions/home", "/dashboard-infrastructure/overview"], - "browser_history_back_allowed": false, - "show_brand_icon": false, - "max_actions": 1, - "preferred_action": "refresh" - }, - "device_context": { - "enabled": false, - "placement": "below_header", - "peer_physical_devices_only": true, - "fixed_order": true, - "persist_across_views": true, - "selected_device_only_content": true - }, - "hero": { - "required_on_overview": true, - "purpose": "answer current system state", - "max_primary_statuses": 1, - "optional_compact_badge": true - }, - "cards": { - "radius_px": 22, - "padding_px": 18, - "vertical_gap_px": 14, - "shadow": "minimal" - }, - "content": { - "mobile_columns": 1, - "desktop_columns": 2, - "primitives": [ - "status_card", - "metric_card", - "state_row", - "action_card", - "alert", - "diagram" - ] - }, - "navigation": { - "mode": "full_width_fixed_bottom_tab_bar", - "floating": false, - "safe_area": true, - "outside_scroll_region": true, - "min_primary_items": 3, - "max_primary_items": 5, - "equal_dynamic_columns": true, - "icon_px": 28, - "label_px": 12, - "label_weight": 700, - "touch_target_px": 52, - "views": ["overview", "control", "diagnostics"] - }, - "semantics": { - "healthy": "green", - "active": "primary", - "warning": "amber", - "fault": "red", - "unknown": "grey", - "unknown_is_healthy": false, - "unavailable_is_healthy": false - }, - "loading": { - "show_shell": true, - "show_header": true, - "show_bottom_tab_bar": true, - "show_skeleton": true, - "blank_screen_allowed": false - }, - "long_press": { - "entity_backed_elements": "home_assistant_more_info", - "header_actions": "none", - "bottom_navigation": "none" - }, - "zoom": { - "viewport_count": 1, - "work_area_only": true, - "range_percent": [75, 200], - "native_vertical_scroll_at_100": true, - "one_finger_pan_above_100_only": true, - "axis_aware_bounds": true, - "snap_to_100_percent_range": [97, 103], - "reset_gesture": "two_finger_double_tap", - "reset_message": "Масштаб 100%", - "permanent_controls": false - }, - "rendering": { - "mount_shell_once": true, - "point_patch_telemetry": true, - "lazy_cache_tabs_and_devices": true, - "routine_shadow_root_replacement": false, - "persistent_background_and_viewport": true - }, - "connection_indicator": { - "enabled": false, - "enable_only_by_explicit_request": true, - "primary_values": ["Локально", "Облако", "Резерв", "Нет связи", "Нет данных"], - "freshness_values": ["Данные актуальны", "Данные устарели", "Нет данных"], - "primary_font_px": 16, - "freshness_font_px": 13, - "status_tinted_background": true - }, - "typography": { - "meaningful_min_px": 12, - "meaningful_max_px": 25, - "schematic_redundant_annotation_px": [9, 10] - }, - "data_truth": { - "entity_source": "integration_or_ha_registry", - "invented_entity_ids": false, - "fixed_entity_ids": "none", - "unknown_unavailable": "explicit", - "command_policy": "integration_services" - }, - "frontend_delivery": { - "mode": "self_contained_bundle", - "production_entrypoints": 1, - "deterministic_build": true, - "runtime_shared_template_dependency": false, - "runtime_previous_version_imports": false, - "cache_busting": "query_ui_version", - "github_releases": false - } + "archived": true, + "adoption_allowed": false, + "reason": "Superseded pre-Shell-v2 example", + "standard_version": "2.2", + "navigation_contract_version": "1.2", + "canonical_source": "NikaSir/ha-contract-generated-ui/templates/shell_v2/nikas-specialized-shell.js" } diff --git a/reference/integration-panel-template/panel-shell.js b/reference/integration-panel-template/panel-shell.js index a1e0202..9335569 100644 --- a/reference/integration-panel-template/panel-shell.js +++ b/reference/integration-panel-template/panel-shell.js @@ -1,7 +1,6 @@ -// NikaS Integration Panel Template v1.9 -// Canonical copy/adapt reference implementation. -// Production rule: copy/adapt into the integration repository and build one -// self-contained integration-owned frontend bundle. Never import this at runtime. +// ARCHIVED pre-Shell-v2 implementation — DO NOT COPY OR LOAD. +// Retained only for source-history review. +// Vendor the NikaS Shell v2.2 source kit from NikaSir/ha-contract-generated-ui. const APP = { title: "Example Panel", @@ -402,7 +401,7 @@ class NikaSIntegrationPanelReference extends HTMLElement { commitStableMarkup(this.shadowRoot.querySelector(".bottom-slot"), this._renderTabBar()); this._attachEntityInteractions(); - // Production bundles concatenate the v1.9 zoom controller before this + // Production bundles concatenate the legacy generation 1.9 zoom controller before this // component. No repository or network runtime import is allowed. window.NikasPanelZoom?.attach?.(this, { min: 0.75, max: 2.0 })?.bind?.(); } diff --git a/scripts/check_nikas_ui_standard.py b/scripts/check_nikas_ui_standard.py index cac8a58..5bd6541 100644 --- a/scripts/check_nikas_ui_standard.py +++ b/scripts/check_nikas_ui_standard.py @@ -26,22 +26,39 @@ def read_relative(path: str) -> str: def main() -> None: config = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) - require(config.get("version") == "1.9", "NikaS UI standard version must be 1.9") + require(config.get("version") == "2.2", "NikaS UI standard version must be 2.2") require( - config.get("navigation_contract_version") == "1.1", - "NikaS navigation contract version must be 1.1", + config.get("navigation_contract_version") == "1.2", + "NikaS navigation contract version must be 1.2", ) standard_path = config.get("standard_path", "docs/NIKAS_SPECIALIZED_PANEL_UI_STANDARD.md") standard = read_relative(standard_path) + knowledge_base_path = config.get("knowledge_base_path") + if knowledge_base_path: + knowledge_base = read_relative(knowledge_base_path) + baseline = f"Normative baseline:** NikaS Specialized Panel UI Standard v{config['version']}" + require(baseline in knowledge_base, "engineering knowledge base baseline does not match the canonical standard") + require("### 2.10 Peer status and selection are different facts" in knowledge_base, "knowledge base is missing the v2.2 peer-status lesson") digest = hashlib.sha256(standard.encode("utf-8")).hexdigest() - require(digest == config.get("standard_sha256"), "local NikaS UI standard is not the canonical v1.9 copy") + require(digest == config.get("standard_sha256"), "local NikaS UI standard is not the canonical v2.2 copy") navigation_contract = read_relative(config["navigation_contract_path"]) navigation_digest = hashlib.sha256(navigation_contract.encode("utf-8")).hexdigest() require( navigation_digest == config.get("navigation_contract_sha256"), "local NikaS navigation contract is not the canonical copy", ) + frontend_delivery_path = config.get("frontend_delivery_path") + if frontend_delivery_path: + frontend_delivery = read_relative(frontend_delivery_path) + require( + "Bottom Tab Bar MDI icons/labels at 26px and 12px/700" in frontend_delivery, + "frontend delivery standard must require 26px Bottom Tab Bar icons", + ) + require( + "Bottom Tab Bar MDI icons/labels at 28px" not in frontend_delivery, + "frontend delivery standard retains the superseded 28px icon rule", + ) for clause in ( "Center title plaque — return to the source NikaS base panel", 'sessionStorage["nikas.specialized.source_route.v1"]', @@ -55,10 +72,48 @@ def main() -> None: "Ambient shell synchronization", "Data truth and command safety", "Production bundle and version coherence", + "Home Assistant host boundary", + "Canonical shell rows", + "Canonical work-content frame", + "Mandatory viewport acceptance", + "capture-phase, non-passive `touchmove` boundary guard", + "never displays the Home Assistant refresh spinner", + "Five destinations, as used by Keenetic, conform to this limit.", + "Build-time shell source", + "/dashboard-house-v13/home", + "/dashboard-rooms-v11/rooms", + "Peer-device status lamps — Stark SolarPower reference", + "red overrides orange, orange overrides green", + "Unchanged lamp state produces no DOM write.", + "icon no larger than `26px`", + "canonical glyph size is `26px`", ): require(clause in standard, f"canonical Header-return clause missing: {clause}") + + shell = config.get("shell_contract", {}) + require(shell.get("version") == "2.1", "NikaS shell contract version must be 2.1") + require(shell.get("host_boundary") == "ha-panel", "shell must bind to the Home Assistant panel host") + require(shell.get("header_body_px") == 60, "canonical Header body must be 60px") + require(shell.get("peer_selector_px") == 52, "canonical peer selector must be 52px") + require(shell.get("bottom_nav_body_px") == 64, "canonical Bottom Tab Bar body must be 64px") + require(shell.get("content_max_width_px") == 1280, "canonical work-content max width must be 1280px") + require(shell.get("coordinate_tolerance_px") == 2, "shell coordinate tolerance must be 2px") + require( + shell.get("scroll_boundary_guard") == "capture-non-passive-touchmove", + "shell must block Home Assistant edge scrolling with the iOS boundary guard", + ) + require(shell.get("specialized_tab_range") == [3, 5], "specialized Bottom Tab Bar must contain 3–5 tabs") + expected_matrix = { + "phone_portrait": "430x932", + "phone_landscape": "932x430", + "tablet_portrait": "768x1024", + "tablet_landscape": "1024x768", + "desktop": "1440x900", + } + require(shell.get("viewport_matrix") == expected_matrix, "canonical viewport matrix drift") for clause in ( - "/dashboard-house-v11/home", + "/dashboard-house-v13/home", + "/dashboard-rooms-v11/rooms", "/dashboard-actions/home", "/dashboard-infrastructure/overview", "/starline", @@ -71,8 +126,29 @@ def main() -> None: ): require(clause in navigation_contract, f"canonical navigation clause missing: {clause}") + lamp = config.get("peer_device_status_lamp_reference", {}) + require(lamp.get("implementation") == "Stark SolarPower", "status-lamp reference must be Stark SolarPower") + require(lamp.get("diameter_px") == 9, "peer-device status lamp must be 9px") + require(lamp.get("halo_px") == 3, "peer-device status lamp halo must be 3px") + require( + lamp.get("states") == { + "good": "green", + "warning": "orange", + "fault": "red", + "unknown": "gray", + }, + "peer-device status-lamp palette drift", + ) + require( + lamp.get("state_priority") == ["fault", "warning", "good", "unknown"], + "peer-device status lamps must retain fail-closed priority", + ) + require(lamp.get("selection_is_independent") is True, "selection and device health must remain independent") + require(lamp.get("update_mode") == "point-patch", "status lamps must use point-only DOM updates") + require(lamp.get("accessible_status_required") is True, "status lamps require accessible text") + role = config.get("role") - require(role in {"base", "specialized", "readiness"}, f"unsupported NikaS UI role: {role}") + require(role in {"mirror", "registry", "base", "specialized", "readiness"}, f"unsupported NikaS UI role: {role}") runtime_files = config.get("runtime_files", []) require(isinstance(runtime_files, list), "runtime_files must be a list") require(len(runtime_files) == len(set(runtime_files)), "runtime_files must not contain duplicates") @@ -85,6 +161,12 @@ def main() -> None: "production_entrypoint must be one of the checked runtime_files", ) + if role == "mirror": + require(not runtime_files, "mirror repository must not claim a panel runtime") + require(not production_entrypoint, "mirror repository must not claim a production panel entrypoint") + require(config.get("canonical_repository") == "NikaSir/ha-contract-generated-ui", "mirror must identify the canonical repository") + return + if role == "readiness": require(not runtime_files, "readiness-only repository must not claim a panel runtime") compliance = read_relative(config["compliance_path"]) @@ -93,12 +175,64 @@ def main() -> None: require("autonomous" in compliance.lower(), "readiness record must cover the autonomous-bundle GAP") return + if role == "registry": + require(not runtime_files, "registry repository must not claim a panel runtime") + require(not production_entrypoint, "registry repository must not claim a production panel entrypoint") + source_kit = config.get("source_kit", {}) + require(source_kit.get("delivery") == "vendored-build-time", "shell kit must be vendored at build time") + require(source_kit.get("runtime_dependency") is False, "shell kit must not be a runtime dependency") + source_kit_text = read_relative(source_kit["path"]) + source_kit_digest = hashlib.sha256(source_kit_text.encode("utf-8")).hexdigest() + require(source_kit_digest == source_kit.get("sha256"), "canonical shell source-kit hash drift") + for token in ( + 'const NIKAS_SHELL_V2_VERSION = "2.1"', + "block-size:100%", + "calc(60px + env(safe-area-inset-top,0px))", + "calc(64px + env(safe-area-inset-bottom,0px))", + "max-inline-size:1280px", + "container:nikas-panel / inline-size", + "@container nikas-panel (min-width:600px)", + "@container nikas-panel (min-width:1024px)", + "--nikas-shell-tab-count", + "padding:2px 3px 6px", + "--mdc-icon-size:26px", + "line-height:14px", + "overscroll-behavior-y:none", + "shouldBlockNikasShellBoundaryMove", + "createNikasShellScrollBoundaryGuard", + "NIKAS_SHELL_BOUNDARY_THRESHOLD_PX = 4", + 'host.addEventListener("touchmove", moveTouch, { passive: false, capture: true })', + "captureNikasShellReturnRoute", + "window.history.pushState", + 'new Event("location-changed")', + "/dashboard-house-v13/home", + "/dashboard-rooms-v11/rooms", + ): + require(token in source_kit_text, f"canonical shell source-kit token missing: {token}") + for forbidden in ("100vw", "100vh", "100dvh", "position:fixed", "position: fixed"): + require(forbidden not in source_kit_text, f"host-bound shell contains forbidden marker: {forbidden}") + route_registry = config.get("route_registry", {}) + route_registry_text = read_relative(route_registry["path"]) + route_registry_digest = hashlib.sha256(route_registry_text.encode("utf-8")).hexdigest() + require(route_registry_digest == route_registry.get("sha256"), "canonical route-registry hash drift") + for route in ( + "/dashboard-house-v13/home", + "/dashboard-rooms-v11/rooms", + "/dashboard-actions/home", + "/dashboard-infrastructure/overview", + "/dashboard-access-v1/home", + "/dashboard-water-accounting", + ): + require(route in route_registry_text, f"canonical route missing from registry: {route}") + return + require(runtime_files, f"{role} repository must declare checked runtime_files") for token in ( "nikas.specialized.source_route.v1", "nikas.specialized.source_route_at.v1", - "/dashboard-house-v11/home", + "/dashboard-house-v13/home", + "/dashboard-rooms-v11/rooms", "/dashboard-actions/home", "/dashboard-infrastructure/overview", ):