From 1546639a621992dba0bf373f395a7d3367205965 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:15:36 +0700 Subject: [PATCH 01/17] fix(studio): improve readable ribbon, consistent hover controls and vertically filled injection layout --- apps/arstack_studio/qml/ModernRibbon.qml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/arstack_studio/qml/ModernRibbon.qml b/apps/arstack_studio/qml/ModernRibbon.qml index 27fdd8139..bf9a737ab 100644 --- a/apps/arstack_studio/qml/ModernRibbon.qml +++ b/apps/arstack_studio/qml/ModernRibbon.qml @@ -47,23 +47,23 @@ Rectangle { spacing: 6 RowLayout { - Layout.preferredWidth: bar.compact ? 190 : 224 + Layout.preferredWidth: bar.compact ? 220 : 270 Layout.alignment: Qt.AlignVCenter spacing: 8 Rectangle { - Layout.preferredWidth: 48 - Layout.preferredHeight: 48 - width: 48 - height: 48 + Layout.preferredWidth: 56 + Layout.preferredHeight: 56 + width: 56 + height: 56 radius: 10 color: "#0d151d" border.width: 1 border.color: bar.theme.lineSoft Image { anchors.centerIn: parent - width: 34 - height: 34 + width: 44 + height: 44 source: Qt.resolvedUrl("../assets/material/smv_injector.svg") sourceSize.width: 68 sourceSize.height: 68 @@ -75,7 +75,7 @@ Rectangle { text: "SMV Injector" color: bar.theme.text font.family: bar.uiFont - font.pixelSize: bar.compact ? 17 : 19 + font.pixelSize: bar.compact ? 20 : 22 font.weight: Font.Bold elide: Text.ElideRight verticalAlignment: Text.AlignVCenter From 28aa514b1bcfaa859b4deaa727c7fefe927ebb27 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:15:41 +0700 Subject: [PATCH 02/17] fix(studio): improve readable ribbon, consistent hover controls and vertically filled injection layout --- apps/arstack_studio/qml/CalmButton.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/CalmButton.qml b/apps/arstack_studio/qml/CalmButton.qml index 882788bfe..7fee4d9f7 100644 --- a/apps/arstack_studio/qml/CalmButton.qml +++ b/apps/arstack_studio/qml/CalmButton.qml @@ -28,6 +28,7 @@ Button { tone === "accent" ? (hovered ? "#1d3c5e" : theme.accentSoft) : (down ? "#1a2530" : hovered ? theme.raisedHover : "#141c25") readonly property color activeEdge: !enabled ? theme.lineSoft : + hovered ? "#aac9e9" : tone === "success" ? "#347a59" : tone === "danger" ? "#86434b" : tone === "accent" ? "#315f8d" : @@ -39,9 +40,9 @@ Button { tone === "accent" ? "#dcebff" : theme.textSoft contentItem: Row { - // Both caption and icon belong to the same centered Control item. - spacing: control.iconSource.toString().length > 0 ? 7 : 0 + // Center the complete icon+caption pair in both axes in Qt Controls. anchors.centerIn: parent + spacing: control.iconSource.toString().length > 0 ? 7 : 0 Image { visible: control.iconSource.toString().length > 0 width: control.iconSize From 73994a84e18ee4d866194ac7c4b4a84cbbcffda8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:15:46 +0700 Subject: [PATCH 03/17] fix(studio): improve readable ribbon, consistent hover controls and vertically filled injection layout --- apps/arstack_studio/qml/StudioDarkCheckBox.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/StudioDarkCheckBox.qml b/apps/arstack_studio/qml/StudioDarkCheckBox.qml index a50529053..8fd0d320a 100644 --- a/apps/arstack_studio/qml/StudioDarkCheckBox.qml +++ b/apps/arstack_studio/qml/StudioDarkCheckBox.qml @@ -7,7 +7,10 @@ CheckBox { id: control property var theme property string uiFont: "Inter" - implicitHeight: 30 + implicitHeight: 32 + // Keep one fixed indicator geometry at rest, during hover and focus. + // Native style decorations must not replace this explicit indicator. + background: Item { implicitWidth: 1; implicitHeight: 1 } implicitWidth: Math.max(28, contentItem.implicitWidth + 30) leftPadding: 1 rightPadding: 2 @@ -25,8 +28,8 @@ CheckBox { radius: 5 color: control.checked ? control.theme.accent : control.theme.surface2 border.width: 1 - border.color: control.checked || control.hovered ? control.theme.accent - : control.activeFocus ? control.theme.textSoft : control.theme.line + border.color: control.checked || control.hovered || control.activeFocus + ? control.theme.accent : control.theme.line Text { anchors.centerIn: parent visible: control.checked From 10b9e280f4915c79e196dec33c6e811a176bdacf Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:15:52 +0700 Subject: [PATCH 04/17] fix(studio): improve readable ribbon, consistent hover controls and vertically filled injection layout --- apps/arstack_studio/qml/RibbonAction.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/arstack_studio/qml/RibbonAction.qml b/apps/arstack_studio/qml/RibbonAction.qml index 8e5bf1ff3..5f78a7402 100644 --- a/apps/arstack_studio/qml/RibbonAction.qml +++ b/apps/arstack_studio/qml/RibbonAction.qml @@ -40,6 +40,7 @@ Button { : tone === "accent" || checked ? "#e1f1ff" : theme.textSoft contentItem: ColumnLayout { spacing: 4 + anchors.centerIn: parent // Controls own the content rectangle; centering is declarative // on the layout itself, rather than anchoring Qt Controls content. Item { Layout.fillHeight: true } From fdab1cdc950d0ee4de62f70af9ae1c6ea204fa52 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:15:57 +0700 Subject: [PATCH 05/17] fix(studio): improve readable ribbon, consistent hover controls and vertically filled injection layout --- apps/arstack_studio/qml/Main.qml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 4ac8d2779..05a29a3c6 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1080,10 +1080,10 @@ ApplicationWindow { ColumnLayout { Layout.fillWidth: true - // Use all available vertical workspace rather than - // a fixed 384px island with empty space below. + // Stack Voltage and Current vertically; allocate + // the available height between the two matrices. Layout.fillHeight: true - Layout.preferredHeight: root.compactLayout ? 350 : 380 + Layout.preferredHeight: root.compactLayout ? 350 : 450 // Allow 960x620 minimum viewport to shrink without // clipping timing and quality rows; on larger // viewports this column expands into spare space. @@ -1171,7 +1171,8 @@ ApplicationWindow { compact: root.compactLayout } } - Item { Layout.fillHeight: true } + // No trailing filler: vertically stacked matrices + // expand to use the otherwise blank injection space. } } From 2895c6d07e95818104b8b0d3c602dc2cc5454724 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:16:39 +0700 Subject: [PATCH 06/17] fix(studio): avoid conflicting anchors in ribbon Controls content layout --- apps/arstack_studio/qml/RibbonAction.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/RibbonAction.qml b/apps/arstack_studio/qml/RibbonAction.qml index 5f78a7402..2855e1436 100644 --- a/apps/arstack_studio/qml/RibbonAction.qml +++ b/apps/arstack_studio/qml/RibbonAction.qml @@ -40,9 +40,8 @@ Button { : tone === "accent" || checked ? "#e1f1ff" : theme.textSoft contentItem: ColumnLayout { spacing: 4 - anchors.centerIn: parent - // Controls own the content rectangle; centering is declarative - // on the layout itself, rather than anchoring Qt Controls content. + // Qt Quick Controls sizes the content rectangle; equal stretchers + // center the icon and caption vertically without anchor conflicts. Item { Layout.fillHeight: true } Image { visible: control.iconSource.toString().length > 0 From 474e1d4fa57fbeb4b1082d5f2bb96b26343d7825 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:17:23 +0700 Subject: [PATCH 07/17] test(studio): lock snapshot START, full 4I+4V signed phases and readable UI components --- .../tests/check_operator_workspace.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 184b54d46..304ba9c0b 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -73,9 +73,26 @@ def main() -> None: and main_qml.index("id: voltageMatrix") < main_qml.index("id: currentMatrix"), "Voltage and Current need a vertical, flexible-height editor workspace") ribbon_qml = source("ModernRibbon.qml") - require("Layout.preferredWidth: bar.compact ? 190 : 224" in ribbon_qml - and "font.pixelSize: bar.compact ? 17 : 19" in ribbon_qml, - "SMV Injector logo/caption must be instrument-sized, not micro branding") + require("Layout.preferredWidth: bar.compact ? 220 : 270" in ribbon_qml + and "font.pixelSize: bar.compact ? 20 : 22" in ribbon_qml + and "width: 44" in ribbon_qml, + "SMV Injector branding must be readable at compact and normal widths") + device_controller = (STUDIO / "src/DeviceController.cpp").read_text(encoding="utf-8") + workflow = source("WorkflowBar.qml") + require("const scenario = controller.startScenarioSnapshot()" in workflow + and "smartSession.requestStartWithScenario(scenario)" in workflow + and "smartSession.requestStart()" not in workflow, + "every START must transmit all 8 visible setpoints, never reuse stale board angles") + require('QStringLiteral("IA"), QStringLiteral("IB")' in device_controller + and 'QStringLiteral("IC"), QStringLiteral("IN")' in device_controller + and 'QStringLiteral("UA"), QStringLiteral("UB")' in device_controller + and 'QStringLiteral("UC"), QStringLiteral("UN")' in device_controller + and 'commands << QStringLiteral("START");' in device_controller + and 'const auto phaseMdeg = std::llround(angleMdeg);' in device_controller, + "START transaction must retain every channel and signed millidegree") + require("anchors.centerIn: parent" in source("CalmButton.qml") + and "Text.AlignHCenter" in source("RibbonAction.qml"), + "control captions must center on both axes") checkbox_qml = source("StudioDarkCheckBox.qml") require("width: 20" in checkbox_qml and "height: 20" in checkbox_qml, "checkbox geometry must remain unchanged by hover") From 513c3272fdc25c9dc3e465205a260cf5086ffaca Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:19:36 +0700 Subject: [PATCH 08/17] fix(studio): dark caption on white hover for Advanced icon/text tool buttons --- apps/arstack_studio/qml/DarkToolButton.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/DarkToolButton.qml b/apps/arstack_studio/qml/DarkToolButton.qml index f21242095..0b00d1357 100644 --- a/apps/arstack_studio/qml/DarkToolButton.qml +++ b/apps/arstack_studio/qml/DarkToolButton.qml @@ -29,7 +29,7 @@ ToolButton { visible: control.iconSource.toString().length === 0 anchors.fill: parent text: control.text - color: control.enabled ? (control.hovered ? "#ffffff" : theme.textSoft) : theme.muted2 + color: control.enabled ? (control.hovered ? "#142334" : theme.textSoft) : theme.muted2 font: control.font horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -37,7 +37,7 @@ ToolButton { } background: Rectangle { radius: 6 - color: control.down ? theme.accentSoft : (control.hovered ? theme.raisedHover : "transparent") + color: control.down ? "#d6e6f9" : (control.hovered ? "#edf5ff" : "transparent") border.width: control.activeFocus ? 1 : 0 border.color: theme.accent } From fa6c60a602ca2af30d33d742590df6241a0d8191 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:19:54 +0700 Subject: [PATCH 09/17] test(studio): keep readable Advanced tool hovers and single-size checkbox visual --- apps/arstack_studio/tests/check_operator_workspace.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 304ba9c0b..575dcab72 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -99,6 +99,12 @@ def main() -> None: require("hovered ? \"#edf5ff\"" in source("CalmButton.qml") and "hovered ? \"#142334\"" in source("CalmButton.qml"), "light hover background must always pair with dark caption") + dark_tool = source("DarkToolButton.qml") + require('control.hovered ? "#142334"' in dark_tool + and 'control.hovered ? "#edf5ff"' in dark_tool, + "Advanced text/icon tool buttons must use dark captions on white hover") + require("background: Item { implicitWidth: 1; implicitHeight: 1 }" in checkbox_qml, + "checkbox hover must not render a second platform-native indicator") menu = source("StudioActionMenu.qml") cmake = (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8") From 7a2826c444b4685f3bddd9473ade5e0bebcb12f2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:22:23 +0700 Subject: [PATCH 10/17] test(studio): pin verified SV/PTP phase-evidence profile across future GUI builds --- .../tests/check_operator_workspace.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 575dcab72..315361a29 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -25,6 +25,22 @@ def ordered(data: str, first: str, second: str, reason: str) -> None: def main() -> None: + # Lock the successful 120-second PTP/SV baseline in the operator CI. + # A GUI-only change must not silently restore the old free-running bundle. + project_root = STUDIO.parent.parent + firmware_profile = (project_root / ".github/workflows/esp32p4-smv-injector.yml").read_text(encoding="utf-8") + preview_profile = (project_root / ".github/workflows/arstack-studio-native-preview.yml").read_text(encoding="utf-8") + require("CONFIG_AR_SMV_PTP_PHASE_SERVO_EXPERIMENTAL=y" in firmware_profile + and "CONFIG_AR_SMV_HW_TX_EVIDENCE_EXPERIMENTAL=y" in firmware_profile + and "# CONFIG_AR_SMV_BROADCAST_MIRROR is not set" in firmware_profile + and "phase_sync_preview: true" in preview_profile, + "Studio native preview must ship measured PTP-SV AUTO firmware with no diagnostic mirror") + sv_gate = (project_root / "include/ariec61850/time_sync/sv_tx_egress_evidence.hpp").read_text(encoding="utf-8") + sv_tests = (project_root / "tests/test_sv_tx_egress_evidence.cpp").read_text(encoding="utf-8") + require("kMaxSoftwarePhaseBoundUs = 4U" in sv_gate + and "kMaxCanonicalTxOffsetNs = 250000LL" in sv_gate + and "check_auto_local_sync_gate_requires_both_clocks_and_fresh_mac" in sv_tests, + "retain independently bounded PTP sample-phase and nonzero MAC publication latency") main_qml = source("Main.qml") preview = source("SignalPreview.qml") tray = source("StudioTimingTray.qml") From e7e197ed3a4ca4542124f75cf9824931d2d2bd3f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:23:29 +0700 Subject: [PATCH 11/17] fix(studio): Advanced dropdown hover uses white surface and dark readable option text --- apps/arstack_studio/qml/StudioDarkComboBox.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/StudioDarkComboBox.qml b/apps/arstack_studio/qml/StudioDarkComboBox.qml index 60f7afb00..3e84add7c 100644 --- a/apps/arstack_studio/qml/StudioDarkComboBox.qml +++ b/apps/arstack_studio/qml/StudioDarkComboBox.qml @@ -52,9 +52,10 @@ ComboBox { width: combo.width - 8 implicitHeight: 36 highlighted: combo.highlightedIndex === index + hoverEnabled: true contentItem: Text { text: combo.textAt(choice.index) - color: combo.theme.text + color: choice.highlighted || choice.hovered ? "#142334" : combo.theme.text font: combo.font verticalAlignment: Text.AlignVCenter elide: Text.ElideRight @@ -63,7 +64,7 @@ ComboBox { } background: Rectangle { radius: 5 - color: parent.highlighted ? combo.theme.accentSoft : combo.theme.raised + color: choice.highlighted || choice.hovered ? "#edf5ff" : combo.theme.raised } } From 03d49a93ef209290ff51080927f321182b128eb2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:23:45 +0700 Subject: [PATCH 12/17] test(studio): prevent white-on-white Advanced dropdown hover regressions --- apps/arstack_studio/tests/check_operator_workspace.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 315361a29..7ced7ed63 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -115,6 +115,10 @@ def main() -> None: require("hovered ? \"#edf5ff\"" in source("CalmButton.qml") and "hovered ? \"#142334\"" in source("CalmButton.qml"), "light hover background must always pair with dark caption") + combo = source("StudioDarkComboBox.qml") + require('choice.highlighted || choice.hovered ? "#142334"' in combo + and 'choice.highlighted || choice.hovered ? "#edf5ff"' in combo, + "Advanced dropdown options must be readable on white hover") dark_tool = source("DarkToolButton.qml") require('control.hovered ? "#142334"' in dark_tool and 'control.hovered ? "#edf5ff"' in dark_tool, From 9a34990fa74a0e20547fa71e35f5f212f24cf74a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:27:18 +0700 Subject: [PATCH 13/17] docs: freeze 120s SV/PTP local-lock wire checkpoint and table-to-SV fidelity acceptance --- docs/ARSTACK_SV_PTP_LOCK_BASELINE_20261018.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/ARSTACK_SV_PTP_LOCK_BASELINE_20261018.md diff --git a/docs/ARSTACK_SV_PTP_LOCK_BASELINE_20261018.md b/docs/ARSTACK_SV_PTP_LOCK_BASELINE_20261018.md new file mode 100644 index 000000000..2b9a5108e --- /dev/null +++ b/docs/ARSTACK_SV_PTP_LOCK_BASELINE_20261018.md @@ -0,0 +1,20 @@ +# ARStack Studio — SV/PTP non-regression baseline (2026-10-18) + +## Frozen engineering evidence +Operator's latest 120s ASIX PCAP contained 480,294 canonical 4I+4V SV frames on APPID 0x4000, svID ARSTACK_SV01, 4,000 fps, VLAN 0 PCP 4; **zero** smpCnt discontinuities (120 modulo-4000 wraps), every frame smpSynch=1 LOCAL, and no broadcast diagnostic mirror. There were 120 each of PTP Announce, Sync and matched Follow_Up on domain 0, all from ARStack's own local SOURCE. Correlation between host-received smpCnt=0 frames and PTP timestamp epochs yielded ~+48.9 us median / ~+67.2 us p95 across 120 matched observations; these are **Windows/ASIX host RX offsets**, not actual hardware sampling phase error. + +This is strong wire-level evidence for stable local-epoch alignment, **not** a certificate of external GM synchronization, cross-vendor acceptance, or microsecond hardware phase accuracy. + +## CI release guardrails +- Native Windows Preview must embed firmware from its own exact commit with phase servo ON, canonical MAC TX timestamp evidence ON, and diagnostic mirror OFF. Never quietly fall back to the old free-running firmware. +- Preserve fail-closed AUTO=0 when clock/sample-phase evidence is absent; qualified SOURCE-local AUTO=1 is allowed; never imply AUTO=2 global time traceability without evidence. +- Independently measure GPTimer sample-epoch phase (4us implementation threshold) and MAC SV transmission publication latency (nonzero is expected, admission range currently [-4us,+250us]); do not conflate the two. +- Never modify canonical EtherType, APPID 0x4000, svID, VLAN, confRev, ASDU 4I+4V payload, sample-counter continuity or PTP profile as collateral of UI work. +- Every START must use a fresh snapshot of all eight displayed waveform setpoints: IA IB IC IN UA UB UC UN, preserving signed angle millidegrees, magnitude scaling, enable flags and quality. A bare START that can reuse old board setpoints is forbidden. +- GUI angles are user setpoints, not independent firmware readback. **Do not claim actual phase injection matches until a same-run SET transaction, board SHOW phase_mdeg, and decoded SV waveform agree**. +- Default PTP Source/AUTO, Phasor ON and Wave ON; the operator should not encounter additional dialog gates for normal PTP ON and SMV Start. + +## Follow-up acceptance +With GUI Ia=-30°, Ib=-150°, Ic=+90°, capture a new immediately adjacent SV waveform *after* starting with that exact table. Verify the board's per-channel phase_mdeg (e.g. IA=-30000), then compare LS-fitted raw SV instantaneous waveform phase to the requested values. Repeat after STOP/START and reconnect. Verify PTP AUTO local acquisition and smpCnt continuity remain intact. + +This baseline intentionally does not claim SIPROTEC 5 lock or IED compatibility without actual subscriber evidence. From 8ce086fac68db5ed42c8b463d656dce62ebf80fc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:29:27 +0700 Subject: [PATCH 14/17] fix(studio): pair white Advanced button hover with dark icon artwork too --- apps/arstack_studio/qml/CalmButton.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/qml/CalmButton.qml b/apps/arstack_studio/qml/CalmButton.qml index 7fee4d9f7..924570197 100644 --- a/apps/arstack_studio/qml/CalmButton.qml +++ b/apps/arstack_studio/qml/CalmButton.qml @@ -10,6 +10,7 @@ Button { property url iconSource property real iconSize: 16 property string toolTipText: "" + readonly property string hoverPath: iconSource.toString().replace("/assets/material/", "/assets/material_dark/") implicitHeight: theme ? theme.controlHeight : 36 implicitWidth: Math.max(64, contentItem.implicitWidth + 20) @@ -48,7 +49,7 @@ Button { width: control.iconSize height: control.iconSize anchors.verticalCenter: parent.verticalCenter - source: control.iconSource + source: control.hovered && control.enabled ? control.hoverPath : control.iconSource sourceSize.width: control.iconSize * 2 sourceSize.height: control.iconSize * 2 opacity: control.enabled ? 0.94 : 0.30 From 62baf84c4a57adc0243b9745b6e151cdacda0614 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:29:45 +0700 Subject: [PATCH 15/17] test(studio): preserve dark icon/caption on light Advanced hover --- apps/arstack_studio/tests/check_operator_workspace.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 7ced7ed63..e21ba4bcd 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -113,8 +113,9 @@ def main() -> None: require("width: 20" in checkbox_qml and "height: 20" in checkbox_qml, "checkbox geometry must remain unchanged by hover") require("hovered ? \"#edf5ff\"" in source("CalmButton.qml") - and "hovered ? \"#142334\"" in source("CalmButton.qml"), - "light hover background must always pair with dark caption") + and "hovered ? \"#142334\"" in source("CalmButton.qml") + and "/assets/material_dark/" in source("CalmButton.qml"), + "light hover background must pair with dark caption and icon") combo = source("StudioDarkComboBox.qml") require('choice.highlighted || choice.hovered ? "#142334"' in combo and 'choice.highlighted || choice.hovered ? "#edf5ff"' in combo, From 1ea49120ba47b4472ba122c1043e6c1ee24e067a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:36:06 +0700 Subject: [PATCH 16/17] fix(studio): distribute vertical matrix height among four editable channels instead of blank spacer --- apps/arstack_studio/qml/SignalMatrix.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/qml/SignalMatrix.qml b/apps/arstack_studio/qml/SignalMatrix.qml index 101e9c2d3..ac323221d 100644 --- a/apps/arstack_studio/qml/SignalMatrix.qml +++ b/apps/arstack_studio/qml/SignalMatrix.qml @@ -227,6 +227,10 @@ Rectangle { } Layout.fillWidth: true + // Distribute spare workspace across editable channel rows; + // never leave a large dead zone under four fixed-height rows. + Layout.fillHeight: true + Layout.minimumHeight: 34 Layout.preferredHeight: matrix.compact ? 40 : 42 color: signalRow.selected ? "#101c27" : (magnitudeField.activeFocus || phaseField.activeFocus) ? "#0f1922" @@ -438,6 +442,6 @@ Rectangle { } } - Item { Layout.fillHeight: true } + // Channel delegates stretch into available height. } } From 4ea4b0a932ad12171f3c6179afa08c73696bda72 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 12:36:22 +0700 Subject: [PATCH 17/17] test(studio): prevent blank-bottom Voltage/Current editor layout regression --- apps/arstack_studio/tests/check_operator_workspace.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index e21ba4bcd..11825422f 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -77,6 +77,10 @@ def main() -> None: and "root.ptpDesiredForSession = false" in main_qml, "PTP user intent restoration must be bounded, explicit and cancellable") matrix = source("SignalMatrix.qml") + require("Layout.fillHeight: true" in matrix + and "Layout.minimumHeight: 34" in matrix + and "Item { Layout.fillHeight: true }" not in matrix, + "editable channel rows must consume vertical space without a blank filler") require("function flushPendingEdits()" in matrix and "magnitudeApplyTimer.stop()" in matrix and "phaseApplyTimer.stop()" in matrix