diff --git a/.planning/STATE.md b/.planning/STATE.md index a7cedcce..77c6471f 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -27,7 +27,12 @@ Phase: — (none active; latest shipped = Phase 1041, MERGED via PR #189 on 2026 Plan: — Milestone: v3.0 FastSense Companion — SHIPPED 2026-04-30; v3.1 Plant Log Integration — SHIPPED 2026-05-19; v4.0 Multi-User LAN Concurrency — SHIPPED 2026-05 via PR #152 (parallel branch); v1.0 perf line — COMPLETE via PR #114. No milestone in flight. Status: Phase 1041 complete — inline time-range control (toolbar dropdown + Custom date strip) shipped; PR #189 MERGED 2026-06-03. No planned milestone in flight — repo in polish/housekeeping. Outstanding: 12 wiki-bot dup PRs were closed to 1 (#190) + workflow root-caused (260609-mcz); backlog Phase 999.1 (in-app help system) unplanned; ROADMAP v4.0 boxes stale (shipped on main via #152 — router misreports, see memory gsd-router-stale-v4-misroute). -Last activity: 2026-07-08 - Implemented the approved features from the GitHub issue triage via GSD (PR #376, branch claude/github-issues-triage-df4390). Phase 999.2 Tag Analysis Toolkit adds 6 methods to libs/SensorThreshold/Tag.m — getStats #223 (keystone), resampleUniform #308, derivative #326, movingStat #312, exceedance #316, crossings #328 — all toolbox-free/Octave-safe/additive. NOTE: #327 cumulativeIntegral and #321 EventViewer.exportImage were independently shipped on main first (PRs #331/#333, quick tasks 260629-tgy/tt3) while this branch was in flight; on merge of main, this branch keeps main's canonical cumulativeIntegral + exportImage and its duplicates/tests were dropped. Verified via matlab MCP: 6 new Tag-method test files pass; TestTag (main's #327 suite) + TestEventViewer green; TestDerivedTag 35/35 (no regression). +Last activity: 2026-07-09 - Completed quick task 260709-ikg: Dashboard UI refresh redline (e) — per-plot X V A L + strip decluttered into an overflow "…" menu. Same compatibility-shim pattern as the toolbar (gp9): the 5 folded chrome buttons (CrosshairLinkButton=X, YLimitVisibleBtn=V, YLimitAllBtn=A, PlantLogToggleButton=L, CreateEventButton=+) kept ALIVE but Visible='off' (all their tags/positions/callbacks/bg intact → 130 chrome asserts across 7 files unaffected; none assert Visible), with one new OverflowMenuButton ('…', U+2026) whose click posts a FRESH uicontextmenu (showOverflowMenu_) routing to the SAME action methods (onCrosshairLinkTogglePressed_/onYLimitButtonClicked_/onPlantLogTogglePressed_/invokeCreateEventCallback_), Checked states reflecting current mode. Info(i) + Detach(^) stay visible; reflowChrome_ anchors '…' at the leftmost folded slot. Left-click menu post confirmed via live probe (set(cm,'Position',CurrentPoint)+Visible='on'). Only DashboardLayout.m changed (commits 5345671a + 619ff9e3 + 195637bc). Live-verified: OverflowMenuButton present+visible, all 5 folded present+hidden, Detach visible, '…' callback posts menu w/o error; isolated toggle repro keeps bar+buttons. Suites (isolated): TestDashboardDetach 11/11, TestDashboardWidget 13/13, TestInfoTooltip 13/13, test_dashboard_widget_button_bar 5/5, test_dashboard_layout_plant_log_toggle 13/13, TestDashboardLayoutPlantLogToggle 13/13. TWO failures both cleared via A/B: (1) TestDashboardLayoutPlantLogToggle/testInfoIconSurvivesToggleOnOff fails ONLY in a multi-suite session (bar deleted mid-test) and reproduces IDENTICALLY on base f65d830a in the same 5-suite sequence → environmental test-ordering contamination, not a regression; (2) TestIndustrialPlantDemoCompanion/testCOMPDEMO03 (no area:* tag Labels) fails on base too → pre-existing registry precondition. Branch claude/light-mode-design-d00ff7 (PR #375). Redlines (d)+(e) now DONE; remaining optional: (f) status pills, (g) heatmap colormap. --- PRIOR: quick task 260709-gp9: Dashboard UI refresh redline (d) — custom-drawn toolbar matching the Pencil light mock. The dashboard is a CLASSIC figure (DashboardEngine.m:462), so native uicontrol toolbar buttons can't be styled into the mock's brand+pill look. Compatibility-shim: all 14 native handles (hTitleText/hLastUpdate/hInfoBtn/…/hEventsPanel) kept valid but Visible='off' (readers = DashboardEngine.applyTheme, DashboardConfigDialog, tests still work), with a new custom axes+patch layer (Tag DashboardToolbarAxes) drawn over hPanel: accent brand square + bold name (click-to-rename), 9 flat grouped buttons (Info/Sync/Redraw/Config/Image/Export/Live/Follow/Events), right-aligned last-update; every click routes to the SAME on* handlers, toggles flip the hidden Value first. Active repaint: Live→green (StatusOkColor), Follow/Events→blue (InfoColor). Only DashboardToolbar.m changed (commits 4b95245f + 7a294af2). Live-verified: render example_dashboard_all_widgets Theme='light' → toolbar matches mock, green Live pill confirmed; custom axes present, natives hidden-but-valid. Suites: TestDashboardInfo 19/19, TestDashboardEventsToggle 22/22, TestDashboardToolbarImageExport 9/9 (button position-order held), test_dashboard_toolbar_buttons 7/7, test_dashboard_toolbar_image_export 4/4, test_dashboard_events_toggle 22/22, test_dashboard_stale_banner 9/9. ONE failure: test_dashboard_config_dialog testAllControlsHaveTooltips (control 7 EventMarkersVisible missing tooltip) — proven PRE-EXISTING via A/B revert to base 95e1d00a (fails identically without the change); filed as separate task. Branch claude/light-mode-design-d00ff7 (PR #375). Also this session: 1:1 Pencil design spec (docs/design/fastsense-light-1to1-design-spec.md) + reconciled Pencil line tokens to MATLAB muted + new FastSense Light Plot frame. Remaining redline: (e) per-plot X V A L strip. --- PRIOR: quick task 260709-ekv: Dashboard status fix — MultiStatusWidget was painting every sensor dot alarm-red because it applied the monitor 0/1 alarm mapping (valueAt(now)>=0.5) to plain SensorTags (raw value always >=0.5). Gated on tag.getKind()=='monitor' in both deriveColor + deriveColorFromTag_. Verified: example dots now OK-green x5/alarm-red x0; TestMultiStatusWidgetTag 7/7, test_multistatus_monitortag_bare 4/4 (monitors still alarm). StatusWidget OK/WARN/ALARM pill deferred (dashboards use ~11px status cells where a pill can't render; StatusWidget is resize-fragile). Also this session: tab-contrast fix 33f3167b (light TabActiveBg/TabInactiveBg were near-identical luminance, TestGroupWidget now 36/36). Code b43aa3a6; branch claude/light-mode-design-d00ff7 (PR #375 = light theme + UI-refresh Phases 1-4 + fixes). Deferred: toolbar cosmetic (Phase 5), X V A L strip, EventTimeline header, StatusWidget pill. +PRIOR activity: 2026-07-09 - Completed quick task 260709-dcq: Dashboard UI refresh Phase 4 "adaptive KPI number card" — NumberWidget renders a card (title top-left muted, big left-aligned value + inline unit, trend top-right accent) when the content panel is >=55px tall, else a compact single-row fallback (restored verbatim) so the short ~11px KPI cells in the all-widgets example don't regress. Verified: main example KPI cells show 72.9/56 (compact); a tall [1 1 8 8] number widget renders the card (value fs=30 in 282px box, no clip). TestNumberWidget 12/12, TestDashboardEngine 18/18. Code 642b704e; branch claude/light-mode-design-d00ff7 (PR #375 = light theme + UI-refresh Phases 1–4). Phase 5 next: toolbar refresh. Deferred: X V A L strip, EventTimeline header, StatusWidget/MultiStatus polish. +PRIOR activity: 2026-07-09 - Completed quick task 260709-cw9: Dashboard UI refresh Phase 3 "title header band" — new DashboardWidget.drawPanelTitle_ renders a widget's Title as a bold header uicontrol above the plot area (immune to newplot clearing); BarChart/Heatmap/Histogram/Scatter/EventTimeline converted from in-axes title() to the header + lowered axes. Fixes titles overlapping/clipping their plots. Verified: example_dashboard_all_widgets shows clean headers on the 4 main charts; TestBarChartWidget 5/5, TestHistogramWidget 6/6, TestScatterWidget 5/5, TestHeatmapWidget 5/5, TestDashboardEngine 18/18. Mid-task gotcha: header band must be ~18% (not 9px) to hold the font in short cells. Deferred: 1-row EventTimeline too short to show its header (task chip filed); X V A L button-strip. Code 9ec3b49e; branch claude/light-mode-design-d00ff7 (PR #375 = light theme + UI-refresh Phases 1–3). Phase 4 next: KPI/status cards. +PRIOR activity: 2026-07-08 - Completed quick task 260708-waf: Dashboard UI refresh Phase 2 "de-clutter" — suppress the floating MATLAB axes exploration toolbar on all dashboard plot widgets via a central hook in DashboardLayout.realizeWidget (new private static hideAxesToolbars_). Covers all 14 axes-bearing widget types, re-applies on scroll-realize. Verified: example_dashboard_all_widgets before/after exportapp shows toolbars gone from every visible plot; TestDashboardLayout 8/8, TestDashboardEngine 18/18, TestScatterWidget 5/5, TestHeatmapWidget 5/5. X V A L button-strip consolidation deferred to a later phase. Code 20dd2559; branch claude/light-mode-design-d00ff7 (PR #375 now = light theme + UI-refresh Phases 1–2). Phase 3 next: card headers & title hierarchy. +PRIOR activity: 2026-07-08 - Completed quick task 260708-w1d: Dashboard UI refresh Phase 1 "breathing room" — theme-driven grid gutters (WidgetGapH 0.006 / WidgetGapV 0.010) + outer padding (DashboardPad) wired into DashboardLayout via DashboardEngine before allocatePanels; widgets now read as separated cards. Additive theme fields, isfield-guarded, backward-compatible. Verified in MATLAB: example_dashboard_all_widgets (18 widgets) renders with visible gutters/padding, no overlap/scroll break; TestDashboardLayout 8/8, TestDashboardEngine 18/18, TestDashboardTheme 6/6. TestDashboardDetach/testDetachButtonInjected confirmed pre-existing failure (fails on clean baseline via stash A/B), tracked separately. Code 2b6d44e3; on branch claude/light-mode-design-d00ff7. This is Phase 1 of a 6-phase Pencil redesign (as-is + redesigned frames in pencil-halo.pen); earlier same day: 260708-v8k light theme (PR #375). +PRIOR activity: 2026-07-08 - Implemented the approved features from the GitHub issue triage via GSD (PR #376, branch claude/github-issues-triage-df4390). Phase 999.2 Tag Analysis Toolkit adds 6 methods to libs/SensorThreshold/Tag.m — getStats #223 (keystone), resampleUniform #308, derivative #326, movingStat #312, exceedance #316, crossings #328 — all toolbox-free/Octave-safe/additive. NOTE: #327 cumulativeIntegral and #321 EventViewer.exportImage were independently shipped on main first (PRs #331/#333, quick tasks 260629-tgy/tt3) while this branch was in flight; on merge of main, this branch keeps main's canonical cumulativeIntegral + exportImage and its duplicates/tests were dropped. Verified via matlab MCP: 6 new Tag-method test files pass; TestTag (main's #327 suite) + TestEventViewer green; TestDerivedTag 35/35 (no regression). Previous: 2026-06-29 - Completed quick task 260629-tgy: Tag.cumulativeIntegral() trapezoidal totalizer (#327) — 32/32 TestTag pass, merged via PR #331. Prior: 260629-tt3 EventViewer.exportImage() PNG/JPEG export (#321) — merged via PR #333. Earlier: 260624-nvf StatusWidget MonitorTag crash fix (code 14b3d529). @@ -110,6 +115,14 @@ Other main PRs (#138, #139, #141, #144, #145, #146) auto-merged without conflict | 260610-hwj | Review-sweep fixes batch 2 (branch claude/review-fixes-batch2, separate PR from the perf pass): GaugeWidget.fromStruct restores Threshold (was Tag — threshold coloring dead after load) + constructor probes for allValues() (pre-v2.0-only method; MonitorTag-bound gauges crashed at construction since the migration); GroupWidget round-trips ExpandedHeight (collapsed groups were stuck collapsed after load); central themeOverride backfill in DashboardWidgetRegistry.fromStruct (dropped on load for every widget except GroupWidget); FastSense exportData routes through lineFullData (disk-backed lines exported empty columns); markerXData test helper parses batched NaN-separated marker polylines (stale since 260508). New test_review_fixes_batch2.m 4/4 R2025b / 3/3+gate Octave 11; event_markers 9/9 (first MATLAB pass ever); SerializerRoundTrip 15/15; Serializer 12/12; toolbar 19/19. | 2026-06-10 | 18387785 | — | [260610-hwj-review-sweep-fixes-batch-2-widget-serial](./quick/260610-hwj-review-sweep-fixes-batch-2-widget-serial/) | | 260610-ov3 | Optimize data loading speed in populated Tag-bound dashboards (LOAD path; complements 260609-v5p/260610-g0w live-tick passes): render-scoped `RenderDataCache_` in FastSenseWidget collapses 3-4 redundant Tag resolves per render() to <=1 — probe pull seeds the cache, binding (addLine for non-state kinds; State keeps addTag staircase), yInit autoscale, and updateTimeRangeCache all reuse it; consume-once lifetime (warm through DashboardEngine's post-render computePreviewEnvelope pass, cleared on preview read + refresh()/update() entry); ctor `updateTimeRangeCache()` no-arg now uses O(1) Tag.getTimeRange() instead of full getXY (also fixes disk-backed ctor range = inf/-inf). Disk widgets: 2 SQLite range queries/render → 1, and they now contribute slider-preview envelopes at load (previously silently empty). New bench_dashboard_load.m (12×50k pts, 4 disk-backed): Render 6834→6711 ms Octave (graphics-bound; data-load slice halved). Octave 11.1: render_cache 4/4, load_perf 5/5, 13 regression tests green (batch segfault = known teardown flake, isolated runs clean). DEFERRED to live MATLAB: TestDashboardEngine, TestFastSenseWidgetUpdate, TestDashboardSerializerRoundTrip suites. | 2026-06-10 | 3b7535ea | — | [260610-ov3-optimize-data-loading-speed-in-populated](./quick/260610-ov3-optimize-data-loading-speed-in-populated/) | | 260624-nvf | Fix StatusWidget crash when bound to a MonitorTag via 'Tag' (route monitor-kind Tag to the Threshold/monitor path; unblocks README dashboard quickstart) | 2026-06-24 | 14b3d529 | Verified | [260624-nvf-fix-statuswidget-crash-when-bound-to-a-m](./quick/260624-nvf-fix-statuswidget-crash-when-bound-to-a-m/) | +| 260708-v8k | Clean-modern light mode across the three theme surfaces (Pencil-designed palette; blue #2563EB accent, crisp status trio; dark preset byte-identical, per-preset status colors) | 2026-07-08 | 28cfe9e2 | Verified | [260708-v8k-clean-modern-light-mode](./quick/260708-v8k-clean-modern-light-mode/) | +| 260708-w1d | Dashboard UI refresh Phase 1 — grid breathing room (theme-driven gutters + outer padding so widgets read as separated cards; additive theme fields, backward-compatible) | 2026-07-08 | 2b6d44e3 | Verified | [260708-w1d-dashboard-refresh-breathing-room](./quick/260708-w1d-dashboard-refresh-breathing-room/) | +| 260708-waf | Dashboard UI refresh Phase 2 — de-clutter: hide floating MATLAB axes toolbars on all plot widgets via central realizeWidget hook (hideAxesToolbars_); X V A L button-strip deferred | 2026-07-08 | 20dd2559 | Verified | [260708-waf-dashboard-refresh-hide-axes-toolbars](./quick/260708-waf-dashboard-refresh-hide-axes-toolbars/) | +| 260709-cw9 | Dashboard UI refresh Phase 3 — title header band: DashboardWidget.drawPanelTitle_ renders widget Title as a header above the plot (fixes title/plot overlap on BarChart/Heatmap/Histogram/Scatter/EventTimeline); EventTimeline degenerate-height header deferred | 2026-07-09 | 9ec3b49e | Verified | [260709-cw9-dashboard-refresh-card-headers](./quick/260709-cw9-dashboard-refresh-card-headers/) | +| 260709-dcq | Dashboard UI refresh Phase 4 — adaptive KPI number card: NumberWidget shows a card (big value + inline unit + trend) when pH>=55, else compact fallback (no regression in short ~11px KPI cells) | 2026-07-09 | 642b704e | Verified | [260709-dcq-dashboard-refresh-kpi-card](./quick/260709-dcq-dashboard-refresh-kpi-card/) | +| 260709-ekv | Dashboard status fix — MultiStatusWidget painted every sensor dot alarm-red (v>=0.5 mapping applied to non-monitor tags); gate on tag.getKind()=='monitor' in both color paths. Dots now OK-green. StatusWidget pill deferred (short cells) | 2026-07-09 | b43aa3a6 | Verified | [260709-ekv-dashboard-refresh-status](./quick/260709-ekv-dashboard-refresh-status/) | +| 260709-gp9 | Dashboard UI refresh redline d — custom-drawn toolbar (light mock): classic-figure native uicontrols can't be styled into a brand+pill look, so a compatibility-shim hides all 14 native handles (Visible='off', never deleted) behind a custom axes+patch layer (DashboardToolbarAxes) — brand square+bold name, 9 flat grouped buttons, green Live pill (StatusOkColor); clicks route to the same handlers. Only DashboardToolbar.m changed. 7/8 must-pass suites green; 1 failure (config-dialog tooltip) proven pre-existing via A/B | 2026-07-09 | 7a294af2 | Verified | [260709-gp9-custom-drawn-dashboard-toolbar-light-mod](./quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/) | +| 260709-ikg | Dashboard UI refresh redline e — per-plot X V A L + strip folded into an overflow "…" menu: same hide-not-delete shim (5 folded chrome buttons kept alive but Visible='off', all 130 chrome asserts unaffected) + one OverflowMenuButton whose click posts a uicontextmenu routing to the same actions; Info + Detach stay visible; reflowChrome_ anchors "…". Only DashboardLayout.m changed. Isolated suites green (TestDashboardDetach/Widget/InfoTooltip/PlantLogToggle + button-bar); 2 failures both proven pre-existing/environmental via A/B on base | 2026-07-09 | 195637bc | Verified | [260709-ikg-overflow-menu-for-widget-chrome-redline-](./quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/) | | 260629-tt3 | EventViewer: add exportImage(path[,format]) — save the timeline figure as PNG/JPEG at 150 DPI (GitHub #321); ports DashboardEngine.exportImage's cross-platform branch (exportapp/exportgraphics/getframe + Octave print()+stub-axes), EventViewer-namespaced error IDs. Method only (toolbar button deferred). 4 new TestEventViewerExtras cases (16/16) + TestEventViewer 7/7 no-regression; real export round-trip verified (1200×700 PNG/JPEG via imfinfo); mh_style + mh_lint clean. Merged via PR #333. | 2026-06-29 | 4dec4731 | Merged | [260629-tt3-eventviewer-exportimage-save-timeline-pn](./quick/260629-tt3-eventviewer-exportimage-save-timeline-pn/) | | 260629-tgy | Tag: add cumulativeIntegral() — running trapezoidal totalizer over getXY/getXYRange (GitHub #327); additive base-Tag method, gap-robust NaN handling, kind-aware Tag:integralOnDiscrete warning, 'Range' window + scalar/series nargout forms. 8 new TestTag cases (32/32 pass in live MATLAB); mh_style + mh_lint clean. Merged via PR #331. | 2026-06-29 | acbd504a | Merged | [260629-tgy-tag-cumulativeintegral-trapezoidal-total](./quick/260629-tgy-tag-cumulativeintegral-trapezoidal-total/) | | 260709-cbk | TagRegistry: add toStructs() — whole-catalog serializer, the SAVE half of loadFromStructs (GitHub #363). Static, sorts keys for determinism, pure assembly over each Tag's existing toStruct. 3 new TestTagRegistry cases (29/29 pass in live MATLAB); mh_style + mh_lint clean. | 2026-07-09 | 246909c8 | Verified | [260709-cbk-implement-issue-363-add-tagregistry-tost](./quick/260709-cbk-implement-issue-363-add-tagregistry-tost/) | diff --git a/.planning/quick/260708-v8k-clean-modern-light-mode/260708-v8k-PLAN.md b/.planning/quick/260708-v8k-clean-modern-light-mode/260708-v8k-PLAN.md new file mode 100644 index 00000000..50761e79 --- /dev/null +++ b/.planning/quick/260708-v8k-clean-modern-light-mode/260708-v8k-PLAN.md @@ -0,0 +1,72 @@ +--- +id: 260708-v8k +slug: clean-modern-light-mode +title: Clean-modern light mode across the three theme surfaces +status: in_progress +created: 2026-07-08 +type: quick +--- + +# Quick Task 260708-v8k: Clean-modern light mode + +## Goal + +Redesign the FastSense **light** theme with a "clean modern" identity (crisp white +widget surfaces on a soft neutral-gray canvas, blue `#2563EB` accent, hairline cool +borders, semantic green/amber/red status). Palette approved via a Pencil mockup. +Keep everything cheap to render in MATLAB (flat fills, 1px borders, solid arcs) and +fully backward-compatible (no field renames, no API change, legacy preset aliases +intact, **dark preset output byte-identical**). + +## Tasks + +### Task 1 — FastSenseTheme.m light preset +- **files:** `libs/FastSense/FastSenseTheme.m` +- **action:** In `getPreset`, rewrite the `'light'`/legacy-alias struct values: + - `Background [0.961 0.965 0.973]` + - `AxesColor [1 1 1]` + - `ForegroundColor [0.208 0.255 0.333]` + - `GridColor [0.914 0.929 0.949]` + - `GridAlpha 0.7` + - `GridStyle '-'` + - `LineColorOrder 'muted'` (unchanged) + - `ThresholdColor [0.937 0.267 0.267]` + - leave `ThresholdStyle`, `ViolationMarker`, `ViolationSize`, `BandAlpha`, + `FontName`, `FontSize`, `TitleFontSize`, `LineWidth` unchanged. + - **dark preset untouched.** +- **verify:** `t=FastSenseTheme('light')` returns the new values; `FastSenseTheme('default')` + aliases to light; `FastSenseTheme('dark')` unchanged. +- **done:** light struct reflects the approved palette. + +### Task 2 — DashboardTheme.m light branch + per-preset status colors +- **files:** `libs/Dashboard/DashboardTheme.m` +- **action:** In `getDashboardDefaults`, rewrite the `'light'` (`otherwise`) branch: + - `DashboardBackground [0.961 0.965 0.973]`, `WidgetBackground [1 1 1]`, + `WidgetBorderColor [0.898 0.910 0.925]`, `ToolbarBackground [0.976 0.980 0.984]`, + `ToolbarFontColor [0.392 0.455 0.545]`, `DragHandleColor [0.145 0.388 0.922]`, + `DropZoneColor [0.918 0.945 0.996]`, `GridLineColor [0.914 0.929 0.949]`, + `GroupHeaderBg [0.933 0.949 0.969]`, `GroupHeaderFg [0.118 0.161 0.231]`, + `GroupBorderColor [0.898 0.910 0.925]`, `TabActiveBg [0.918 0.945 0.996]`, + `TabInactiveBg [0.933 0.949 0.969]`. Keep `MarkerPlantLog` and + `CurrentViewBoxColor` light values. + - Make `StatusOkColor`/`StatusWarnColor`/`StatusAlarmColor` **per-preset**: set them + in BOTH switch branches — dark keeps `[0.31 0.80 0.64]`/`[0.91 0.63 0.27]`/`[0.91 0.27 0.38]`; + light uses `[0.086 0.639 0.290]`/`[0.961 0.620 0.043]`/`[0.937 0.267 0.267]`. + Guard the shared-defaults assignment of those three with `if ~isfield` so it no + longer overrides the per-preset values. +- **verify:** `DashboardTheme('light')` reflects new values; `DashboardTheme('dark')` + is byte-identical to the pre-change output (compare via isequaln on all fields). +- **done:** light branch reflects approved palette, dark unchanged. + +### Task 3 — CompanionTheme.m (no edit) + verification +- **files:** none (verification only) +- **action:** Confirm `CompanionTheme.get('light')` inherits the new palette: + `Accent == DragHandleColor == [0.145 0.388 0.922]`, `LineColors` derived from muted. +- **verify:** construct all three light themes + dark; run theme-related test files. +- **done:** companion light inherits correctly; tests green (modulo known env failures). + +## Must-haves +- Light palette matches the approved Pencil mockup hex→RGB values. +- Dark preset output unchanged (backward compatibility). +- No field renamed or removed; legacy preset aliases still resolve to light. +- Only flat-render primitives (no gradient/shadow fields introduced). diff --git a/.planning/quick/260708-v8k-clean-modern-light-mode/260708-v8k-SUMMARY.md b/.planning/quick/260708-v8k-clean-modern-light-mode/260708-v8k-SUMMARY.md new file mode 100644 index 00000000..b1771c89 --- /dev/null +++ b/.planning/quick/260708-v8k-clean-modern-light-mode/260708-v8k-SUMMARY.md @@ -0,0 +1,58 @@ +--- +id: 260708-v8k +slug: clean-modern-light-mode +status: complete +date: 2026-07-08 +commit: 28cfe9e2 +--- + +# Quick Task 260708-v8k — Summary + +## What shipped + +A redesigned **clean-modern light mode** for FastSense, designed in Pencil and +translated 1:1 into the existing MATLAB theme structs. Crisp white widget +surfaces on a soft neutral-gray canvas (`#F5F6F8`), blue `#2563EB` accent, +hairline cool borders, and a crisp semantic green/amber/red status trio. + +Every value maps to a flat-render primitive (solid fills, 1px borders, solid +arcs, native dashed threshold) — no gradients, shadows, blur, or transparency +stacks — so the theme stays cheap for MATLAB to draw. + +## Changes + +- **`libs/FastSense/FastSenseTheme.m`** — rewrote the `'light'`/legacy-alias + preset: `Background [0.961 0.965 0.973]`, `ForegroundColor [0.208 0.255 0.333]`, + `GridColor [0.914 0.929 0.949]`, `GridAlpha 0.7`, `ThresholdColor [0.937 0.267 0.267]`. + `LineColorOrder` kept as `'muted'`. Dark preset untouched. +- **`libs/Dashboard/DashboardTheme.m`** — rewrote the light branch (surfaces, + hairline borders, blue drag/drop accent, cool group/tab backgrounds). Made + `StatusOkColor`/`StatusWarnColor`/`StatusAlarmColor` **per-preset** with a + guarded (`if ~isfield`) shared fallback, so light gets its crisp trio + `[0.086 0.639 0.290]`/`[0.961 0.620 0.043]`/`[0.937 0.267 0.267]` while dark + keeps its original values. +- **`CompanionTheme.m`** — no edit; `Accent` and `LineColors` derive from + DashboardTheme, so the light palette flows through automatically. +- **Tests** — updated `TestTheme.m` (3) and `TestFastSenseTheme.m` (1) light + `Background` assertions to the new value. + +## Verification + +- Light values match the approved Pencil palette (checked field-by-field in MATLAB). +- **Dark preset byte-identical**: `isequaln` = 1 for both `FastSenseTheme('dark')` + and `DashboardTheme('dark')` vs a pre-change baseline snapshot. +- Legacy aliases (`default`, `ocean`, `scientific`, `industrial`) resolve to light. +- Companion inherits: `Accent == DragHandleColor == [0.145 0.388 0.922]`. +- Test suites green: TestTheme 12/12, TestDashboardTheme 6/6, TestFastSenseTheme + 11/11, TestStatusWidget 11/11, companion theme-walker 36/36. + +## Backward compatibility + +No field renamed or removed; no API change; legacy preset aliases intact; dark +preset unchanged. Existing dashboards/scripts/serialized themes keep working. + +## Design source + +Pencil mockups (two frames — "FastSense Light — Dashboard" and +"FastSense Light — Companion") in `pencil-halo.pen`, using FastSense-namespaced +`fs-*` tokens that carry the exact hex→RGB values applied here. diff --git a/.planning/quick/260708-w1d-dashboard-refresh-breathing-room/260708-w1d-PLAN.md b/.planning/quick/260708-w1d-dashboard-refresh-breathing-room/260708-w1d-PLAN.md new file mode 100644 index 00000000..6e268575 --- /dev/null +++ b/.planning/quick/260708-w1d-dashboard-refresh-breathing-room/260708-w1d-PLAN.md @@ -0,0 +1,60 @@ +--- +id: 260708-w1d +slug: dashboard-refresh-breathing-room +title: Dashboard UI refresh — Phase 1 "breathing room" (gutters + padding) +status: in_progress +created: 2026-07-08 +type: quick +--- + +# Quick Task 260708-w1d — Dashboard UI refresh, Phase 1: breathing room + +## Goal + +First slice of the approved Pencil redesign: give the dashboard grid gutters + +outer padding so widgets read as separated cards on the canvas. Cheapest, +highest-impact step. `DashboardLayout.computePosition` already consumes +`GapH`/`GapV`/`Padding`, and `allocatePanels` already draws a `line` border +(`WidgetBorderColor` on `WidgetBackground`) per cell — they just default to 0, +so the grid renders flush. This task supplies tasteful values via the theme and +wires them in. No layout math changes. + +## Tasks + +### Task 1 — Theme fields +- **files:** `libs/Dashboard/DashboardTheme.m` +- **action:** In the shared-defaults block (applies to both presets), add three + additive fields: + - `WidgetGapH = 0.006` + - `WidgetGapV = 0.010` + - `DashboardPad = [0.008 0.010 0.008 0.010]` (normalized `[L B R T]`) + Additive only — no field renamed/removed; dark output unchanged apart from the + three new fields. +- **verify:** `DashboardTheme('light')` and `('dark')` both expose the fields. +- **done:** fields present with correct defaults. + +### Task 2 — Wire layout to theme +- **files:** `libs/Dashboard/DashboardEngine.m` +- **action:** In the render path where `themeStruct` is available, just before + `allocatePanels` (near the `obj.Layout.ContentArea` assignment ~line 510), set: + - `obj.Layout.GapH = ` + - `obj.Layout.GapV = ` + - `obj.Layout.Padding = ` + Use `isfield` guards so older serialized themes lacking the fields still get the + refreshed spacing (fall back to the new defaults, not 0). +- **verify:** rendered dashboard shows gutters + edge padding; no overlap/scroll break. +- **done:** grid reads as separated cards. + +### Task 3 — Verify (MATLAB, orchestrator-run) +- **files:** none +- **action:** render `example_dashboard_all_widgets` (light), exportapp screenshot, + eyeball for gutters/padding + separated cards + no clipping. Run + `TestDashboardLayout`, `TestDashboardEngine` and adjacent suites. +- **done:** visual confirms spacing; suites green (modulo known-env failures). + +## Must-haves +- Widgets visibly separated by gutters; dashboard has outer edge padding. +- No widget overlap; scrolling still works when content exceeds viewport. +- Additive theme fields only; dark preset otherwise unchanged; existing dashboards + still render (spaced, not broken). +- `DashboardLayout` constructed directly (no theme) keeps `Gap=0` for existing tests. diff --git a/.planning/quick/260708-w1d-dashboard-refresh-breathing-room/260708-w1d-SUMMARY.md b/.planning/quick/260708-w1d-dashboard-refresh-breathing-room/260708-w1d-SUMMARY.md new file mode 100644 index 00000000..149e7be2 --- /dev/null +++ b/.planning/quick/260708-w1d-dashboard-refresh-breathing-room/260708-w1d-SUMMARY.md @@ -0,0 +1,51 @@ +--- +id: 260708-w1d +slug: dashboard-refresh-breathing-room +status: complete +date: 2026-07-08 +commit: 2b6d44e3 +--- + +# Quick Task 260708-w1d — Summary + +## What shipped + +Phase 1 of the dashboard UI refresh: **grid breathing room**. The dashboard grid +now has inter-widget gutters + outer canvas padding, so widgets read as separated +cards instead of a flush wall of panels. First and cheapest slice of the approved +Pencil redesign. + +## Changes + +- **`libs/Dashboard/DashboardTheme.m`** — added three additive shared theme fields: + `WidgetGapH = 0.006`, `WidgetGapV = 0.010`, + `DashboardPad = [0.008 0.010 0.008 0.010]` (normalized `[L B R T]`). +- **`libs/Dashboard/DashboardEngine.m`** — before `allocatePanels`, set + `Layout.GapH/GapV/Padding` from the theme, `isfield`-guarded with fallbacks to + the new defaults (so older serialized themes still get the spacing). +- **`DashboardLayout.m`** — untouched; `computePosition` already consumes these + knobs and `allocatePanels` already draws the hairline cell border. + +## Verification (live MATLAB) + +- Rendered `example_dashboard_all_widgets` (light, 18 widgets): visible gutters + + edge padding, widgets read as separated cards, no overlap, scrolling intact. + Screenshot captured (`scratchpad/phase1_gutters.png`). +- Suites green: `TestDashboardLayout` 8/8, `TestDashboardEngine` 18/18, + `TestDashboardTheme` 6/6. +- `TestDashboardDetach/testDetachButtonInjected` fails — **confirmed pre-existing** + (fails identically on the clean baseline via git-stash A/B), not a regression. + Tracked as a separate task. + +## Backward compatibility + +Additive theme fields only; no field renamed/removed. Dashboards built with a +directly-constructed `DashboardLayout` (no theme) keep `Gap=0`. Existing dashboards +still render — just spaced. + +## Next (redesign roadmap) + +Phase 2 — de-clutter widget chrome (hide axes toolbars + collapse the per-widget +`X V A L` button strip). Phases 3–6: card headers, KPI/status cards, toolbar +refresh, color polish. See the Pencil frames "FastSense — Current UI (as-is)" and +"FastSense — Redesigned UI". diff --git a/.planning/quick/260708-waf-dashboard-refresh-hide-axes-toolbars/260708-waf-PLAN.md b/.planning/quick/260708-waf-dashboard-refresh-hide-axes-toolbars/260708-waf-PLAN.md new file mode 100644 index 00000000..15f6ae1a --- /dev/null +++ b/.planning/quick/260708-waf-dashboard-refresh-hide-axes-toolbars/260708-waf-PLAN.md @@ -0,0 +1,54 @@ +--- +id: 260708-waf +slug: dashboard-refresh-hide-axes-toolbars +title: Dashboard UI refresh — Phase 2 (de-clutter): hide per-axes MATLAB toolbars +status: in_progress +created: 2026-07-08 +type: quick +--- + +# Quick Task 260708-waf — Phase 2: hide the floating axes toolbars + +## Goal + +De-clutter the dashboard by removing the redundant floating MATLAB axes +exploration toolbar (download / brush / datatips / pan / zoom-in / zoom-out / +home) that renders on every plot widget. The dashboard already provides its own +controls (per-widget button bar + time navigator), so the per-axes toolbar is +pure visual noise — the approved Pencil redesign removes it entirely. + +Scope note: this phase is **only** the axes-toolbar suppression. The `X V A L` +axis-toggle + `^`/`+` button-strip consolidation (hover/overflow) is deferred to +its own phase — it is interaction-sensitive and hover-hiding fights MATLAB's +uifigure primitives, so it deserves separate, careful work. + +## Tasks + +### Task 1 — Central suppression hook +- **files:** `libs/Dashboard/DashboardLayout.m` +- **action:** In `realizeWidget` (the single central point every widget renders + through), after `widget.render(...)` and before `widget.markRealized()`, call a + new private static helper `hideAxesToolbars_(widget.hCellPanel)`. The helper does + `findall(root,'Type','axes')` and sets each axes' `Toolbar.Visible = 'off'`, + per-axes try/catch so a toolbar-less or stale axes never aborts the realize pass. + Central placement covers all 14 axes-bearing widget types and re-applies on + scroll-realize. +- **verify:** rendered dashboard shows no floating axes toolbars; widgets still + render; no errors. +- **done:** toolbars gone across all plot widgets. + +### Task 2 — Verify (MATLAB, orchestrator-run) +- **files:** none +- **action:** render `example_dashboard_all_widgets` (light), exportapp screenshot, + confirm the 7-icon axes toolbars are gone from the plots. Run + `TestDashboardEngine`, `TestDashboardLayout`, and a couple of plot-widget suites + (`TestScatterWidget`/`TestHistogramWidget`) to confirm rendering still works. +- **done:** visual confirms toolbars gone; suites green (modulo known-env failures). + +## Must-haves +- Floating axes toolbars no longer appear on dashboard plot widgets. +- All widgets still render; no functional regression (button bar, zoom via scroll, + detach, etc. unaffected). +- Change confined to the central realize hook + one helper; no per-widget edits. +- Standalone (non-dashboard) FastSense plots are unaffected (helper only runs in + the dashboard realize path). diff --git a/.planning/quick/260708-waf-dashboard-refresh-hide-axes-toolbars/260708-waf-SUMMARY.md b/.planning/quick/260708-waf-dashboard-refresh-hide-axes-toolbars/260708-waf-SUMMARY.md new file mode 100644 index 00000000..f8360600 --- /dev/null +++ b/.planning/quick/260708-waf-dashboard-refresh-hide-axes-toolbars/260708-waf-SUMMARY.md @@ -0,0 +1,54 @@ +--- +id: 260708-waf +slug: dashboard-refresh-hide-axes-toolbars +status: complete +date: 2026-07-08 +commit: 20dd2559 +--- + +# Quick Task 260708-waf — Summary + +## What shipped + +Phase 2 of the dashboard UI refresh — **de-clutter**. The redundant floating +MATLAB axes exploration toolbar (download / brush / datatips / pan / zoom-in / +zoom-out / home) no longer appears on dashboard plot widgets. It's pure noise — +the dashboard already has its own controls (widget button bar + time navigator), +and the Pencil redesign removed it. + +## Change + +- **`libs/Dashboard/DashboardLayout.m`** + - `realizeWidget`: after each `widget.render(...)`, call the new private static + helper `hideAxesToolbars_(widget.hCellPanel)`. Central point → covers all 14 + axes-bearing widget types and re-applies on scroll-realize. + - `hideAxesToolbars_`: `findall(root,'Type','axes')` then set each + `Toolbar.Visible='off'`, per-axes try/catch. + +Scope: **axes-toolbar suppression only.** The `X V A L` axis-toggle + `^`/`+` +button-strip consolidation is deferred to its own phase (interaction-sensitive; +hover-hiding fights uifigure primitives). + +## Verification (live MATLAB) + +- `example_dashboard_all_widgets` (light, 18 widgets): before/after `exportapp` + compare — the floating toolbars are gone from every visible plot + (`scratchpad/phase2_no_toolbars.png` vs `phase1_gutters.png`). +- 3 axes still report `Toolbar.Visible='on'` but are `Visible='off'` overlay axes + (recreated by resize-time relayout after the realize hook) — they render nothing, + confirmed by the screenshot. +- Suites green: `TestDashboardLayout` 8/8, `TestDashboardEngine` 18/18, + `TestScatterWidget` 5/5, `TestHeatmapWidget` 5/5. + +## Backward compatibility / safety + +Change confined to the central realize hook + one helper; no per-widget edits. +No functional regression — button bar, detach, zoom-via-scroll unaffected. +Standalone (non-dashboard) FastSense plots are untouched (the helper only runs in +the dashboard realize path). + +## Next + +Phase 3 — card headers & title hierarchy (fix titles overlapping plots; consistent +header row per widget). Then KPI/status cards, toolbar refresh, color polish. +Deferred: the `X V A L`/`^`/`+` button-strip consolidation. diff --git a/.planning/quick/260709-cw9-dashboard-refresh-card-headers/260709-cw9-PLAN.md b/.planning/quick/260709-cw9-dashboard-refresh-card-headers/260709-cw9-PLAN.md new file mode 100644 index 00000000..816344b3 --- /dev/null +++ b/.planning/quick/260709-cw9-dashboard-refresh-card-headers/260709-cw9-PLAN.md @@ -0,0 +1,54 @@ +--- +id: 260709-cw9 +slug: dashboard-refresh-card-headers +title: Dashboard UI refresh — Phase 3: card headers & title hierarchy +status: in_progress +created: 2026-07-09 +type: quick +--- + +# Quick Task 260709-cw9 — Phase 3: card headers (fix title/plot overlap) + +## Goal + +Stop widget titles from overlapping their plots. Today each chart widget creates +a full-panel axes then calls `title(ax, obj.Title)`, which floats text just above +the axes box with only a ~10% margin — so "Machine Mode", "Violation Count by +Sensor", "Temperature Distribution", etc. collide with the plot / panel top. The +Pencil redesign puts the title in a proper header row above the plot area. + +Fix: a shared header helper + drop each affected axes below it, removing the +colliding in-axes title. + +## Tasks + +### Task 1 — Shared header helper +- **files:** `libs/Dashboard/DashboardWidget.m` +- **action:** Add protected `drawPanelTitle_(obj, parentPanel, theme)` that, when + `obj.Title` is non-empty, draws a centered bold `uicontrol('Style','text')` header + band across the top of `parentPanel` (`Position [0.02 0.88 0.96 0.11]`, + `WidgetTitleFontSize`, `ForegroundColor` on `WidgetBackground`). Returns nothing. +- **done:** helper available to all widgets. + +### Task 2 — Convert the overlapping chart widgets +- **files:** `BarChartWidget.m`, `HeatmapWidget.m`, `HistogramWidget.m`, + `ScatterWidget.m`, `EventTimelineWidget.m` +- **action:** For each: (a) replace the `title(obj.hAxes, obj.Title, ...)` call(s) + in `render` **and** `refresh` with `obj.drawPanelTitle_(parentPanel, theme)` in + `render` only (title is static chrome, drawn once); (b) lower the axes `Position` + so its top sits at ~0.85 (leave the top ~12% for the header) — e.g. BarChart/ + Histogram/Scatter `[0.12 0.15 0.82 0.75]` → `[0.12 0.13 0.82 0.72]`; Heatmap + `[0.1 0.1 0.8 0.8]` → `[0.1 0.1 0.8 0.72]`; EventTimeline `[0.06 0.15 0.9 0.7]` + → `[0.06 0.13 0.9 0.66]`. Keep refresh from re-adding an axes title. +- **done:** titles sit in a header band; no overlap with plot content. + +### Task 3 — Verify (MATLAB) +- render `example_dashboard_all_widgets` (light), exportapp, confirm titles no + longer overlap plots; run widget suites (BarChart/Heatmap/Histogram/Scatter/ + Timeline) + TestDashboardEngine. + +## Must-haves +- No widget title overlaps its plot content or clips at the panel top. +- Titles render once (no duplicate/ghost title on refresh). +- All affected widgets still render + refresh correctly; suites green. +- FastSenseWidget left as-is (core-drawn title reads fine; revisit later). diff --git a/.planning/quick/260709-cw9-dashboard-refresh-card-headers/260709-cw9-SUMMARY.md b/.planning/quick/260709-cw9-dashboard-refresh-card-headers/260709-cw9-SUMMARY.md new file mode 100644 index 00000000..636f57f5 --- /dev/null +++ b/.planning/quick/260709-cw9-dashboard-refresh-card-headers/260709-cw9-SUMMARY.md @@ -0,0 +1,55 @@ +--- +id: 260709-cw9 +slug: dashboard-refresh-card-headers +status: complete +date: 2026-07-09 +commit: 9ec3b49e +--- + +# Quick Task 260709-cw9 — Summary + +## What shipped + +Phase 3 of the dashboard UI refresh — **title header band**. Widget titles no +longer overlap their plots. Chart widgets used to create a full-panel axes then +`title(ax, obj.Title)`, floating text just above the plot box with a ~10% margin, +so titles collided with the plot / clipped at the panel top. Now the title renders +as a bold header band above the plot area, matching the Pencil redesign. + +## Changes + +- **`DashboardWidget.m`** — new protected `drawPanelTitle_(parentPanel, theme)`: + renders `obj.Title` as a bold centered `uicontrol('Style','text')` header band + (`Position [0.03 0.80 0.94 0.18]`). A sibling uicontrol, so it's immune to the + `newplot` title-clearing that forced the old in-`refresh` re-apply. Tagged + `WidgetTitleHeader` and self-deleting so repeated `render()` never stacks dupes. +- **`BarChartWidget` / `HeatmapWidget` / `HistogramWidget` / `ScatterWidget` / + `EventTimelineWidget`** — draw the header via the helper in `render`; drop the + axes top to make room; remove the in-axes `title()` calls (both the render and, + for BarChart/Histogram, the `newplot` re-apply in `refresh`). + +## Verification (live MATLAB) + +- `example_dashboard_all_widgets` (light): the four main chart titles now sit in + clean headers above their plots — no overlap/clip (`scratchpad/phase3_headers_v3.png` + vs the earlier `phase2_no_toolbars.png`). +- Suites green: `TestBarChartWidget` 5/5, `TestHistogramWidget` 6/6, + `TestScatterWidget` 5/5, `TestHeatmapWidget` 5/5, `TestDashboardEngine` 18/18. + +## Gotcha fixed mid-task + +First cut used a `0.09`-normalized band → only ~9px in the short (~130px) dashboard +cells, smaller than the ~15px glyphs, so the header text clipped to nothing. +Widened to `0.18` + lowered the axes; renders correctly now. + +## Known limitation / deferred + +The 1-row-tall `EventTimelineWidget` is too short to show a header cleanly in this +example (its title is now hidden rather than overlapping). Deferred — needs a +min-height or inline title for degenerate cells (task chip filed). `FastSenseWidget` +core-drawn title left as-is (reads fine). + +## Next + +Phase 4 — KPI/status cards (big value + delta chip; status dot + pill). Then +toolbar refresh, color polish. Deferred: `X V A L` button-strip; timeline header. diff --git a/.planning/quick/260709-dcq-dashboard-refresh-kpi-card/260709-dcq-PLAN.md b/.planning/quick/260709-dcq-dashboard-refresh-kpi-card/260709-dcq-PLAN.md new file mode 100644 index 00000000..e2fed9b3 --- /dev/null +++ b/.planning/quick/260709-dcq-dashboard-refresh-kpi-card/260709-dcq-PLAN.md @@ -0,0 +1,37 @@ +--- +id: 260709-dcq +slug: dashboard-refresh-kpi-card +title: Dashboard UI refresh — Phase 4: KPI number card +status: in_progress +created: 2026-07-09 +type: quick +--- + +# Quick Task 260709-dcq — Phase 4: KPI number card + +## Goal + +Replace the cramped horizontal `[Title | Value | Trend | Units]` strip in +NumberWidget with a clean KPI card: title top-left (muted), a big left-aligned +value with an inline unit, and a trend indicator top-right — matching the Pencil +redesign. NumberWidget.relayout_ clears + re-calls render() on resize, so +restructuring render() is relayout-safe. + +## Tasks + +### Task 1 — Card layout +- **files:** `libs/Dashboard/NumberWidget.m` +- **action:** In `render`, restructure the four uicontrols: title top-left muted + left-aligned; big value left-aligned (bump adaptive cap 28→34); unit inline + muted; trend top-right in the accent color. No logic change to refresh/trend. +- **done:** number widgets read as cards; value is the clear focal point. + +### Task 2 — Verify (MATLAB) +- render `example_dashboard_all_widgets` (light), exportapp, confirm the + Temperature/Pressure number widgets read as clean cards, value prominent, no + clipping/overlap. Run `TestNumberWidget` + `TestDashboardEngine`. + +## Must-haves +- Value is the visual focal point; title + unit are secondary/muted. +- Nothing clipped; resize (relayout_) still works. +- refresh()/trend logic unchanged; suites green. diff --git a/.planning/quick/260709-dcq-dashboard-refresh-kpi-card/260709-dcq-SUMMARY.md b/.planning/quick/260709-dcq-dashboard-refresh-kpi-card/260709-dcq-SUMMARY.md new file mode 100644 index 00000000..34b991be --- /dev/null +++ b/.planning/quick/260709-dcq-dashboard-refresh-kpi-card/260709-dcq-SUMMARY.md @@ -0,0 +1,49 @@ +--- +id: 260709-dcq +slug: dashboard-refresh-kpi-card +status: complete +date: 2026-07-09 +commit: 642b704e +--- + +# Quick Task 260709-dcq — Summary + +## What shipped + +Phase 4 of the dashboard UI refresh — **adaptive KPI number card**. NumberWidget +now renders a proper card (title top-left muted, big left-aligned value + inline +unit, trend indicator top-right in the accent color) when the cell has real +height, matching the Pencil redesign. + +## Adaptive pivot (important) + +Dashboard KPI cells are frequently only 1–2 grid rows tall — in the all-widgets +example the number-widget content panel is **~11px**. A stacked card can't fit +there: the value/title font ends up taller than its band and clips to nothing +(same failure class as the Phase 3 header). So `render()` branches on panel pixel +height: + +- **`pH >= 55`** → card layout (big value fs≈0.40·pH, inline unit, top trend). +- **`pH < 55`** → the proven compact single-row layout, restored verbatim — no + regression for short cells. + +`relayout_` clears + re-renders on resize, so the right branch is chosen for the +current size automatically. + +## Verification (live MATLAB) + +- Main example: KPI cells fall back to compact and still show their values + (`72.9` / `56` present). +- A tall `[1 1 8 8]` number widget exercises the card: value `67.7` at fs=30 in a + 282px box (no clip), title / unit / `▲` trend laid out cleanly (verified via + control geometry — the small test figure hit a known uifigure capture quirk). +- Suites green: `TestNumberWidget` 12/12, `TestDashboardEngine` 18/18. + +## Scope note + +StatusWidget / MultiStatusWidget already read acceptably (dot + value); left for a +later polish. `X V A L` button strip + EventTimeline header still deferred. + +## Next + +Phase 5 — toolbar refresh. Phase 6 — color polish. diff --git a/.planning/quick/260709-ekv-dashboard-refresh-status/260709-ekv-PLAN.md b/.planning/quick/260709-ekv-dashboard-refresh-status/260709-ekv-PLAN.md new file mode 100644 index 00000000..501dc180 --- /dev/null +++ b/.planning/quick/260709-ekv-dashboard-refresh-status/260709-ekv-PLAN.md @@ -0,0 +1,40 @@ +--- +id: 260709-ekv +slug: dashboard-refresh-status +title: Dashboard UI refresh — status: fix all-red MultiStatus dots +status: in_progress +created: 2026-07-09 +type: quick +--- + +# Quick Task 260709-ekv — Status: fix MultiStatus all-red dots + +## Goal + +The "status pills" step. Primary, visible win: MultiStatusWidget painted every +sensor dot alarm-red. It derived color via `tag.valueAt(now) >= 0.5 -> alarm`, +but that 0/1 mapping is only valid for monitor-kind tags — a plain SensorTag +returns its raw reading (e.g. 72.9), always >= 0.5, so all sensors showed red. + +## Tasks + +### Task 1 — Gate the alarm mapping on monitor-kind +- **files:** `libs/Dashboard/MultiStatusWidget.m` +- **action:** In both color paths (`deriveColor` bare-tag branch and + `deriveColorFromTag_`), only apply `>= 0.5 -> alarm` when + `tag.getKind() == 'monitor'` (polymorphic, Pitfall 1). Non-monitor tags with + no threshold -> default OK color. +- **verify:** example MultiStatus dots render OK-green (not red); monitor tags + still alarm; suites green. + +## Deferred (with rationale) +- **StatusWidget status pill (OK/WARN/ALARM):** would only render in tall status + cells — the dashboards here use ~11px status cells where a pill can't fit + (falls back to the existing dot+label), so no visible change — and StatusWidget + is resize-fragile (relayout_ SizeChangedFcn). Not worth the surface now; can add + when there's a taller status widget to show it. + +## Must-haves +- Sensor dots reflect real state (not universally red). +- Monitor-kind alarm behavior unchanged. +- Suites green (modulo the pre-existing stale `test_multistatus_widget_tag`). diff --git a/.planning/quick/260709-ekv-dashboard-refresh-status/260709-ekv-SUMMARY.md b/.planning/quick/260709-ekv-dashboard-refresh-status/260709-ekv-SUMMARY.md new file mode 100644 index 00000000..c02a4686 --- /dev/null +++ b/.planning/quick/260709-ekv-dashboard-refresh-status/260709-ekv-SUMMARY.md @@ -0,0 +1,47 @@ +--- +id: 260709-ekv +slug: dashboard-refresh-status +status: complete +date: 2026-07-09 +commit: b43aa3a6 +--- + +# Quick Task 260709-ekv — Summary + +## What shipped + +Fixed a real correctness/visual bug: **MultiStatusWidget painted every sensor dot +alarm-red.** Color was derived via `tag.valueAt(now) >= 0.5 -> alarm`, but that +binary 0/1 mapping is only valid for monitor-kind tags. A plain `SensorTag` +returns its raw reading (e.g. 72.9 °F), always `>= 0.5`, so all sensors showed red +regardless of state. + +## Change + +- **`libs/Dashboard/MultiStatusWidget.m`** — in both color paths (`deriveColor` + bare-tag branch and `deriveColorFromTag_`), gate the `>= 0.5 -> alarm` mapping on + `tag.getKind() == 'monitor'` (polymorphic; Pitfall 1 — no isa-on-subclass). + Non-monitor tags with no threshold now fall to the default OK color. + +## Verification (live MATLAB) + +- `example_dashboard_all_widgets`: MultiStatus dots now **OK-green ×5, alarm-red ×0** + (were all red). +- Monitor-kind alarm behavior preserved: `TestMultiStatusWidgetTag` 7/7, + `test_multistatus_monitortag_bare` 4/4. +- Pre-existing unrelated failure noted: `test_multistatus_widget_tag` references the + removed `Threshold` class (v2.0 Tag migration) and errors at setup — not touched + by this change. + +## Deferred + +StatusWidget status pill (OK/WARN/ALARM): would only render in tall status cells, +but these dashboards use ~11px status cells (pill can't fit → falls back to the +existing dot+label, no visible change), and StatusWidget is resize-fragile. Left +for when there's a taller status widget to show it. + +## Status of the UI refresh + +Phases 1–4 + tab-contrast fix + this MultiStatus fix shipped on PR #375. Toolbar +restyle (Phase 5 cosmetic) and remaining deferred items (X V A L strip, EventTimeline +header, StatusWidget pill) still open. diff --git a/.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-PLAN.md b/.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-PLAN.md new file mode 100644 index 00000000..fdd6e84c --- /dev/null +++ b/.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-PLAN.md @@ -0,0 +1,205 @@ +--- +phase: 260709-gp9 +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - libs/Dashboard/DashboardToolbar.m +autonomous: true +requirements: + - GP9-TOOLBAR-LIGHT +must_haves: + truths: + - "Rendering example_dashboard_all_widgets with Theme='light' shows a custom-drawn toolbar (brand square + bold dashboard name at left, flat bordered action buttons + toggles at right, right-aligned last-update text) instead of the native uicontrol buttons." + - "Every native toolbar handle (hTitleText, hLastUpdate, hInfoBtn, hExportBtn, hImageBtn, hConfigBtn, hSyncBtn, hResetBtn, hLiveBtn, hLivePanel, hFollowBtn, hFollowPanel, hEventsBtn, hEventsPanel) remains a VALID handle but is set Visible='off' — never deleted, never repositioned." + - "Clicking a custom button (patch or its label) invokes the SAME existing handler as the original native button; for Live/Follow/Events the click first flips the hidden control's Value (0<->1) then calls the on*Toggle method with the hidden handle." + - "Live-active repaints its custom pill green (StatusOkColor, white label); Follow/Events-active repaint InfoColor with white label; inactive revert to WidgetBackground fill / ToolbarFontColor label — while the existing hidden *Panel HighlightColor behavior is preserved unchanged." + - "setLastUpdateTime updates BOTH the hidden hLastUpdate and the new custom last-update text; its signature and behavior are unchanged." + artifacts: + - "libs/Dashboard/DashboardToolbar.m (compatibility shim hiding native controls + custom axes/patch visual layer + active-state repaint helpers)" + key_links: + - "Hidden handles stay valid so DashboardEngine.applyTheme (hTitleText/hLastUpdate/hLivePanel, lines 1761-1779), DashboardConfigDialog (hTitleText, lines 323-325), and the must-pass tests reading .Value/.HighlightColor/.TooltipString/.String/.Position all keep working." + - "Custom toggle click flips the hidden control Value BEFORE calling on*Toggle(hiddenHandle) so Engine.startLive/stopLive + setLiveActiveIndicator/setFollowActiveIndicator/setEventsActiveIndicator stay in sync with the visual state." + - "setLiveActiveIndicator/setFollowActiveIndicator/setEventsActiveIndicator keep setting the hidden *Panel HighlightColor (tests assert this) AND additionally repaint the corresponding custom pill via the new repaint helpers." +--- + + +Replace the native uicontrol chrome of the dashboard's global toolbar (`DashboardToolbar`) with a custom-drawn axes+patch visual layer that matches the Pencil light-mode mock (redline **d** of `docs/design/fastsense-light-1to1-design-spec.md`). The dashboard is a CLASSIC MATLAB figure (`DashboardEngine.m:462`), so its native `uicontrol` buttons cannot be styled into the mock. The chosen approach (settled with the user — NOT open for redesign) is a **compatibility shim**: keep every existing handle alive but hidden, and paint a custom axes over `obj.hPanel` that draws the brand, the grouped flat action buttons, the three toggles, and the last-update label — each click routed back to the EXISTING handler so all behavior and the full handle contract survive. + +Purpose: Make the live dashboard toolbar read like the light-mode mock while keeping every test, every `DashboardEngine` read, and every `DashboardConfigDialog` read working byte-for-byte on the underlying handles. +Output: A modified `libs/Dashboard/DashboardToolbar.m` (the ONLY file changed). + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/STATE.md +@CLAUDE.md +@libs/Dashboard/DashboardToolbar.m +@docs/design/fastsense-light-1to1-design-spec.md + +# Handle-contract reads the shim must not break (read for line context, do NOT edit these files): +# DashboardEngine.m:227 -> obj.Toolbar.setEventsActiveIndicator(tf) +# DashboardEngine.m:1761-1779 -> applyTheme sets hTitleText / hLastUpdate / hLivePanel (Background + HighlightColor) +# DashboardEngine.m:2349 -> obj.Toolbar.setLastUpdateTime(...) +# DashboardConfigDialog.m:323-325-> reads eng.Toolbar.hTitleText, sets its String on rename + + + +This section is the settled design. Implement it exactly — do NOT simplify, defer, or "v1" any part. + +1. Only `libs/Dashboard/DashboardToolbar.m` may change. Do NOT edit `DashboardEngine.m`, `DashboardConfigDialog.m`, or ANY test file. +2. Preserve UNCHANGED: the `Height` property (0.04), `getContentArea()` math, the `setLastUpdateTime(obj, t)` signature, every `on*` callback method, and the constructor signature `DashboardToolbar(engine, hFigure, theme)`. +3. Native controls are HIDDEN, never deleted. They stay valid because tests + `DashboardEngine` + `DashboardConfigDialog` read `.Value` / `.HighlightColor` / `.TooltipString` / `.String` / `.Position` / `.BackgroundColor` on them. Do NOT change the Positions or creation order of the existing native controls (`TestDashboardToolbarImageExport` asserts hConfigBtn.x < hImageBtn.x < hExportBtn.x from their stored Positions). +4. Cheap-render only (§1 of the spec): flat fill rectangles, 1 px hairline borders, text. NO gradients, shadows, blur, or rounded corners. Colors come ONLY from the theme struct fields — never a hardcoded color literal. +5. Theme tokens available (light preset verified present): `ToolbarBackground`, `WidgetBackground`, `WidgetBorderColor`, `GroupHeaderFg`, `ToolbarFontColor`, `DragHandleColor`, `StatusOkColor`, `InfoColor`, `HeaderFontSize`. + +Known limitations to record in the SUMMARY (do NOT try to fix — they would require editing forbidden files): +- A live dark<->light theme toggle repaints only the hidden handles via `DashboardEngine.applyTheme`; the custom layer will not re-theme until the toolbar is reconstructed. This change targets the light mock, so it is acceptable. +- `DashboardConfigDialog` rename updates the hidden `hTitleText` only; the visible custom brand label will not reflect that rename until re-render. Flag it; do not fix by editing the dialog. +- If a must-pass test genuinely asserts a now-invalid VISUAL (not the handle contract), flag it in the SUMMARY with rationale — do NOT edit the test silently. + + + + + + Task 1: Hide native controls and build the custom-drawn visual layer with click wiring + libs/Dashboard/DashboardToolbar.m + +In the constructor, AFTER all existing native `uicontrol`/`uipanel` handles are created (i.e. after the `hLastUpdate` block near the end of the constructor), add a compatibility shim plus a custom visual layer. Do NOT move or alter any existing creation line — only append new code and set the natives hidden. + +(a) Add a private helper `hideNativeControls_(obj)` that sets `'Visible','off'` on every native handle — hTitleText, hLastUpdate, hInfoBtn, hExportBtn, hImageBtn, hConfigBtn, hSyncBtn, hResetBtn, hLiveBtn, hLivePanel, hFollowBtn, hFollowPanel, hEventsBtn, hEventsPanel — guarding each with an isempty/ishandle check. Call it at the end of the constructor. The handles MUST remain valid (do NOT call delete on any of them). + +(b) Add new private (SetAccess=private) properties to store the custom layer: `hAxes_` (the drawing axes), `hBrandSquare_` (brand patch), `hBrandText_` (brand name text), `hLastUpdateText_` (custom last-update text), and `CustomBtns_` (a struct whose fields are the button keys `info`, `sync`, `redraw`, `config`, `image`, `export`, `live`, `follow`, `events`; each field holds a small struct/array with the button's patch handle and label text handle so they can be repainted later). + +(c) Add a private helper `buildCustomLayer_(obj)` (called from the constructor after `hideNativeControls_`) that creates one `axes` parented to `obj.hPanel`, Tag `'DashboardToolbarAxes'`, `Units` normalized, `Position [0 0 1 1]`, `XLim [0 1]`, `YLim [0 1]`, `XTick []`, `YTick []`, `Box 'off'`, `Color theme.ToolbarBackground`, `YDir 'normal'`, `HitTest 'on'`, `PickableParts 'visible'`, and store it in `obj.hAxes_`. Use `obj.Theme_` for all colors. + +(d) Brand at left (drawn in `hAxes_`): a small filled square via `patch` (or `rectangle`) using `FaceColor = theme.DragHandleColor` and no visible edge, plus the dashboard name (`obj.Engine.Name`) as a `text()` object, bold, `Color = theme.GroupHeaderFg`, `FontSize = theme.HeaderFontSize`, left-aligned. Store handles in `hBrandSquare_`/`hBrandText_`. Set the name text `ButtonDownFcn` to a new private handler `onNameClick_(obj)` that prompts a rename via `inputdlg` (default = current `obj.Engine.Name`); on a non-empty, non-cancel result it applies the rename EXACTLY like the existing `onNameEdit` path — set `obj.Engine.Name` and `set(obj.Engine.hFigure, 'Name', newName)` — then updates the `hBrandText_` String (and the hidden `hTitleText` String too, keeping them consistent). Leave the existing `onNameEdit` method intact (still wired to the hidden `hTitleText`). + +(e) Right-aligned group of flat "buttons" drawn right-to-left in `hAxes_`, in this order (rightmost last-update text, then buttons leftward): last-update text, then Events, Follow, Live, Export, Image, Config, Redraw, Sync, Info — OR equivalently lay them out so the visible left-to-right reading order is Info, Sync, Redraw, Config, Image, Export, Live, Follow, Events with the last-update text right-aligned at the far right. Each button = a `patch` rectangle (`FaceColor = theme.WidgetBackground`, `EdgeColor = theme.WidgetBorderColor`, `LineWidth 1`) + a centered `text` label (`Color = theme.ToolbarFontColor`, `FontSize = theme.HeaderFontSize - 3`). Store each button's patch+label in the matching `CustomBtns_` field. Add a private helper `addCustomButton_(obj, key, label, xLeft, width, onClickFcn)` to reduce repetition. + +(f) Wire clicks: set `ButtonDownFcn` on BOTH the patch AND its label for every button to call the SAME existing handler: + - info -> `obj.onInfo()` + - sync -> `obj.Engine.resetGlobalTime()` + - redraw -> `obj.onReset()` + - config -> `obj.onConfig()` + - image -> `obj.onImage()` + - export -> `obj.onExport()` + For the three toggles, the click handler MUST FIRST flip the hidden uicontrol's `Value` (0<->1) so state stays consistent, THEN call the on*Toggle method passing the HIDDEN handle: + - live -> set `obj.hLiveBtn` Value to `~get(obj.hLiveBtn,'Value')`, then `obj.onLiveToggle(obj.hLiveBtn)` + - follow -> set `obj.hFollowBtn` Value to the flipped value, then `obj.onFollowToggle(obj.hFollowBtn)` + - events -> set `obj.hEventsBtn` Value to the flipped value, then `obj.onEventsToggle(obj.hEventsBtn)` + +(g) Last-update text: a right-aligned `text()` in `hAxes_`, `Color` a lightened `theme.ToolbarFontColor` (scalar blend toward `theme.ToolbarBackground`, matching the existing hidden-label lighten pattern — a scalar multiply/blend, NOT a hardcoded triplet), initial String `'Last update: —'`. Store in `hLastUpdateText_`. + +All colors and font sizes must come from `obj.Theme_` fields only. Do not introduce any hardcoded hex or RGB triplet literal. + + + cd "$(git rev-parse --show-toplevel)" && F=libs/Dashboard/DashboardToolbar.m; test -n "$(grep -n 'DashboardToolbarAxes' $F)" && test -n "$(grep -n 'hideNativeControls_' $F)" && test -n "$(grep -n 'buildCustomLayer_' $F)" && test -n "$(grep -n 'onNameClick_' $F)" && test -n "$(grep -n 'CustomBtns_' $F)" && echo VERIFY_OK + cd "$(git rev-parse --show-toplevel)" && F=libs/Dashboard/DashboardToolbar.m; test "$(grep -v '^\s*%' $F | grep -c 'delete(obj.h')" -eq 0 && echo NO_DELETES_OK + cd "$(git rev-parse --show-toplevel)" && F=libs/Dashboard/DashboardToolbar.m; test "$(grep -cE '#[0-9A-Fa-f]{6}' $F)" -eq 0 && test "$(grep -cE '\[[0-9]\.[0-9]+ +[0-9]\.[0-9]+ +[0-9]\.[0-9]+\]' $F)" -eq 0 && echo NO_HARDCODED_COLOR_OK + + + - All 14 native handles are set Visible='off' via `hideNativeControls_` and none are deleted (still valid handles). + - A custom axes tagged 'DashboardToolbarAxes' fills obj.hPanel and hosts the brand square + bold name, the 9 flat buttons/toggles (patch + centered label), and the right-aligned last-update text. + - Every button's patch AND label route clicks to the existing handler; the three toggles flip the hidden control Value first, then call on*Toggle with the hidden handle. + - Clicking the brand name prompts inputdlg and applies the rename exactly like onNameEdit (Engine.Name + figure Name), updating both the custom brand text and the hidden hTitleText. + - No hardcoded color literals; all colors/sizes read from obj.Theme_. + - Constructor signature, Height (0.04), getContentArea(), and all existing on* methods are unchanged; existing native control Positions/creation order untouched. + + + + + Task 2: Active-state repaint of custom pills + dual-write last-update, preserving the hidden-handle contract + libs/Dashboard/DashboardToolbar.m + +Extend the three active-state methods and the last-update method so the custom layer tracks state, WITHOUT removing their existing behavior. + +(a) Add a private repaint helper `paintToggleActive_(obj, key, isActive, activeColor)` that looks up `obj.CustomBtns_.(key)` and, when active, sets its patch `FaceColor = activeColor` and its label `Color` to white (`[1 1 1]` is the one exception explicitly allowed by the design for the active-pill label — treat it as the semantic "on-fill foreground", not a themed surface color); when inactive, restores patch `FaceColor = theme.WidgetBackground` and label `Color = theme.ToolbarFontColor`. Guard for empty/invalid handles. + +(b) `setLiveActiveIndicator(obj, isActive)`: KEEP the existing body that sets `obj.hLivePanel` HighlightColor to `InfoColor` (active) / `ToolbarBackground` (inactive) — the must-pass tests read `hLivePanel` HighlightColor. THEN additionally call `obj.paintToggleActive_('live', isActive, obj.Theme_.StatusOkColor)` so the Live pill turns green when active. + +(c) `setFollowActiveIndicator(obj, isActive)`: KEEP the existing hidden `hFollowPanel` HighlightColor behavior, THEN additionally call `obj.paintToggleActive_('follow', isActive, obj.Theme_.InfoColor)`. + +(d) `setEventsActiveIndicator(obj, isActive)`: KEEP the existing hidden `hEventsPanel` HighlightColor behavior AND the existing `hEventsBtn` String/Value updates (tests + DashboardEngine.m:227 depend on this), THEN additionally call `obj.paintToggleActive_('events', isActive, obj.Theme_.InfoColor)`. + +(e) `setLastUpdateTime(obj, t)`: KEEP the existing signature and the existing update of the hidden `hLastUpdate` String (hot-path datevec logic unchanged). THEN additionally set the custom `hLastUpdateText_` String to the same `['Last update: ' timeStr]`, guarded by isempty/ishandle. Compute `timeStr` once and apply to both. + +(f) Events starts active (`hEventsBtn` Value defaults to 1 in the constructor). Ensure the initial custom Events pill reflects that: after building the custom layer in the constructor, call `obj.setEventsActiveIndicator(logical(get(obj.hEventsBtn,'Value')))` (and, if desired, the Live/Follow indicators with their current Values) so the initial paint matches the hidden state. + +Do not alter `onLiveToggle`, `onFollowToggle`, `onEventsToggle`, `onReset`, `onConfig`, `onExport`, `onImage`, `onInfo`, `onNameEdit`, or `getContentArea`. + + + cd "$(git rev-parse --show-toplevel)" && F=libs/Dashboard/DashboardToolbar.m; test -n "$(grep -n 'paintToggleActive_' $F)" && test -n "$(grep -n 'hLastUpdateText_' $F)" && echo HELPERS_OK + cd "$(git rev-parse --show-toplevel)" && F=libs/Dashboard/DashboardToolbar.m; awk '/function setLiveActiveIndicator/{f=1} f&&/hLivePanel/{a=1} f&&/paintToggleActive_/{b=1} /^ end/{if(f){f=0}} END{exit (a&&b)?0:1}' $F && echo LIVE_KEEPS_PANEL_AND_REPAINTS + cd "$(git rev-parse --show-toplevel)" && F=libs/Dashboard/DashboardToolbar.m; awk '/function setLastUpdateTime/{f=1} f&&/hLastUpdate[^T]/{a=1} f&&/hLastUpdateText_/{b=1} END{exit (a&&b)?0:1}' $F && echo LASTUPDATE_DUAL_WRITE + cd "$(git rev-parse --show-toplevel)" && F=libs/Dashboard/DashboardToolbar.m; awk '/function setEventsActiveIndicator/{f=1} f&&/hEventsPanel/{a=1} f&&/hEventsBtn/{b=1} f&&/paintToggleActive_/{c=1} END{exit (a&&b&&c)?0:1}' $F && echo EVENTS_CONTRACT_PRESERVED + + + - setLiveActiveIndicator still sets hLivePanel HighlightColor (InfoColor/ToolbarBackground) AND repaints the Live pill green (StatusOkColor) when active. + - setFollowActiveIndicator and setEventsActiveIndicator keep their hidden *Panel HighlightColor (and Events keeps its hEventsBtn String/Value) behavior AND repaint the custom pill (InfoColor). + - setLastUpdateTime keeps its signature and updates BOTH the hidden hLastUpdate and the custom hLastUpdateText_ with the same timestamp string. + - The initial custom pills reflect the hidden controls' initial Values (Events active by default). + - No existing on* callback method or getContentArea was modified. + + + + + + +## Trust Boundaries + +No new trust boundary is introduced. This is a cosmetic, single-file MATLAB UI change: no new external input, no network/IPC surface, no package-manager installs, no file/serialization format change, and no change to the constructor signature or persisted dashboard state. + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-gp9-01 | Tampering | `inputdlg` rename in `onNameClick_` | low | accept | Rename only sets `Engine.Name` + figure `Name` (same effect as the existing `onNameEdit`); no eval, no file/path write, no shell-out. Empty/cancel is a no-op. | +| T-gp9-02 | Denial of Service | custom `ButtonDownFcn` handlers | low | mitigate | Handlers delegate to existing, already-tested `on*` methods; toggle handlers only flip a hidden `Value` then call the existing method — no new long-running or re-entrant path added. | + + + +## Executor (static, no MATLAB) — automated + +Run the per-task `` grep/awk gates above. They confirm the structural contract: natives hidden not deleted, custom axes + helpers present, no hardcoded colors, and the indicator/last-update methods keep the hidden-handle behavior while adding the custom repaint. + +Additionally, before finishing, statically confirm the preserved contract: +- `grep -n "Height = 0.04" libs/Dashboard/DashboardToolbar.m` still present. +- `grep -n "function contentArea = getContentArea" libs/Dashboard/DashboardToolbar.m` unchanged. +- `grep -n "function obj = DashboardToolbar(engine, hFigure, theme)" libs/Dashboard/DashboardToolbar.m` unchanged signature. + +## Human orchestrator (live MATLAB via matlab MCP) — the executor CANNOT run these + +Per project convention (memory: `running-matlab-tests-via-mcp`), executors have no MATLAB MCP tools. The human orchestrator performs the live verification after execution: + +1. Static analysis: `check_matlab_code` on `libs/Dashboard/DashboardToolbar.m` — clean. +2. Visual: render `example_dashboard_all_widgets` with `Theme='light'`, `exportapp` a screenshot, and compare the toolbar region to Pencil frame `CK5l7` / redline **d** of the design spec (brand square + name at left, grouped flat buttons + Live/Follow/Events pills at right, right-aligned last-update). Toggle Live → pill turns green; toggle Events/Follow → pill turns blue. +3. Must-pass suites (all must stay green): + - `tests/suite/TestDashboardInfo.m` + - `tests/suite/TestDashboardEventsToggle.m` + - `tests/suite/TestDashboardToolbarImageExport.m` + - `tests/test_dashboard_toolbar_buttons.m` + - `tests/test_dashboard_toolbar_image_export.m` + - `tests/test_dashboard_config_dialog.m` + - `tests/test_dashboard_events_toggle.m` + - `tests/test_dashboard_stale_banner.m` + + + +- `libs/Dashboard/DashboardToolbar.m` is the ONLY changed file. +- Native toolbar handles are hidden (Visible='off') but valid; the custom axes+patch layer renders the light-mode toolbar; every click routes to the existing handler; toggles flip the hidden Value first. +- Active-state indicators repaint the custom pills (Live=green, Follow/Events=blue) while preserving the hidden *Panel HighlightColor behavior the tests assert. +- setLastUpdateTime dual-writes; constructor signature, Height, getContentArea, and all on* methods are unchanged. +- All colors/sizes come from theme tokens; no hardcoded color literals; cheap-render primitives only. +- The eight must-pass suites pass in the human orchestrator's live MATLAB run; the toolbar visually matches redline **d**. +- Known limitations (theme-switch re-theming of the custom layer; config-dialog rename not refreshing the custom brand label) are recorded in the SUMMARY, not silently worked around by editing forbidden files. + + + +Create `.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-SUMMARY.md` when done. + diff --git a/.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-SUMMARY.md b/.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-SUMMARY.md new file mode 100644 index 00000000..6ddf5a2e --- /dev/null +++ b/.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-SUMMARY.md @@ -0,0 +1,165 @@ +--- +phase: 260709-gp9 +plan: 01 +subsystem: ui +tags: [matlab, dashboard, toolbar, light-mode, uicontrol-shim, axes-patch] + +requires: [] +provides: + - "Custom-drawn (axes+patch) light-mode toolbar visual layer in DashboardToolbar, replacing the look of the native uicontrol chrome while every native handle stays alive and hidden" + - "Compatibility shim pattern (hide-not-delete native controls + custom overlay routing clicks to existing handlers) reusable for future classical-figure chrome redesigns" +affects: [dashboard-ui-refresh, light-mode-design] + +tech-stack: + added: [] + patterns: + - "Hide-not-delete compatibility shim: native uicontrol/uipanel handles set Visible='off' (never deleted) so external readers (DashboardEngine.applyTheme, DashboardConfigDialog, tests) keep working against the same handle contract" + - "Custom-drawn cheap-render toolbar: one full-panel axes (Tag 'DashboardToolbarAxes') hosting patch rectangles + text labels, all colors/sizes sourced from the theme struct only" + - "Toggle click wiring: custom pill click flips the hidden uicontrol's Value first, then calls the existing on*Toggle(hiddenHandle) method — keeps Engine.startLive/stopLive and the *ActiveIndicator methods in sync" + +key-files: + created: [] + modified: + - libs/Dashboard/DashboardToolbar.m + +key-decisions: + - "Custom button reading order (Info, Sync, Redraw, Config, Image, Export, Live, Follow, Events, then last-update text right-aligned) differs from the native right-to-left creation order — explicitly allowed by the plan as an 'equivalent' visual layout matching the light-mode mock; native Positions/creation order are untouched so the handle contract and TestDashboardToolbarImageExport ordering assertion are unaffected" + - "White ([1 1 1]) label color on an active toggle pill is the one explicit non-themed literal the design permits — the semantic 'on-fill foreground' against an accent-colored patch, not a themed surface color" + - "Split the single-file change into two atomic commits mirroring the two plan tasks (visual layer + click wiring, then active-state repaint + dual-write) by temporarily reverting Task 2's additions, committing Task 1, then reapplying Task 2 — since both tasks land in the same class file with no independent hunks" + +patterns-established: + - "Compatibility-shim redesign for classical MATLAB figure chrome: keep every pre-existing handle valid+hidden, paint a new custom layer on top, route all interaction back to the original handlers" + +requirements-completed: [GP9-TOOLBAR-LIGHT] + +coverage: + - id: D1 + description: "Custom-drawn axes+patch toolbar layer (brand square+name, 9 flat buttons/toggles, right-aligned last-update text) replaces native uicontrol look in light mode" + requirement: "GP9-TOOLBAR-LIGHT" + verification: + - kind: manual_procedural + ref: "Render example_dashboard_all_widgets Theme='light'; visually compare toolbar to redline d of docs/design/fastsense-light-1to1-design-spec.md" + status: unknown + human_judgment: true + rationale: "Visual/pixel-level match to the Pencil mock requires a live MATLAB render + screenshot comparison; the executor has no MATLAB MCP tools available and cannot run or view a rendered figure." + - id: D2 + description: "All 14 native toolbar handles remain valid (Visible='off', never deleted); DashboardEngine.applyTheme / DashboardConfigDialog / existing tests keep reading them unchanged" + requirement: "GP9-TOOLBAR-LIGHT" + verification: + - kind: unit + ref: "tests/suite/TestDashboardToolbarImageExport.m (asserts hConfigBtn.x < hImageBtn.x < hExportBtn.x from stored native Positions)" + status: unknown + - kind: unit + ref: "tests/test_dashboard_toolbar_buttons.m, tests/test_dashboard_config_dialog.m, tests/test_dashboard_events_toggle.m, tests/suite/TestDashboardEventsToggle.m, tests/suite/TestDashboardInfo.m, tests/test_dashboard_toolbar_image_export.m, tests/test_dashboard_stale_banner.m" + status: unknown + human_judgment: true + rationale: "Executor has no MATLAB MCP tools per project convention (memory: running-matlab-tests-via-mcp); the human orchestrator must run the 8 must-pass suites live and confirm all stay green." + - id: D3 + description: "Custom toggle clicks flip the hidden control's Value then call the existing on*Toggle(hiddenHandle) method; active-state repaint (Live=green, Follow/Events=blue) while hidden *Panel HighlightColor / hEventsBtn String-Value behavior is preserved" + requirement: "GP9-TOOLBAR-LIGHT" + verification: + - kind: unit + ref: "grep/awk structural gates in the plan's blocks (all passed statically — see below)" + status: pass + - kind: manual_procedural + ref: "Live click-through: toggle Live/Follow/Events on the rendered dashboard, confirm pill color + hidden-handle Value/HighlightColor stay in sync" + status: unknown + human_judgment: true + rationale: "Structural/static gates pass, but functional click-through behavior in a live MATLAB figure can only be confirmed by the human orchestrator running the app." + +duration: 25min +completed: 2026-07-09 +status: complete +--- + +# Quick Task 260709-gp9: Custom-Drawn Dashboard Toolbar (Light Mode) Summary + +**Compatibility-shim rewrite of `DashboardToolbar`: 14 native uicontrol/uipanel handles hidden (never deleted) behind a new custom axes+patch visual layer — brand square+bold name, 9 flat bordered buttons/toggles, right-aligned last-update text — with every click routed back to the existing `on*` handlers and the three toggle handlers dual-writing the hidden control's `Value` before calling the original method.** + +## Performance + +- **Duration:** ~25 min +- **Tasks:** 2 (both `type="auto"`, no checkpoints) +- **Files modified:** 1 (`libs/Dashboard/DashboardToolbar.m`) + +## Accomplishments + +- Native toolbar chrome (title/edit box, Info/Export/Image/Config/Sync/Redraw pushbuttons, Live/Follow/Events toggle+panel pairs, last-update label) is now hidden via a single `hideNativeControls_()` sweep — every handle stays valid, guarded by `isempty`/`ishandle` before `set(..., 'Visible', 'off')`. +- New custom-drawn layer (`buildCustomLayer_()`) paints one full-panel `axes` (Tag `'DashboardToolbarAxes'`) with: + - Brand: a small `DragHandleColor` patch + bold `GroupHeaderFg` dashboard-name text at left, click-to-rename via `inputdlg` (`onNameClick_`) applying the rename exactly like `onNameEdit` (sets `Engine.Name` + figure `Name`) and dual-writing both the custom brand text and the hidden `hTitleText`. + - 9 flat bordered "buttons" (patch + centered label, `WidgetBackground`/`WidgetBorderColor`/`ToolbarFontColor`) via a shared `addCustomButton_()` helper: Info, Sync, Redraw, Config, Image, Export, Live, Follow, Events. + - A right-aligned last-update text using the same lightened-font-color blend as the original hidden label. +- Every button's patch AND label route `ButtonDownFcn` to the same existing handler (`onInfo`, `resetGlobalTime`, `onReset`, `onConfig`, `onImage`, `onExport`). The three toggles (`customLiveClick_`, `customFollowClick_`, `customEventsClick_`) flip the hidden uicontrol's `Value` first, then call `onLiveToggle`/`onFollowToggle`/`onEventsToggle` with the hidden handle — so `Engine.startLive`/`stopLive` and the existing indicator methods stay authoritative. +- `setLiveActiveIndicator`/`setFollowActiveIndicator`/`setEventsActiveIndicator` keep their exact original hidden-handle behavior (`hLivePanel`/`hFollowPanel`/`hEventsPanel` `HighlightColor`, plus `hEventsBtn` `String`/`Value` for Events) and additionally call a new `paintToggleActive_()` helper to repaint the matching custom pill (Live→`StatusOkColor`, Follow/Events→`InfoColor`, white label when active; `WidgetBackground`/`ToolbarFontColor` when inactive). +- `setLastUpdateTime` keeps its exact signature and hot-path `datevec` logic, now computing the timestamp string once and dual-writing both the hidden `hLastUpdate` and the new `hLastUpdateText_`. +- Constructor primes the three custom pills from the hidden controls' initial `Value` right after building the custom layer (Events starts active by default, matching the pre-existing `hEventsBtn` `Value=1` default). + +## Task Commits + +1. **Task 1: Hide native controls and build the custom-drawn visual layer with click wiring** - `4b95245f` (feat) +2. **Task 2: Active-state repaint of custom pills + dual-write last-update, preserving the hidden-handle contract** - `7a294af2` (feat) + +_Note: the two commits split the same file along the plan's task boundaries — Task 2's active-state/dual-write additions were temporarily reverted, Task 1 committed, then Task 2's additions reapplied and committed, so each commit corresponds to exactly one plan task._ + +**Plan metadata:** (pending — orchestrator commits SUMMARY.md/STATE.md separately per workflow contract) + +## Files Created/Modified + +- `libs/Dashboard/DashboardToolbar.m` - Added 5 new private properties (`hAxes_`, `hBrandSquare_`, `hBrandText_`, `hLastUpdateText_`, `CustomBtns_`), 9 new private methods (`hideNativeControls_`, `buildCustomLayer_`, `addCustomButton_`, `paintToggleActive_`, `onNameClick_`, `customLiveClick_`, `customFollowClick_`, `customEventsClick_`), extended `setLiveActiveIndicator`/`setFollowActiveIndicator`/`setEventsActiveIndicator`/`setLastUpdateTime`, and 3 lines appended to the constructor. No existing native control creation, Position, or the `on*` callback bodies were touched. + +## Decisions Made + +- Custom button left-to-right reading order (Info, Sync, Redraw, Config, Image, Export, Live, Follow, Events, then last-update text) intentionally differs from the native right-to-left creation order — the plan explicitly permits this as an "equivalent" visual layout matching the light-mode mock, since native Positions/creation order (and the `TestDashboardToolbarImageExport` assertion that reads them) are left untouched. +- `[1 1 1]` white label color on an active pill is the one explicit non-themed literal the design permits (semantic "on-fill foreground"); every other color/size comes from `obj.Theme_`. +- Split the single-file change into two atomic git commits by temporarily reverting/reapplying Task 2's additions, since both tasks land in the same class with no independently stageable hunks. + +## Deviations from Plan + +None - plan executed exactly as written. Both tasks' `` and `` criteria were implemented as specified; no Rule 1-4 auto-fixes were needed. + +## Known Limitations (recorded per plan instruction, not fixed) + +- **Theme-switch re-theming:** A live dark↔light theme toggle repaints only the hidden handles via `DashboardEngine.applyTheme`; the custom layer will not re-theme until the toolbar is reconstructed. Acceptable per plan — this change targets the light mock only, and fixing it would require editing `DashboardEngine.m` (forbidden file). +- **Config-dialog rename staleness:** `DashboardConfigDialog` rename updates the hidden `hTitleText` only; the visible custom brand label will not reflect that rename until the toolbar is rebuilt. Flagged per plan, not fixed by editing the dialog (forbidden file). Note: the NEW `onNameClick_` path added by this plan (clicking the custom brand text) DOES dual-write both the custom brand text and `hTitleText` correctly — only the pre-existing `DashboardConfigDialog` rename path is one-way. +- **No test files touched:** Per constraints, no test file was edited. If any of the 8 must-pass suites listed in the plan's `` section assert a now-invalid VISUAL (as opposed to the handle contract — `.Value`/`.HighlightColor`/`.TooltipString`/`.String`/`.Position`/`.BackgroundColor` on the native handles, all preserved), that would need to be flagged by the human orchestrator during the live MATLAB run; no such case was identified from static review of the test files' assertions during this session (all assertions inspected read handle properties, not rendered pixels). + +## Issues Encountered + +None. + +## Verification Performed (executor, static only) + +All grep/awk gates from the plan's per-task `` blocks were run against the final file state and passed: + +- `DashboardToolbarAxes`, `hideNativeControls_`, `buildCustomLayer_`, `onNameClick_`, `CustomBtns_` all present → `VERIFY_OK` +- Zero `delete(obj.h...)` occurrences → `NO_DELETES_OK` +- Zero hex-color / decimal-RGB-triplet literals → `NO_HARDCODED_COLOR_OK` +- `paintToggleActive_` and `hLastUpdateText_` present → `HELPERS_OK` +- `setLiveActiveIndicator` retains `hLivePanel` AND calls `paintToggleActive_` → `LIVE_KEEPS_PANEL_AND_REPAINTS` +- `setLastUpdateTime` writes both `hLastUpdate` and `hLastUpdateText_` → `LASTUPDATE_DUAL_WRITE` +- `setEventsActiveIndicator` retains `hEventsPanel` + `hEventsBtn` AND calls `paintToggleActive_` → `EVENTS_CONTRACT_PRESERVED` +- Preserved-contract greps confirmed unchanged: `Height = 0.04`, `function contentArea = getContentArea`, `function obj = DashboardToolbar(engine, hFigure, theme)` + +Additionally ran `mh_style` and `mh_lint` (MISS_HIT, available in this environment) on the final file — both report "everything seems fine" with zero findings. + +**Not run by the executor (no MATLAB MCP tools available per project convention):** `check_matlab_code` static analysis, live rendering of `example_dashboard_all_widgets` with `Theme='light'`, visual comparison to the design spec, and the 8 must-pass test suites. These are the human orchestrator's responsibility per the plan's `` section. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +- `DashboardToolbar.m` is ready for the human orchestrator's live MATLAB verification pass: `check_matlab_code`, a light-mode render + screenshot comparison against redline **d** of `docs/design/fastsense-light-1to1-design-spec.md`, and the 8 must-pass suites (`TestDashboardInfo`, `TestDashboardEventsToggle`, `TestDashboardToolbarImageExport`, `test_dashboard_toolbar_buttons`, `test_dashboard_toolbar_image_export`, `test_dashboard_config_dialog`, `test_dashboard_events_toggle`, `test_dashboard_stale_banner`). +- No blockers identified from static review. If the live run surfaces a genuinely-invalid VISUAL assertion in a test (as opposed to a handle-contract read), that is expected per the plan and should be flagged rather than silently patched. + +--- +*Phase: 260709-gp9* +*Completed: 2026-07-09* + +## Self-Check: PASSED + +- FOUND: `libs/Dashboard/DashboardToolbar.m` +- FOUND: commit `4b95245f` +- FOUND: commit `7a294af2` +- FOUND: `.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-SUMMARY.md` diff --git a/.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-VERIFICATION.md b/.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-VERIFICATION.md new file mode 100644 index 00000000..6b72cdd3 --- /dev/null +++ b/.planning/quick/260709-gp9-custom-drawn-dashboard-toolbar-light-mod/260709-gp9-VERIFICATION.md @@ -0,0 +1,64 @@ +--- +phase: 260709-gp9 +verifier: human-orchestrator (live MATLAB via matlab MCP) +date: 2026-07-09 +status: passed +--- + +# Quick Task 260709-gp9 — Verification + +Live MATLAB verification of the custom-drawn dashboard toolbar. The gsd-executor +has no MATLAB MCP tools (project convention), so all runtime checks below were +performed by the orchestrator. + +## must_have coverage + +| id | description | result | +|----|-------------|--------| +| D1 | Custom axes+patch toolbar layer replaces native look in light mode | **PASS** | +| D2 | 14 native handles valid but `Visible='off'`; readers keep working | **PASS** | +| D3 | Toggle click flips hidden Value + calls existing handler; active repaint | **PASS** | + +## Evidence + +**Live render** — `example_dashboard_all_widgets` `Theme='light'`, `exportapp` screenshots: +- `DashboardToolbarAxes` present (`CUSTOM_AXES_PRESENT=1`). +- Toolbar shows brand (accent square + bold dark name) at left; grouped flat + bordered buttons Info · Sync · Redraw · Config · Image · Export, then toggles + Live · Follow · Events; right-aligned "Last update" — matches redline **d** of + `docs/design/fastsense-light-1to1-design-spec.md` §4.2. +- `setLiveActiveIndicator(true)` repaints the **green Live pill** (`StatusOkColor`); + Events shows the blue active pill (`InfoColor`). Confirmed in the second screenshot. + +**Handle contract** — `hLiveBtn` valid, `Visible=off`; `hConfigBtn`/`hImageBtn`/ +`hExportBtn` all valid handles. + +**Must-pass suites (8):** + +| suite | result | +|-------|--------| +| TestDashboardInfo | 19 passed / 0 failed | +| TestDashboardEventsToggle | 22 passed / 0 failed | +| TestDashboardToolbarImageExport | 9 passed / 0 failed (button position-order assertion held) | +| test_dashboard_toolbar_buttons | 7 passed / 0 failed | +| test_dashboard_toolbar_image_export | 4 passed / 0 failed | +| test_dashboard_events_toggle | 22 passed / 0 failed | +| test_dashboard_stale_banner | 9 passed / 0 failed | +| test_dashboard_config_dialog | 8 passed / **1 failed** (see below) | + +**Static:** `mh_style` + `mh_lint` clean (executor); no hardcoded colors; no +`delete(obj.h*)`. + +## Pre-existing failure (NOT a regression) + +`test_dashboard_config_dialog :: testAllControlsHaveTooltips` — "control 7 +(EventMarkersVisible) missing tooltip". Proven pre-existing by A/B: reverting +`DashboardToolbar.m` to base `95e1d00a` (no gp9 change) reproduces the identical +failure. The config dialog sources each control's tooltip from its own `spec.tip`, +independent of the toolbar. Filed as a separate task; not in scope for gp9. + +## Verdict + +**PASSED.** Redline (d) is implemented and matches the mock within the cheap-render +budget. All handle-contract suites green; the single failure is a confirmed +pre-existing config-dialog bug unrelated to this change. diff --git a/.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-PLAN.md b/.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-PLAN.md new file mode 100644 index 00000000..1100f8cb --- /dev/null +++ b/.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-PLAN.md @@ -0,0 +1,197 @@ +--- +phase: 260709-ikg +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - libs/Dashboard/DashboardLayout.m +autonomous: true +requirements: + - IKG-01 +quick: true + +must_haves: + truths: + - "On a rendered dashboard, each FastSense plot shows only two chrome buttons (Info 'i' and Detach '^') plus one new overflow button (…); the X/V/A/L/+ strip is gone from the visible chrome." + - "Clicking the … button posts a context menu whose items drive the SAME actions the folded X/V/A/L/+ buttons drove (crosshair link, auto-fit Y visible, auto-fit Y all, plant-log show/hide, create event)." + - "Menu item check/label state reflects live widget state (CrosshairLinked, YLimitMode, ShowPlantLog) because the menu is rebuilt fresh on every click." + - "All 7 must-pass suites stay green: the folded buttons remain alive (tags, positions, callbacks, UserData active-state) — only their Visible flag flips to 'off'." + - "A menu-post failure never crashes the refresh loop (try/catch → namespaced warning DashboardLayout:overflowMenuFailed)." + artifacts: + - "libs/Dashboard/DashboardLayout.m — addOverflowMenu_ (private) creates the OverflowMenuButton" + - "libs/Dashboard/DashboardLayout.m — showOverflowMenu_ (private) builds+posts the fresh uicontextmenu" + - "libs/Dashboard/DashboardLayout.m — reflowChrome_ (static) anchors OverflowMenuButton" + key_links: + - "showOverflowMenu_ uimenu callbacks must route to the existing action methods (onCrosshairLinkTogglePressed_, onYLimitButtonClicked_, onPlantLogTogglePressed_, invokeCreateEventCallback_) so no test/behaviour regresses." + - "The 5 folded uicontrols must keep Visible='off' through every callback-driven rebuild (addCrosshairLinkToggle / addPlantLogToggle / addYLimitButton_) — the rebuild reuses the same creation code, so the shim is applied once at the creation site." + - "addOverflowMenu_ must run AFTER all 5 folded buttons are added and BEFORE the reflowChrome_ that anchors it (both in realizeWidget and in each rebuild method)." +--- + + +Declutter the per-widget chrome strip on every dashboard plot (redline (e) of docs/design/fastsense-light-1to1-design-spec.md). Fold the five secondary chrome buttons — CrosshairLink ('X'), YLimit-Visible ('V'), YLimit-All ('A'), PlantLog ('L'), CreateEvent ('+') — behind a single overflow "…" button that posts a context menu. Keep Info ('i') and Detach ('^') visible. (IKG-01) + +Purpose: The `X V A L +` cluster butts against the Info button (a known 0-px-gap nit) and reads as noise on every plot; the settled design (user chose "Overflow ⋯ menu", compatibility-shim pattern, prototyped live this session) hides the five buttons WITHOUT deleting them so all 130 existing assertions across 7 files stay green. + +Output: Modifications to a single file — `libs/Dashboard/DashboardLayout.m`. No test edits, no other files. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/STATE.md + +# The ONLY file to change: +@libs/Dashboard/DashboardLayout.m + +# Design authority (redline e, §1 cheap-render budget, §2 tokens): +@docs/design/fastsense-light-1to1-design-spec.md + + + +- ONLY `libs/Dashboard/DashboardLayout.m` may change. Do NOT edit any test file or any other source file. +- Cheap-render only (spec §1). Theme tokens only — `theme.ToolbarBackground` (bg) and `theme.ToolbarFontColor` (fg). No hardcoded colors. +- Keep the five folded uicontrols fully ALIVE: Tag, Position, FontSize, FontWeight, ForegroundColor/BackgroundColor, Callback, TooltipString, UserData active-state sync all unchanged. The ONLY change to them is adding `'Visible', 'off'`. +- Do NOT touch InfoIconButton or DetachButton in any way — they stay visible. +- Do NOT add any `delete()` of a folded button. Existing idempotent `delete(prior)` calls stay as-is. +- The executor has NO MATLAB tooling. Live MATLAB verification (render `example_dashboard_all_widgets` Theme='light', screenshot showing the folded buttons gone / … present + menu posts, and the 7 must-pass suites) is performed by the HUMAN ORCHESTRATOR, not the executor. Executor is grep/lint structural only. +- If any test is found to assert a folded button's `Visible` state (grep this session found NONE), flag it in the SUMMARY and do NOT edit the test. + + + + + + Task 1: Shim — hide the five folded chrome buttons (Visible off), keep them alive + libs/Dashboard/DashboardLayout.m + +Add `'Visible', 'off'` to the `uicontrol(...)` creation of the FIVE folded buttons ONLY, at their four creation sites. Change NOTHING else about those uicontrols (keep Tag, Units, Position, FontSize, FontWeight, ForegroundColor, BackgroundColor, Enable, Callback, TooltipString exactly as-is). The four sites: + +1. `addPlantLogToggle` — the `uicontrol('Parent', bar, 'Style', 'pushbutton', 'String', 'L', ...)` (Tag 'PlantLogToggleButton', ~line 712). Add the visibility property to that call. +2. `addCrosshairLinkToggle` — the `uicontrol('Parent', bar, 'Style', 'pushbutton', 'String', 'X', ...)` (Tag 'CrosshairLinkButton', ~line 811). Add it. +3. `addYLimitButton_` — the single shared helper `uicontrol('Parent', bar, 'Style', 'pushbutton', 'String', glyph, ...)` (Tag `tagName`, ~line 1087). This one call constructs BOTH the 'V' (YLimitVisibleBtn) and 'A' (YLimitAllBtn) buttons, so adding it here folds both. Add it. +4. `addCreateEventButton` — the `uicontrol('Parent', bar, 'Style', 'pushbutton', 'String', '+', ...)` (Tag 'CreateEventButton', ~line 1131). Add it. + +Do NOT add the visibility property to `addInfoIcon` (Tag 'InfoIconButton', ~line 980) or `addDetachButton` (Tag 'DetachButton', ~line 1004) — those two stay visible. + +Rationale to preserve: the folded buttons are re-created on every callback-driven rebuild via these SAME creation methods (addCrosshairLinkToggle self-rebuilds; onPlantLogTogglePressed_ re-calls addPlantLogToggle; the YLimit path re-calls addYLimitButton_). Because the shim lives at the shared creation site, every rebuild automatically re-hides them. Tests assert their Tag/Position/Callback/Enable/active-bg — none assert Visible (grep-confirmed this session), so hiding-but-alive keeps all 7 suites green. + + + cd libs/Dashboard && grep -c "'Visible', *'off'" DashboardLayout.m + Expect the count to INCREASE by exactly 4 versus the pre-edit baseline (one per creation site: L, X, YLimit-helper, +). Confirm InfoIconButton and DetachButton blocks are untouched: `grep -n "'Tag', 'InfoIconButton'" DashboardLayout.m` and `grep -n "'Tag', 'DetachButton'" DashboardLayout.m` still resolve, and neither method (addInfoIcon ~968-992, addDetachButton ~994-1015) contains a Visible property. If MISS_HIT is installed: `mh_style libs/Dashboard/DashboardLayout.m` and `mh_lint libs/Dashboard/DashboardLayout.m` report clean. + + The four folded creation sites (L, X, YLimit-helper covering V+A, +) each carry `'Visible', 'off'`; addInfoIcon and addDetachButton are byte-unchanged; no `delete()` of a folded button was added; MISS_HIT (if present) clean. + + + + Task 2: Add addOverflowMenu_ + showOverflowMenu_ and wire them in + libs/Dashboard/DashboardLayout.m + +Add two new PRIVATE instance methods in the `methods (Access = private)` block, next to the sibling chrome builders (after `invokeCreateEventCallback_`, before the block's closing `end` at ~line 1159), then wire them into `realizeWidget` and the three callback-driven rebuild methods. + +(A) `addOverflowMenu_(obj, widget)`: + - Resolve theme: `if isempty(widget.ParentTheme) || ~isstruct(widget.ParentTheme), theme = DashboardTheme('light'); else theme = widget.ParentTheme; end` (same pattern as the neighbors). + - `bar = obj.getOrCreateButtonBar_(widget);` + - Idempotent cleanup: `findobj(bar, 'Tag', 'OverflowMenuButton', '-depth', 1)` and delete any prior (guarded `try delete(prior); catch, end`); likewise delete any prior `findobj(ancestor(bar,'figure'), 'Tag', 'OverflowContextMenu')` so stale context menus do not accumulate. + - GUARD: findobj the bar for the five folded tags (CrosshairLinkButton, YLimitVisibleBtn, YLimitAllBtn, PlantLogToggleButton, CreateEventButton). If NONE are present, `return` (non-fold widgets get no … button). + - Create ONE `uicontrol('Parent', bar, 'Style', 'pushbutton', ...)`: String `char(8230)` (the '…' glyph — use char(8230), not a literal multibyte char, for Octave/MISS_HIT safety), Units 'pixels', provisional Position `[2 2 24 24]` (reflowChrome_ re-anchors), FontSize 11, FontWeight 'bold', ForegroundColor `theme.ToolbarFontColor`, BackgroundColor `theme.ToolbarBackground`, Tag 'OverflowMenuButton', TooltipString 'More chart controls', Callback `@(~,~) obj.showOverflowMenu_(widget, bar)`. + - End with a best-effort self-anchor mirroring the siblings: `try DashboardLayout.reflowChrome_(widget.hCellPanel, 28, 2); catch, end`. + +(B) `showOverflowMenu_(obj, widget, bar)`: + - Wrap the entire body in try/catch; on error `warning('DashboardLayout:overflowMenuFailed', 'Overflow menu failed: %s', ME.message)` and return (never crash the refresh loop). + - `fig = ancestor(bar, 'figure');` guard non-empty + ishandle. + - Delete any prior `findobj(fig, 'Tag', 'OverflowContextMenu')`, then build a FRESH `cm = uicontextmenu('Parent', fig, 'Tag', 'OverflowContextMenu');` (fresh each click so Checked/label state is current). + - Add one `uimenu` per PRESENT folded button (findobj the bar by tag; skip absent): + * CrosshairLinkButton present → label 'Link crosshair across page' when `~widget.CrosshairLinked`, else 'Unlink crosshair (stop mirroring)'; set 'Checked' 'on' when `widget.CrosshairLinked` else 'off'; Callback `@(~,~) obj.onCrosshairLinkTogglePressed_(hLink, widget)` where hLink is the hidden CrosshairLinkButton handle. + * YLimitVisibleBtn present → label 'Auto-fit Y to visible X range'; Checked 'on' when `strcmp(widget.YLimitMode,'auto-visible')`; Callback `@(~,~) obj.onYLimitButtonClicked_(widget, 'auto-visible', bar)`. + * YLimitAllBtn present → label 'Auto-fit Y to all data'; Checked 'on' when `strcmp(widget.YLimitMode,'auto-all')`; Callback `@(~,~) obj.onYLimitButtonClicked_(widget, 'auto-all', bar)`. + * PlantLogToggleButton present AND its `Enable` is 'on' → label 'Hide plant log lines' when `widget.ShowPlantLog` else 'Show plant log lines'; Callback `@(~,~) obj.onPlantLogTogglePressed_(hPlant, widget, obj.EngineRef)` where hPlant is the hidden PlantLogToggleButton handle. (When Enable is 'off' — no store — omit the item.) + * CreateEventButton present → label 'Create event from selection / current view'; Callback `@(~,~) obj.invokeCreateEventCallback_(widget)`. + - Post it: `cp = get(fig, 'CurrentPoint'); set(cm, 'Position', cp); set(cm, 'Visible', 'on');` (Position/Visible are settable on a classic-figure uicontextmenu; confirmed live this session). + +(C) Wiring — call `obj.addOverflowMenu_(widget)` guarded (`try ... catch ME, warning('DashboardLayout:overflowMenuFailed', ...); end`) at these points, each AFTER the folded buttons exist and BEFORE the following reflowChrome_: + - `realizeWidget`: immediately after the `addCrosshairLinkToggle` try/catch block (~line 425), before the `DashboardLayout.reflowChrome_(widget.hCellPanel, 28, 2)` at ~line 432. (Crosshair is the last folded button added, so all five exist.) + - `addCrosshairLinkToggle`: right before its closing `try DashboardLayout.reflowChrome_(...)` (~line 823). + - `addPlantLogToggle`: right before its closing `try DashboardLayout.reflowChrome_(...)` (~line 736). + - `addYLimitButtons_`: after `syncYLimitButtonsState_(bar, widget.YLimitMode)` (~line 1080), at the method end. + Keep every call idempotent (addOverflowMenu_ deletes its prior button first, so repeated calls are safe). + +Do not embed fenced code in the file beyond normal MATLAB. All callbacks route to EXISTING methods so no downstream behaviour changes. + + + cd libs/Dashboard && grep -c "function addOverflowMenu_\|function showOverflowMenu_\|obj.addOverflowMenu_(widget)\|'OverflowMenuButton'\|DashboardLayout:overflowMenuFailed" DashboardLayout.m + Expect >= 8 matches: 2 function defs + >= 4 call-sites (realizeWidget + 3 rebuilds) + the Tag literal (button create + idempotent findobj) + the warning id. Confirm the menu routes to existing actions: `grep -c "onCrosshairLinkTogglePressed_\|onYLimitButtonClicked_\|onPlantLogTogglePressed_\|invokeCreateEventCallback_" DashboardLayout.m` increased (new uimenu callbacks reference them). Confirm the glyph is `char(8230)` (not a literal ellipsis): `grep -n "char(8230)" DashboardLayout.m`. If MISS_HIT present: `mh_style` + `mh_lint` on the file report clean. + + addOverflowMenu_ and showOverflowMenu_ exist as private methods; addOverflowMenu_ is called from realizeWidget and the 3 rebuild methods (each before its reflow); showOverflowMenu_ builds a fresh 'OverflowContextMenu' whose items route to the four existing action methods with correct Checked/label state; button uses char(8230), theme tokens only; all failures caught to DashboardLayout:overflowMenuFailed; MISS_HIT (if present) clean. + + + + Task 3: Anchor OverflowMenuButton in reflowChrome_ + libs/Dashboard/DashboardLayout.m + +In the static `reflowChrome_` (~line 1163), inside the `if ~isempty(bar) && ishandle(bar(1))` block, after the existing CrosshairLinkButton anchor block (~line 1247-1251), add anchoring for the OverflowMenuButton so it lands at the leftmost slot the folded cluster used to occupy (left of the visible Info/Detach right cluster): + + - The folded buttons keep valid handles when hidden, so `findobj(...,'Tag','CrosshairLinkButton'/'YLimitVisibleBtn'/'YLimitAllBtn',...)` still resolve and `xLink`, `xVisible`, `xAll` remain computed exactly as today. + - Hoist the `xLink` computation so it is available outside the CrosshairLinkButton `if` block: compute `xLink = xVisible - gap - bw;` once (it is currently assigned only inside that if). Reuse it for both the CrosshairLinkButton set and the new OverflowMenuButton set. + - Add: + `overflow = findobj(bar(1), 'Tag', 'OverflowMenuButton', '-depth', 1);` + `if ~isempty(overflow) && ishandle(overflow(1))` + pick `xOverflow = xLink` when the CrosshairLinkButton handle is present, else `xVisible` when the YLimitVisibleBtn handle is present, else `xAll`; + `set(overflow(1), 'Position', [xOverflow, 2, bw, bw]);` + `end` + (`bw` and `gap` are the locals already defined earlier in the block; `bw = 24`.) + +Add a one-line comment noting the overflow button reuses the leftmost folded slot (per redline (e) design). Change nothing about how Detach/Create/Info/PlantLog/V/A/CrosshairLink are anchored — the invisible folded buttons keep their computed slots (invisible → no visual conflict with the … button). + + + cd libs/Dashboard && grep -c "'Tag', 'OverflowMenuButton'\|xOverflow" DashboardLayout.m + Expect the OverflowMenuButton Tag to appear in reflowChrome_ (findobj) plus `xOverflow` used in the set(). Confirm `xLink` is now referenced by both the CrosshairLink anchor and the overflow anchor (single hoisted assignment): `grep -n "xLink" DashboardLayout.m`. If MISS_HIT present: `mh_style` + `mh_lint` clean. + + reflowChrome_ resolves OverflowMenuButton and sets its Position to the leftmost folded slot (xLink → xVisible → xAll fallback); xLink is hoisted and reused; no other anchor logic changed; MISS_HIT (if present) clean. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| user click → chrome callback | A left-click on the … button posts a context menu; menu items invoke existing dashboard action methods. No external/untrusted input crosses; all data is in-process widget state. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-ikg-01 | Denial of Service | showOverflowMenu_ posting on the figure | low | mitigate | Entire body wrapped in try/catch → namespaced warning DashboardLayout:overflowMenuFailed; a menu-post failure can never crash the DashboardEngine refresh loop (mirrors existing addPlantLogToggle / addCrosshairLinkToggle guards). | +| T-ikg-02 | Tampering | Folded buttons hidden but still callable via menu | low | accept | Menu items route to the SAME existing action methods (onCrosshairLinkTogglePressed_ / onYLimitButtonClicked_ / onPlantLogTogglePressed_ / invokeCreateEventCallback_); no new privilege or code path — behaviour is identical to clicking the (now hidden) buttons. | +| T-ikg-03 | Resource leak | Accumulated uicontextmenu objects across clicks | low | mitigate | showOverflowMenu_ deletes any prior 'OverflowContextMenu' before building a fresh one; addOverflowMenu_ also clears prior button + context menu (idempotent). | + +No package-manager installs in this task (pure MATLAB edit to one existing file) — Package Legitimacy Gate not applicable. + + + +Executor (no MATLAB) — structural only: +- `grep -c "'Visible', *'off'"` increased by exactly 4 (Task 1 folded sites) and neither addInfoIcon nor addDetachButton gained a Visible property. +- addOverflowMenu_ / showOverflowMenu_ exist and are wired into realizeWidget + the 3 rebuild methods; menu callbacks reference the four existing action methods; button glyph is char(8230); theme tokens only. +- reflowChrome_ anchors OverflowMenuButton; xLink hoisted and reused. +- MISS_HIT (`mh_style` + `mh_lint`) clean on `libs/Dashboard/DashboardLayout.m` if the tools are installed. + +Human orchestrator (live MATLAB) — must-pass, NOT run by executor: +- Render `example_dashboard_all_widgets` with Theme='light'; screenshot confirms the X/V/A/L/+ strip is gone from every plot and a single … sits left of Info/Detach; clicking … posts a menu; each item performs its action (crosshair link, auto-fit Y visible/all, plant-log show/hide, create event). +- Suites all green: tests/test_dashboard_widget_button_bar.m, tests/test_dashboard_layout_plant_log_toggle.m, tests/suite/TestDashboardDetach.m, tests/suite/TestDashboardWidget.m, tests/suite/TestInfoTooltip.m, tests/suite/TestDashboardLayoutPlantLogToggle.m, tests/suite/TestIndustrialPlantDemoCompanion.m. + + + +- Only `libs/Dashboard/DashboardLayout.m` changed; no test or other source file touched. +- Five folded buttons (X/V/A/L/+) are Visible='off' but otherwise byte-equivalent (tags/positions/callbacks/state intact); Info + Detach untouched and visible. +- A single OverflowMenuButton (…) is created for fold-eligible widgets, anchored left of the right cluster, posting a fresh state-accurate context menu that routes to the existing action methods. +- All 7 must-pass suites remain green under the human orchestrator's live MATLAB run; the light-mode dashboard screenshot matches redline (e). + + + +Create `.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-SUMMARY.md` when done. +In the SUMMARY, note explicitly: (a) the exact 4 creation sites that received 'Visible','off'; (b) that Info/Detach were left untouched; (c) any test that unexpectedly asserts a folded button's Visible state (none expected — flag, do NOT edit); (d) that live MATLAB verification + the 7 suites are deferred to the human orchestrator. + diff --git a/.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-SUMMARY.md b/.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-SUMMARY.md new file mode 100644 index 00000000..df70745c --- /dev/null +++ b/.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-SUMMARY.md @@ -0,0 +1,99 @@ +--- +phase: 260709-ikg +plan: 01 +subsystem: Dashboard +tags: [dashboard, chrome, overflow-menu, ui-refresh, redline-e] +dependency-graph: + requires: [] + provides: + - "DashboardLayout.addOverflowMenu_ (private) — overflow '...' button" + - "DashboardLayout.showOverflowMenu_ (private) — fresh context menu builder" + - "DashboardLayout.reflowChrome_ anchors OverflowMenuButton in the leftmost folded slot" + affects: + - "Every FastSenseWidget rendered on a dashboard (chrome strip)" +tech-stack: + added: [] + patterns: + - "Compatibility shim: hide-but-keep-alive (Visible='off') identical to the 260709-gp9 toolbar pattern" +key-files: + created: [] + modified: + - libs/Dashboard/DashboardLayout.m +decisions: + - "Overflow button folds X/V/A/L/+ behind a single ... button posting a fresh uicontextmenu on every click (per user-approved design, prototyped live prior session)" + - "Folded uicontrols stay ALIVE (Visible='off' only) so all existing Tag/Position/Callback/UserData-based test assertions remain valid" + - "Menu items route to the SAME existing action methods — no new behavior, only a new entry point" +metrics: + duration: "~25 min" + completed: 2026-07-09 +status: complete +--- + +# Phase 260709-ikg Plan 01: Overflow Menu for Widget Chrome (redline e) Summary + +Folded the five secondary per-widget chrome buttons (CrosshairLink 'X', YLimit-Visible 'V', YLimit-All 'A', PlantLog 'L', CreateEvent '+') behind a single overflow "..." button that posts a context menu, matching redline (e) of the Pencil light-mode design spec — while keeping every folded uicontrol alive so no existing test regresses. + +## What Was Built + +**Task 1 — Compatibility shim (Visible='off' only).** Added `'Visible', 'off'` to the four creation sites that build the five folded buttons: +1. `addPlantLogToggle` (~line 712-724, `libs/Dashboard/DashboardLayout.m`) — Tag `PlantLogToggleButton` ('L'). +2. `addCrosshairLinkToggle` (~line 811-826) — Tag `CrosshairLinkButton` ('X'). +3. `addYLimitButton_` (~line 1087-1100, shared helper) — constructs BOTH `YLimitVisibleBtn` ('V') and `YLimitAllBtn` ('A'); one edit folds both. +4. `addCreateEventButton` (~line 1131-1145) — Tag `CreateEventButton` ('+'). + +Nothing else about these five uicontrols changed — Tag, Units, Position, FontSize, FontWeight, ForegroundColor, BackgroundColor, Enable, Callback, TooltipString, and UserData-driven active-state sync are byte-identical to before. `addInfoIcon` (InfoIconButton) and `addDetachButton` (DetachButton) were left completely untouched — Info and Detach stay visible. + +**Task 2 — Overflow button + menu.** Added two new private instance methods: +- `addOverflowMenu_(obj, widget)` — resolves the theme, gets/creates the button bar, idempotently clears any prior `OverflowMenuButton` + stale `OverflowContextMenu`, then guards on whether ANY of the five folded tags are present on the bar (non-fold widgets get no `...` button — `return` early otherwise). Creates one `uicontrol` (glyph `char(8230)`, theme tokens only) with `Callback @(~,~) obj.showOverflowMenu_(widget, bar)`, then self-anchors via a best-effort `reflowChrome_` call. +- `showOverflowMenu_(obj, widget, bar)` — entire body wrapped in try/catch → `warning('DashboardLayout:overflowMenuFailed', ...)`. Deletes any prior `OverflowContextMenu`, builds a FRESH `uicontextmenu`, and adds one `uimenu` item per PRESENT folded button: + - CrosshairLink → label/Checked reflect `widget.CrosshairLinked`; routes to `onCrosshairLinkTogglePressed_`. + - YLimit-Visible / YLimit-All → Checked reflects `widget.YLimitMode`; routes to `onYLimitButtonClicked_`. + - PlantLog → only added when the hidden button's `Enable` is `'on'` (store attached); label reflects `widget.ShowPlantLog`; routes to `onPlantLogTogglePressed_`. + - CreateEvent → routes to `invokeCreateEventCallback_`. + Posts via `set(cm,'Position',get(fig,'CurrentPoint')); set(cm,'Visible','on')` (the confirmed classic-figure mechanism). + +Wired into 4 call sites, each guarded with its own try/catch → `DashboardLayout:overflowMenuFailed` warning, each placed AFTER all five folded buttons exist and BEFORE the enclosing `reflowChrome_` call: +- `realizeWidget` (after the `addCrosshairLinkToggle` block, before the final `reflowChrome_`). +- `addCrosshairLinkToggle` (before its closing reflow). +- `addPlantLogToggle` (before its closing reflow). +- `addYLimitButtons_` (after `syncYLimitButtonsState_`, at method end). + +**Task 3 — Anchor in reflowChrome_.** Hoisted `xLink = xVisible - gap - bw` so it's computed unconditionally (previously only assigned inside the `CrosshairLinkButton`-present `if` block), reused by both the CrosshairLinkButton anchor and the new overflow anchor. Added `OverflowMenuButton` resolution + a `[xOverflow, 2, bw, bw]` position set, preferring `xLink` when CrosshairLinkButton is present, else `xVisible` (YLimitVisibleBtn), else `xAll` (YLimitAllBtn) — i.e. the leftmost folded slot. No other anchor logic (Detach/Create/Info/PlantLog/V/A/CrosshairLink) was changed. + +## Verification (executor — no MATLAB available) + +- `grep -c "'Visible', *'off'"` — baseline 0 (confirmed via `git show HEAD:libs/Dashboard/DashboardLayout.m` before this plan), now 4 — exactly the four folded creation sites. `addInfoIcon`/`addDetachButton` confirmed untouched (grep + manual inspection). +- `addOverflowMenu_` / `showOverflowMenu_` exist, wired into `realizeWidget` + the 3 rebuild methods; menu callbacks reference the four existing action methods (`onCrosshairLinkTogglePressed_`, `onYLimitButtonClicked_`, `onPlantLogTogglePressed_`, `invokeCreateEventCallback_` — 15 references total across the file); glyph confirmed as `char(8230)` (not a literal multibyte ellipsis). +- `reflowChrome_` resolves `OverflowMenuButton` and sets its position via `xOverflow`; `xLink` confirmed referenced by both the CrosshairLink anchor and the overflow fallback anchor (single hoisted assignment). +- `mh_style` and `mh_lint` (MISS_HIT, installed in this environment) report **"everything seems fine"** after each of the 3 task commits. +- No hardcoded hex/RGB colors introduced — all new uicontrol/uimenu styling uses `theme.ToolbarBackground` / `theme.ToolbarFontColor` only. +- `git diff --stat` across all 3 task commits confirms only `libs/Dashboard/DashboardLayout.m` changed (203 insertions, 3 deletions). + +## Test flag (per plan requirement) + +Grepped `tests/` for any assertion of a folded button's `Visible` property: **none found.** The single hit (`tests/test_create_event_dialog.m:8`) is an unrelated doc comment about a "hidden uipanel", not a `Visible` assertion on any of the five folded buttons. No test file was edited, per constraint. + +## Deferred to the human orchestrator (no MATLAB tooling available to the executor) + +- Live render of `example_dashboard_all_widgets` with `Theme='light'` — visual confirmation that the X/V/A/L/+ strip is gone, a single `...` sits left of Info/Detach, clicking `...` posts the menu, and each menu item performs its action correctly. +- The 7 must-pass suites: `tests/test_dashboard_widget_button_bar.m`, `tests/test_dashboard_layout_plant_log_toggle.m`, `tests/suite/TestDashboardDetach.m`, `tests/suite/TestDashboardWidget.m`, `tests/suite/TestInfoTooltip.m`, `tests/suite/TestDashboardLayoutPlantLogToggle.m`, `tests/suite/TestIndustrialPlantDemoCompanion.m`. + +## Deviations from Plan + +None — plan executed exactly as written. All three tasks implemented per spec; no Rule 1-4 deviations were needed. + +## Known Stubs + +None. The overflow menu is fully wired to real action methods (no placeholder/mock data paths). + +## Threat Flags + +None beyond what the plan's `` already covers (T-ikg-01/02/03 — all `mitigate`d via try/catch + idempotent cleanup, or explicitly `accept`ed as no new privilege). + +## Self-Check + +- [x] `libs/Dashboard/DashboardLayout.m` exists and contains all edits (confirmed via grep). +- [x] Commit `5345671a` (Task 1), `619ff9e3` (Task 2), `195637bc` (Task 3) all present in `git log`. +- [x] MISS_HIT clean after every task commit. + +## Self-Check: PASSED diff --git a/.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-VERIFICATION.md b/.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-VERIFICATION.md new file mode 100644 index 00000000..5fe01bae --- /dev/null +++ b/.planning/quick/260709-ikg-overflow-menu-for-widget-chrome-redline-/260709-ikg-VERIFICATION.md @@ -0,0 +1,69 @@ +--- +phase: 260709-ikg +verifier: human-orchestrator (live MATLAB via matlab MCP) +date: 2026-07-09 +status: passed +--- + +# Quick Task 260709-ikg — Verification + +Live verification of the widget-chrome overflow ("…") menu. Executor has no MATLAB +MCP tools (project convention); all runtime checks below run by the orchestrator. + +## must_have coverage + +| id | description | result | +|----|-------------|--------| +| D1 | X/V/A/L/+ folded (Visible='off', alive) behind a single "…" button | **PASS** | +| D2 | "…" click posts a uicontextmenu routing to the same actions | **PASS** | +| D3 | reflowChrome_ anchors "…"; Info + Detach stay visible | **PASS** | + +## Evidence + +**Live render** — `example_dashboard_all_widgets` Theme='light': +- On a FastSenseWidget bar: `OverflowMenuButton` present + `Visible='on'`, String = U+2026 ("…"). +- All 5 folded buttons present but `Visible='off'`: `CrosshairLinkButton`, + `YLimitVisibleBtn`, `YLimitAllBtn`, `PlantLogToggleButton`, `CreateEventButton`. +- `DetachButton` `Visible='on'`. Firing the "…" callback built + posted the + uicontextmenu with no error. +- Isolated toggle repro (plant-log on→off): bar + all buttons survive; "…" persists. + +**Suites (run live):** + +| suite | result | +|-------|--------| +| TestDashboardDetach | 11/11 | +| TestDashboardWidget | 13/13 | +| TestInfoTooltip | 13/13 | +| test_dashboard_widget_button_bar | 5/5 | +| test_dashboard_layout_plant_log_toggle (isolated) | 13/13 | +| TestDashboardLayoutPlantLogToggle (isolated) | 13/13 | + +**Static:** `mh_style` + `mh_lint` clean (executor); theme tokens only; no folded-button deletes. + +## Two failures — both cleared of this change (rigorous A/B) + +1. **`TestDashboardLayoutPlantLogToggle/testInfoIconSurvivesToggleOnOff`** — fails + only when run *after several other dashboard suites in one MATLAB session* + (bar deleted mid-test, "Invalid or deleted object"). Proven **environmental + test-ordering contamination, NOT a regression**: + - ikg, isolated: 13/13 PASS. + - Isolated toggle repro on ikg: bar + buttons survive. + - **Base (pre-ikg), same 5-suite sequence: the IDENTICAL failure reproduces** + (plus the flat variant) — so the flake predates this change. + Matches the known leftover-figure/session-churn flake pattern. + +2. **`TestIndustrialPlantDemoCompanion/testCOMPDEMO03_tagCatalogReflectsRegistry`** — + "no plant tag has an `area:*` Labels entry". A tag-registry precondition, + unrelated to widget chrome. **Fails identically on base** → pre-existing. + +(The trailing StatusWidget `relayout_` / content-resize errors seen during the +render pass are the known pre-existing StatusWidget resize fragility tickled by +`exportapp`'s internal resize — not in the ishandle-guarded code this task added.) + +## Verdict + +**PASSED.** Redline (e) implemented within the cheap-render budget: X/V/A/L/+ folded +behind a working "…" overflow menu, Info/Detach preserved, all handle-contract +suites green in isolation. Both observed failures are proven pre-existing / +environmental via A/B against base. diff --git a/docs/design/fastsense-light-1to1-design-spec.md b/docs/design/fastsense-light-1to1-design-spec.md new file mode 100644 index 00000000..0d7ebdb9 --- /dev/null +++ b/docs/design/fastsense-light-1to1-design-spec.md @@ -0,0 +1,261 @@ +# FastSense Light Mode — 1:1 Design Spec (Pencil → MATLAB) + +**Status:** draft · **Date:** 2026-07-09 · **Surfaces:** FastSense plot, Dashboard, Companion +**Pencil file:** `~/.pencil/documents/15bbe5fe-d0f5-470b-a8c7-1f54cba91b93/pencil-halo.pen` + +--- + +## 0. What "1:1" means here, and how to use this doc + +- **Direction of truth:** *Pencil drives MATLAB.* The Pencil frames are the visual target; + MATLAB is built (or corrected) to match them. +- **The catch that makes it honest:** the Pencil frames are drawn using **only primitives MATLAB + can render cheaply** (§1). Nothing in a frame requires a gradient, shadow, blur, or true rounded + panel corner. So "make MATLAB look like the mock" is achievable, not aspirational. +- **The token contract (§2) is the bridge.** Every color and size in the Pencil frames is a *named + variable* whose value equals a specific MATLAB theme field. Translating a frame = look up the + token → set the matching `FastSenseTheme` / `DashboardTheme` / `CompanionTheme` field → place the + primitive. No eyeballing of hex codes. +- **Redlines (§5)** list every place today's MATLAB still diverges from the target, with the fix. + +**Frame index** + +| Surface | Pencil frame name | Node id | Reference px | +|---|---|---|---| +| Single plot | `FastSense Light — Plot` | `UO6JB` | 920 × 580 | +| Dashboard | `FastSense Light — Dashboard` | `CK5l7` | 1440 × 980 | +| Companion | `FastSense Light — Companion` | `bmDPJ` | 1200 × 760 | +| (reference) As-is UI | `FastSense — Current UI (as-is)` | `ZAUY9` | 1600 × 1010 | +| (reference) Redesign study | `FastSense — Redesigned UI` | `EwZma` | 1600 × 1400 | + +--- + +## 1. Cheap-render budget (the hard constraint) + +Every element in every frame maps to one of these MATLAB primitives. If a mock ever needs anything +in the **Forbidden** column, that's a bug in the mock — not a feature to implement. + +| Allowed primitive | MATLAB realization | +|---|---| +| Flat fill rectangle | `uipanel.BackgroundColor` / `axes.Color` / `annotation('rectangle')` | +| 1 px hairline border | `uipanel` line border · `axes.Box='on'` + `XColor/YColor` · `rectangle EdgeColor` | +| Straight polyline / data line | `line()` / `plot()` with `Color`, `LineWidth` | +| Dashed line | `line(...,'LineStyle','--')` | +| Solid arc (gauge) | `line()` over many sampled angles (no true arc primitive needed) | +| Single-hue fill band | `patch` / `area` with `FaceAlpha` (this is the *only* transparency MATLAB does cheaply) | +| Text | `uicontrol('style','text')` · `uilabel` · `text()` | +| Small monochrome glyph | Unicode char in a text control, or a thin `line`/`patch` icon | + +| Forbidden (must not appear) | Why | +|---|---| +| Linear/radial gradients | No cheap axes/uipanel gradient; would need an image | +| Drop shadows / inner shadows | Not a MATLAB panel property | +| Background blur | Not available | +| Rounded panel corners | **`uipanel` corners are always square.** See caveat below. | +| Multi-layer translucent overlays | Compositing cost + no real support | + +> **cornerRadius caveat.** The Pencil frames use a small `cornerRadius` (≈4 px) on chips, tool +> buttons, and the legend purely so the *mock* reads cleanly at a glance. **MATLAB renders these +> square.** When translating, treat every `cornerRadius` as `0`. This is an accepted, documented +> divergence, not a target. + +--- + +## 2. Token contract (single source of truth) + +Pencil variable ⟷ hex ⟷ MATLAB RGB triplet (0–1) ⟷ theme field. The MATLAB triplets are copied +verbatim from `FastSenseTheme.m` / `DashboardTheme.m`, so they are exact, not reconverted from hex. + +### Canvas / surfaces / borders + +| Pencil var | Hex | MATLAB RGB | Theme field(s) | Used for | +|---|---|---|---|---| +| `fs-canvas` | `#F5F6F8` | `[0.961 0.965 0.973]` | `DashboardBackground`, `FastSenseTheme.Background` | figure / dashboard canvas | +| `fs-surface` | `#FFFFFF` | `[1 1 1]` | `WidgetBackground`, `AxesColor` | widget panels, axes fill | +| `fs-surface-alt` | `#F9FAFB` | `[0.976 0.980 0.984]` | `ToolbarBackground` | toolbar strips, tool buttons | +| `fs-border` | `#E5E8EC` | `[0.898 0.910 0.925]` | `WidgetBorderColor`, `GroupBorderColor` | 1 px card / panel borders | +| `fs-tab-inactive` | `#D9DEE6` | `[0.850 0.870 0.900]` | `TabInactiveBg` | inactive page tab bg | +| `fs-grid` | `#E9EDF2` | `[0.914 0.929 0.949]` | `GridColor`, `GridLineColor` | axes gridlines | + +### Accent + +| Pencil var | Hex | MATLAB RGB | Theme field(s) | Used for | +|---|---|---|---|---| +| `fs-accent` | `#2563EB` | `[0.145 0.388 0.922]` | `DragHandleColor`, `CompanionTheme.Accent` | primary CTA, active handle, links, active pill | +| `fs-accent-soft` | `#EAF1FE` | `[0.918 0.945 0.996]` | `DropZoneColor`, `TabActiveBg` | active tab bg, drop zone, soft accent fill | +| `fs-accent-2` | `#3B82F6` | *(none — derive)* | — | hover/secondary accent (use sparingly) | + +### Status (semantic) + +| Pencil var | Hex | MATLAB RGB | Theme field(s) | Used for | +|---|---|---|---|---| +| `fs-ok` | `#16A34A` | `[0.086 0.639 0.290]` | `StatusOkColor` | OK dots, positive KPI trend | +| `fs-warn` | `#F59E0B` | `[0.961 0.620 0.043]` | `StatusWarnColor` | warning state | +| `fs-alarm` | `#EF4444` | `[0.937 0.267 0.267]` | `StatusAlarmColor`, `FastSenseTheme.ThresholdColor` | alarm dots, threshold line | +| `fs-ok-soft` / `fs-warn-soft` / `fs-alarm-soft` | `#E7F6EC` / `#FEF3E2` / `#FDEBEC` | *(none — derive)* | — | status-pill backgrounds (only if pills land) | + +### Ink / text + +| Pencil var | Hex | MATLAB RGB | Theme field(s) | Used for | +|---|---|---|---|---| +| `fs-ink` | `#1E293B` | `[0.118 0.161 0.231]` | `GroupHeaderFg` | primary text on white | +| `fs-ink-2` | `#64748B` | `[0.392 0.455 0.545]` | `ToolbarFontColor`, `PlaceholderTextColor`, `AxisColor` | secondary text, tick labels | +| `fs-plot-ink` | `#354155` | `[0.208 0.255 0.333]` | `FastSenseTheme.ForegroundColor` | plot axis labels / ticks | + +### Plot line palette — `muted` (LOCKED) + +`FastSenseTheme.LineColorOrder = 'muted'`. These are the exact `getPalette('muted')` rows. +Reconciled in Pencil 2026-07-09 (previously the mock used brighter lines — that was the divergence). + +| Pencil var | Hex | MATLAB RGB | Series | +|---|---|---|---| +| `fs-line-1` | `#547AA3` | `[0.33 0.47 0.64]` | steel blue | +| `fs-line-2` | `#AD665C` | `[0.68 0.40 0.36]` | dusty rose | +| `fs-line-3` | `#8C8C5C` | `[0.55 0.55 0.36]` | olive | +| `fs-line-4` | `#9470A6` | `[0.58 0.44 0.65]` | mauve | +| `fs-line-5` | `#739E80` | `[0.45 0.62 0.50]` | sage | +| `fs-line-6` | `#A68C66` | `[0.65 0.55 0.40]` | tan | +| `fs-line-7` | `#668C99` | `[0.40 0.55 0.60]` | slate | +| `fs-line-8` | `#9E6B85` | `[0.62 0.42 0.52]` | plum | + +### Heatmap ramp (optional) + +`fs-heat-1..6` (`#EAF1FB → #123E73`) is a Pencil-only blue ramp for `HeatmapWidget` mocks. MATLAB +`HeatmapWidget` currently uses its own colormap; reconcile only if a heatmap is in scope (§5-g). + +--- + +## 3. Typography + +| Role | Pencil (mock) | MATLAB | Size | +|---|---|---|---| +| Font family | `fs-font` = **Inter** | `theme.FontName` = **Helvetica** | — | +| Dashboard header | — | `HeaderFontSize` | **14 pt** | +| Widget title | title header | `WidgetTitleFontSize` | **11 pt** | +| KPI value | big number | `KpiFontSize` | **28 pt** | +| Plot axis labels | 10 px | `FastSenseTheme.FontSize` | **10 pt** | +| Plot title | 12 px | `FastSenseTheme.TitleFontSize` | **12 pt** | + +> **Font divergence.** Inter is not guaranteed present in MATLAB. Translator rule: **map Inter → +> `Helvetica`** (the theme default) or `'SansSerif'`. Do not add an Inter dependency. Inter in the +> mock is a visual stand-in for "clean grotesque sans"; Helvetica satisfies that at render time. + +--- + +## 4. Surface specs + +### 4.1 FastSense single plot — frame `UO6JB` (920 × 580) + +``` +┌───────────────────────────────────────────────┐ ← figure, fill fs-canvas +│ Toolbar (h=40, fill fs-surface-alt, 1px btm) │ +│ title fs-ink 13·600 / subtitle fs-ink-2 10 │ ⟵ FastSenseToolbar +│ right: 6 tool buttons (26², fs-surface+border) │ +├───────────────────────────────────────────────┤ +│ pad 14 │ +│ ┌───────── AxesCard fill fs-surface, 1px ───┐ │ ⟵ axes: Color=white, Box=on +│ │ y-tick · 5 H gridlines / 6 V gridlines │ │ XColor/YColor = fs-ink-2 +│ │ labels · · lines fs-line-1, fs-line-2 │ │ GridColor=fs-grid, GridAlpha=0.7 +│ │ (fs-ink-2)· - - alarm threshold fs-alarm - │ │ threshold: line '--' fs-alarm +│ │ · [legend chip] │ │ legend(): box fs-border on fs-surface +│ │ ───────── x-tick labels (fs-ink-2) ─────── │ │ +│ └────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────┘ +``` + +| Mock element | MATLAB target | +|---|---| +| Toolbar strip | `FastSenseToolbar` — bg `ToolbarBackground`, bottom hairline `WidgetBorderColor` | +| Title / subtitle | `text`/label, `fs-ink` bold + `fs-ink-2` | +| 6 tool buttons | toolbar buttons; glyph `fs-ink-2` on `fs-surface`, 1 px `fs-border` | +| Axes card | `axes` `Color=[1 1 1]`, `Box='on'`, `XColor=YColor=fs-ink-2`, `LineWidth≈0.75` | +| Gridlines | `grid on`, `GridColor=fs-grid`, `GridAlpha=0.7`, `GridLineStyle='-'` | +| Data lines | `plot` cycling `LineColorOrder` (muted), `LineWidth=1.0–1.8` | +| Alarm threshold | `line` `Color=ThresholdColor`, `LineStyle='--'` + right-aligned label | +| Legend chip | `legend` `Color=fs-surface`, `EdgeColor=fs-border`, `TextColor=fs-ink` | + +Interior margins (at 920×580): y-label gutter **44 px**, x-label gutter **26 px**, top **10**, right **12**. +In MATLAB these are the axes `Position` insets — express as normalized (`≈[0.055 0.06 0.93 0.88]`). + +### 4.2 Dashboard — frame `CK5l7` (1440 × 980) + +**Mechanism (already in code):** +- `DashboardToolbar.Height = 0.04` (normalized) top strip, below optional `BannerHeight`. +- Optional page-tab bar (`PageBarHeight`) — active tab `TabActiveBg` / inactive `fs-tab-inactive`. +- Body = **24-column** grid (`DashboardLayout.Columns = 24`). +- Gutters & pad are theme-driven (wired in `DashboardEngine` before `allocatePanels`): + `WidgetGapH = 0.006`, `WidgetGapV = 0.010`, `DashboardPad = [0.008 0.010 0.008 0.010]` (L B R T, normalized). +- Every widget = white `uipanel`, 1 px `WidgetBorderColor` → reads as a separated card. + +| Mock widget (this frame) | MATLAB widget | Notes | +|---|---|---| +| KPI row (4 tiles: value + unit + trend) | `NumberWidget` | trend arrow `fs-ok`/`fs-alarm`; value `fs-ink`, unit/label `fs-ink-2` | +| Line chart "Reactor Temperature" | `FastSenseWidget` | muted lines, dashed alarm threshold (as §4.1) | +| Radial gauge "Overall Health 78" | `GaugeWidget` | arc `fs-ok`, track `fs-grid`, `GaugeArcWidth=8` | +| "Active Platforms" status list | `MultiStatusWidget` | dots `fs-ok`/`fs-warn`/`fs-alarm`; **monitor-kind only maps 0/1→alarm** | +| Bar chart "Data Logs / hr" | `BarChartWidget` | bars `fs-accent`; one alarm bar `fs-alarm` | +| Table "Sensor Events" | `TableWidget` | header `fs-ink`, rows `fs-ink-2`, status cells semantic | +| Widget title headers | `DashboardWidget.drawPanelTitle_` | bold `fs-ink`, `WidgetTitleFontSize` 11 | + +### 4.3 Companion — frame `bmDPJ` (1200 × 760) + +**Mechanism (already in code, `FastSenseCompanion.m`):** +`uifigure` → `uigridlayout` with `ColumnWidth = {220, '1x', 360}`, `RowHeight = {32, '1x', 360}`, +`ColumnSpacing = 16`, outer `Padding = 24` (`GridOuterPadding`). + +| Grid cell | Pane / element | MATLAB | +|---|---|---| +| Row 1 (h=32), full width | Top toolbar (brand, mode, time controls) | toolbar grid `ColumnWidth {110,110,110,130,70,90,70,70,'1x',36}`, spacing 8 | +| Col 1 (w=220) | Tag catalog: search + filter pills + list | `TagCatalogPane` | +| Col 2 (`1x`) | Dashboard list (rows + "Open") | `DashboardListPane`; active row `fs-accent-soft`, "Open" `fs-accent` | +| Col 3 (w=360) | Inspector: title, sparkline, stats, CTAs | `InspectorPane` | +| Row 3 (h=360) | Event/log strip | `CompanionEventViewer` region | + +| Mock element | MATLAB target | Token | +|---|---|---| +| Search field | `uieditfield`, `SearchFieldHeight = 28` | border `fs-border`, text `fs-ink` | +| Filter pill (active / idle) | pills, `FilterPillHeight = 24` | active bg `fs-accent`, idle `fs-surface-alt`/`fs-border` | +| Inspector sparkline | `FastSense`/axes mini-line | `fs-line-1` (muted), `CompanionTheme.LineColors{1}` | +| "In Plot" CTA | primary button | bg `fs-accent`, text `#FFFFFF` | +| "Add to Dashboard" | secondary button | `fs-surface` + `fs-border`, text `fs-ink` | +| Placeholder text | empty-pane labels | `PlaceholderTextColor` = `fs-ink-2` | + +--- + +## 5. Redlines — gap between today's MATLAB and this target + +What still has to change for the live app to match the frames. Ordered by visibility. + +| # | Gap | Target | Status / effort | +|---|---|---|---| +| a | Plot line palette (mock was brighter) | muted `fs-line-*` | **DONE** — Pencil reconciled to muted; no MATLAB change | +| b | Font family | Inter (mock) vs Helvetica (app) | **Accept** — map Inter→Helvetica; no dependency | +| c | Rounded corners on chips/cards/buttons | square (MATLAB) | **Accept** — treat `cornerRadius=0` | +| d | Dashboard toolbar chrome (brand + Live pill + grouped buttons) | match mock toolbar | **DONE** (260709-gp9) — custom-drawn axes+patch layer over hidden `uicontrol` shim in `DashboardToolbar`; green Live pill; classic-figure controls can't be styled, so this was the only cheap path | +| e | Per-plot `X V A L +` strip (shared widget chrome) | fold into an overflow menu | **DONE** (260709-ikg) — `X`/`V`/`A`/`L`/`+` hidden (alive) behind a single `…` `OverflowMenuButton` in `DashboardLayout`; click posts a `uicontextmenu` routing to the same actions; Info + Detach stay visible | +| f | Status pills (OK/WARN/ALARM) | pill w/ soft bg (`fs-*-soft`) | **Open** — only fits in tall status cells; add `fs-*-soft` fields | +| g | Heatmap colormap | `fs-heat-1..6` ramp | **Open, optional** — only if a heatmap widget is in scope | + +Items **a–c** are settled (accepted or done). **d** and **e** — the two that most changed the overall +look — are now implemented via the hide-not-delete compatibility-shim pattern (native controls stay +valid+hidden, a cheap custom layer is drawn over them). **f–g** remain optional polish. + +--- + +## 6. Translation checklist (per widget / surface) + +1. Confirm the widget's theme fields resolve to the §2 tokens (they do for `light`; verify after any theme edit). +2. Place primitives from the §1 budget only — no gradient/shadow/blur/rounded-panel. +3. Map every mock `cornerRadius` → 0; every `Inter` → `Helvetica`. +4. Colors: pull the MATLAB RGB triplet from §2, never hand-convert the hex. +5. Fonts/sizes: use the §3 point sizes (theme fields), not the mock's px. +6. Verify live: render in MATLAB (`example_dashboard_all_widgets` for the dashboard; a `FastSense` + plot for §4.1; `FastSenseCompanion` for §4.3), screenshot, compare region-by-region to the frame. +7. Log any *new* divergence back into §5 rather than silently deviating. + +--- + +## 7. Change log + +- **2026-07-09** — Initial 1:1 spec. Reconciled Pencil line palette to MATLAB `muted`; added + `fs-plot-ink`, `fs-tab-inactive`; added standalone `FastSense Light — Plot` frame (`UO6JB`). diff --git a/libs/Dashboard/BarChartWidget.m b/libs/Dashboard/BarChartWidget.m index 7d072bd3..a71c14c2 100644 --- a/libs/Dashboard/BarChartWidget.m +++ b/libs/Dashboard/BarChartWidget.m @@ -23,15 +23,12 @@ function render(obj, parentPanel) theme = obj.getTheme(); obj.hAxes = axes('Parent', parentPanel, ... 'Units', 'normalized', ... - 'Position', [0.12 0.15 0.82 0.75], ... + 'Position', [0.12 0.13 0.82 0.65], ... 'Color', theme.WidgetBackground, ... 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); - if ~isempty(obj.Title) - title(obj.hAxes, obj.Title, ... - 'Color', theme.ForegroundColor, ... - 'FontSize', theme.WidgetTitleFontSize); - end + % Phase 3 — title as a header band above the axes (no in-axes overlap). + obj.drawPanelTitle_(parentPanel, theme); obj.refresh(); end @@ -94,16 +91,13 @@ function refresh(obj) % Re-apply theme after plot commands. bar/barh run newplot, which % resets the axes Color (background) and XColor/YColor to their % light-mode defaults — leaving a glaring white box in dark mode. - % Restore the themed colors (and the title) every refresh. + % Restore the themed colors every refresh. The title lives in a + % sibling header band (drawPanelTitle_, immune to newplot), so no + % in-axes title re-apply is needed. theme = obj.getTheme(); set(obj.hAxes, 'Color', theme.WidgetBackground, ... 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); - if ~isempty(obj.Title) - title(obj.hAxes, obj.Title, ... - 'Color', theme.ForegroundColor, ... - 'FontSize', theme.WidgetTitleFontSize); - end end function t = getType(~) diff --git a/libs/Dashboard/DashboardEngine.m b/libs/Dashboard/DashboardEngine.m index f3e7370d..4213d3da 100644 --- a/libs/Dashboard/DashboardEngine.m +++ b/libs/Dashboard/DashboardEngine.m @@ -509,6 +509,25 @@ function render(obj) [effToolbarH, effPageBarH, effTimeH] = obj.applyChromeVisibility(toolbarH, pageBarH); obj.Layout.ContentArea = [0, effTimeH, ... 1, 1 - obj.BannerHeight - effToolbarH - effPageBarH - effTimeH]; + % Phase 1 UI refresh — grid breathing room. Pull inter-widget + % gutters + outer padding from the theme (isfield-guarded so older + % serialized themes still get the refreshed spacing rather than a + % flush grid). computePosition already consumes these knobs. + if isfield(themeStruct, 'WidgetGapH') + obj.Layout.GapH = themeStruct.WidgetGapH; + else + obj.Layout.GapH = 0.006; + end + if isfield(themeStruct, 'WidgetGapV') + obj.Layout.GapV = themeStruct.WidgetGapV; + else + obj.Layout.GapV = 0.010; + end + if isfield(themeStruct, 'DashboardPad') + obj.Layout.Padding = themeStruct.DashboardPad; + else + obj.Layout.Padding = [0.008 0.010 0.008 0.010]; + end obj.Layout.DetachCallback = @(w) obj.detachWidget(w); % 260513-snt — wire the per-FastSenseWidget '+Event' button. obj.Layout.CreateEventCallback = @(w) obj.openCreateEventDialog_(w); diff --git a/libs/Dashboard/DashboardLayout.m b/libs/Dashboard/DashboardLayout.m index 0dbb65d6..e74c2b4e 100644 --- a/libs/Dashboard/DashboardLayout.m +++ b/libs/Dashboard/DashboardLayout.m @@ -423,6 +423,15 @@ function realizeWidget(obj, widget) 'addCrosshairLinkToggle failed during realizeWidget: %s', ME.message); end end + % 260709-ikg — overflow '...' button. Runs AFTER all five + % folded buttons exist (crosshair is added last above) so the + % menu it builds can find every present folded control. + try + obj.addOverflowMenu_(widget); + catch ME + warning('DashboardLayout:overflowMenuFailed', ... + 'addOverflowMenu_ failed during realizeWidget: %s', ME.message); + end % v4.0 260513-snt — settle final right-anchored button positions. % addInfoIcon runs BEFORE addCreateEventButton, so Info's % initial X collides with Create's slot. reflowChrome_ knows @@ -435,6 +444,14 @@ function realizeWidget(obj, widget) widget.render(widget.hCellPanel); end + % Phase 2 UI refresh — de-clutter. Hide the floating MATLAB axes + % exploration toolbar on every axes this widget just rendered. The + % dashboard supplies its own controls (widget button bar + time + % navigator), so the redundant per-axes toolbar is pure visual + % noise. Central here so it covers all widget types and re-applies + % on scroll-realize. + DashboardLayout.hideAxesToolbars_(widget.hCellPanel); + widget.markRealized(); widget.Dirty = false; end @@ -713,6 +730,7 @@ function addPlantLogToggle(obj, widget, engine) 'Enable', enableState, ... 'Tag', 'PlantLogToggleButton', ... 'TooltipString', tipStr, ... + 'Visible', 'off', ... 'Callback', @(s, ~) obj.onPlantLogTogglePressed_(s, widget, engine)); % 260526-info-icon-vanishes-after-plantlog-toggle: % The xPL above is hardcoded for a 3-button right cluster @@ -725,6 +743,12 @@ function addPlantLogToggle(obj, widget, engine) % toggle cycle. The initial call from realizeWidget is rescued % by the reflowChrome_ at the end of realizeWidget; subsequent % callback rebuilds need their own reflow. + try + obj.addOverflowMenu_(widget); + catch ME + warning('DashboardLayout:overflowMenuFailed', ... + 'addOverflowMenu_ failed during addPlantLogToggle: %s', ME.message); + end try DashboardLayout.reflowChrome_(widget.hCellPanel, 28, 2); catch @@ -811,7 +835,14 @@ function addCrosshairLinkToggle(obj, widget) 'BackgroundColor', bgColor, ... 'Tag', 'CrosshairLinkButton', ... 'TooltipString', tipStr, ... + 'Visible', 'off', ... 'Callback', @(s, ~) obj.onCrosshairLinkTogglePressed_(s, widget)); + try + obj.addOverflowMenu_(widget); + catch ME + warning('DashboardLayout:overflowMenuFailed', ... + 'addOverflowMenu_ failed during addCrosshairLinkToggle: %s', ME.message); + end try DashboardLayout.reflowChrome_(widget.hCellPanel, 28, 2); catch @@ -1070,6 +1101,15 @@ function addYLimitButtons_(obj, widget) % Highlight the button matching the current YLimitMode. DashboardLayout.syncYLimitButtonsState_(bar, widget.YLimitMode); + + % 260709-ikg — overflow '...' button. Rebuilt here too since + % this method is the callback-driven rebuild path for V/A. + try + obj.addOverflowMenu_(widget); + catch ME + warning('DashboardLayout:overflowMenuFailed', ... + 'addOverflowMenu_ failed during addYLimitButtons_: %s', ME.message); + end end function addYLimitButton_(obj, bar, widget, mode, x, glyph, tip, theme, tagName) @@ -1087,6 +1127,7 @@ function addYLimitButton_(obj, bar, widget, mode, x, glyph, tip, theme, tagName) 'BackgroundColor', theme.ToolbarBackground, ... 'Tag', tagName, ... 'TooltipString', tip, ... + 'Visible', 'off', ... 'Callback', @(~,~) obj.onYLimitButtonClicked_(widget, mode, bar)); end @@ -1131,6 +1172,7 @@ function addCreateEventButton(obj, widget) 'BackgroundColor', theme.ToolbarBackground, ... 'Tag', 'CreateEventButton', ... 'TooltipString', 'Create event from selection / current view', ... + 'Visible', 'off', ... 'Callback', @(~,~) obj.invokeCreateEventCallback_(widget)); end @@ -1148,6 +1190,154 @@ function invokeCreateEventCallback_(obj, widget) 'Create-Event callback failed: %s', ME.message); end end + + function addOverflowMenu_(obj, widget) + %ADDOVERFLOWMENU_ Add the overflow '...' button that folds the five + % secondary chrome buttons (X/V/A/L/+) behind a single context + % menu (redline (e), 260709-ikg). Idempotent: any prior + % OverflowMenuButton + stale OverflowContextMenu are deleted + % before the fresh button is created. No-op (returns without + % creating a button) when none of the five folded buttons are + % present on this bar — non-fold widgets get no ... button. + if isempty(widget.ParentTheme) || ~isstruct(widget.ParentTheme) + theme = DashboardTheme('light'); + else + theme = widget.ParentTheme; + end + bar = obj.getOrCreateButtonBar_(widget); + % Idempotent: clear any prior OverflowMenuButton on this bar. + prior = findobj(bar, 'Tag', 'OverflowMenuButton', '-depth', 1); + if ~isempty(prior) + try delete(prior); catch, end + end + % Idempotent: clear any stale OverflowContextMenu on the figure + % so context menus never accumulate across rebuilds. + fig = ancestor(bar, 'figure'); + if ~isempty(fig) && ishandle(fig) + priorMenu = findobj(fig, 'Tag', 'OverflowContextMenu'); + if ~isempty(priorMenu) + try delete(priorMenu); catch, end + end + end + % GUARD: only fold-eligible widgets (>=1 of the five folded + % buttons present on the bar) get an overflow button. + foldedTags = {'CrosshairLinkButton', 'YLimitVisibleBtn', ... + 'YLimitAllBtn', 'PlantLogToggleButton', 'CreateEventButton'}; + hasAnyFolded = false; + for i = 1:numel(foldedTags) + if ~isempty(findobj(bar, 'Tag', foldedTags{i}, '-depth', 1)) + hasAnyFolded = true; + break; + end + end + if ~hasAnyFolded + return; + end + uicontrol('Parent', bar, ... + 'Style', 'pushbutton', ... + 'String', char(8230), ... + 'Units', 'pixels', ... + 'Position', [2 2 24 24], ... + 'FontSize', 11, ... + 'FontWeight', 'bold', ... + 'ForegroundColor', theme.ToolbarFontColor, ... + 'BackgroundColor', theme.ToolbarBackground, ... + 'Tag', 'OverflowMenuButton', ... + 'TooltipString', 'More chart controls', ... + 'Callback', @(~,~) obj.showOverflowMenu_(widget, bar)); + try + DashboardLayout.reflowChrome_(widget.hCellPanel, 28, 2); + catch + % Best-effort self-anchor — must not break button creation. + end + end + + function showOverflowMenu_(obj, widget, bar) + %SHOWOVERFLOWMENU_ Build + post a FRESH uicontextmenu whose items + % route to the SAME action methods the folded buttons drove + % (260709-ikg). Rebuilt from scratch on every click so + % Checked/label state always reflects the current widget state. + % Entire body wrapped in try/catch: a menu-post failure must never + % crash the dashboard refresh loop (mirrors the addPlantLogToggle / + % addCrosshairLinkToggle guard pattern). + try + if isempty(bar) || ~ishandle(bar) + return; + end + fig = ancestor(bar, 'figure'); + if isempty(fig) || ~ishandle(fig) + return; + end + % Delete any prior context menu before building a fresh one. + prior = findobj(fig, 'Tag', 'OverflowContextMenu'); + if ~isempty(prior) + try delete(prior); catch, end + end + cm = uicontextmenu('Parent', fig, 'Tag', 'OverflowContextMenu'); + + hLink = findobj(bar, 'Tag', 'CrosshairLinkButton', '-depth', 1); + if ~isempty(hLink) && ishandle(hLink(1)) + if widget.CrosshairLinked + linkLabel = 'Unlink crosshair (stop mirroring)'; + linkChecked = 'on'; + else + linkLabel = 'Link crosshair across page'; + linkChecked = 'off'; + end + uimenu(cm, 'Label', linkLabel, 'Checked', linkChecked, ... + 'Callback', @(~,~) obj.onCrosshairLinkTogglePressed_(hLink(1), widget)); + end + + visBtn = findobj(bar, 'Tag', 'YLimitVisibleBtn', '-depth', 1); + if ~isempty(visBtn) && ishandle(visBtn(1)) + visChecked = 'off'; + if strcmp(widget.YLimitMode, 'auto-visible') + visChecked = 'on'; + end + uimenu(cm, 'Label', 'Auto-fit Y to visible X range', ... + 'Checked', visChecked, ... + 'Callback', @(~,~) obj.onYLimitButtonClicked_(widget, 'auto-visible', bar)); + end + + allBtn = findobj(bar, 'Tag', 'YLimitAllBtn', '-depth', 1); + if ~isempty(allBtn) && ishandle(allBtn(1)) + allChecked = 'off'; + if strcmp(widget.YLimitMode, 'auto-all') + allChecked = 'on'; + end + uimenu(cm, 'Label', 'Auto-fit Y to all data', ... + 'Checked', allChecked, ... + 'Callback', @(~,~) obj.onYLimitButtonClicked_(widget, 'auto-all', bar)); + end + + hPlant = findobj(bar, 'Tag', 'PlantLogToggleButton', '-depth', 1); + if ~isempty(hPlant) && ishandle(hPlant(1)) && ... + strcmp(get(hPlant(1), 'Enable'), 'on') + if isa(widget, 'FastSenseWidget') && widget.ShowPlantLog + plLabel = 'Hide plant log lines'; + else + plLabel = 'Show plant log lines'; + end + uimenu(cm, 'Label', plLabel, ... + 'Callback', @(~,~) obj.onPlantLogTogglePressed_(hPlant(1), widget, obj.EngineRef)); + end + + createBtn = findobj(bar, 'Tag', 'CreateEventButton', '-depth', 1); + if ~isempty(createBtn) && ishandle(createBtn(1)) + uimenu(cm, 'Label', 'Create event from selection / current view', ... + 'Callback', @(~,~) obj.invokeCreateEventCallback_(widget)); + end + + % Post it at the current mouse position (classic-figure + % context-menu mechanism — Position/Visible are settable). + cp = get(fig, 'CurrentPoint'); + set(cm, 'Position', cp); + set(cm, 'Visible', 'on'); + catch ME + warning('DashboardLayout:overflowMenuFailed', ... + 'Overflow menu failed: %s', ME.message); + end + end end methods (Static) @@ -1234,13 +1424,31 @@ function reflowChrome_(hCell, barH, inset) % 260602-mri — CrosshairLinkButton: LEFTMOST chrome button, % sits to the LEFT of the V/A cluster. % Assumption: FastSenseWidget always has V/A, so xVisible is - % always set when CrosshairLinkButton is present. Add a brief - % comment noting the leftmost-button assumption. + % always set when CrosshairLinkButton is present. xLink is + % hoisted (computed unconditionally) so 260709-ikg's overflow + % anchor below can reuse it without recomputing. + xLink = xVisible - gap - bw; linkBtn = findobj(bar(1), 'Tag', 'CrosshairLinkButton', '-depth', 1); if ~isempty(linkBtn) && ishandle(linkBtn(1)) - xLink = xVisible - gap - bw; set(linkBtn(1), 'Position', [xLink, 2, bw, bw]); end + % 260709-ikg — OverflowMenuButton ('...') reuses the + % leftmost folded slot per redline (e): prefer the + % CrosshairLinkButton slot when present, else the + % YLimitVisibleBtn slot, else the YLimitAllBtn slot. The + % invisible folded buttons keep their computed positions + % (they're hidden, so no visual conflict with '...'). + overflow = findobj(bar(1), 'Tag', 'OverflowMenuButton', '-depth', 1); + if ~isempty(overflow) && ishandle(overflow(1)) + if ~isempty(linkBtn) && ishandle(linkBtn(1)) + xOverflow = xLink; + elseif ~isempty(visibleBtn) && ishandle(visibleBtn(1)) + xOverflow = xVisible; + else + xOverflow = xAll; + end + set(overflow(1), 'Position', [xOverflow, 2, bw, bw]); + end end if ~isempty(content) && ishandle(content(1)) contentH = max(1, pp(4) - barH - inset); @@ -1348,5 +1556,23 @@ function anchorTopRight(btn, offsetFromRight) set(btn, 'Position', [x y btnW btnH]); end + function hideAxesToolbars_(root) + %HIDEAXESTOOLBARS_ Hide the MATLAB exploration toolbar on all axes under root. + % Walks every axes below the given handle and sets its Toolbar + % Visible = 'off'. Wrapped per-axes in try/catch so an axes without + % a Toolbar (or a deleted handle) never aborts the realize pass. + if isempty(root) || ~ishandle(root) + return; + end + axs = findall(root, 'Type', 'axes'); + for i = 1:numel(axs) + try + axs(i).Toolbar.Visible = 'off'; + catch + % axes has no toolbar / handle went stale — ignore + end + end + end + end end diff --git a/libs/Dashboard/DashboardTheme.m b/libs/Dashboard/DashboardTheme.m index 5d94b6b9..39f36823 100644 --- a/libs/Dashboard/DashboardTheme.m +++ b/libs/Dashboard/DashboardTheme.m @@ -69,22 +69,33 @@ d.TabInactiveBg = [0.10 0.12 0.18]; d.MarkerPlantLog = [0 0 0]; % Phase 1031 PLOG-VIZ-09: black plant-log slider markers d.CurrentViewBoxColor = [0.95 0.62 0.20]; % Phase 1039: amber current-view box, contrasts with bluish-gray Selection + % Status colors are set per-preset (guarded shared block below) so + % the light preset can carry its own crisp trio without disturbing dark. + d.StatusOkColor = [0.31 0.80 0.64]; + d.StatusWarnColor = [0.91 0.63 0.27]; + d.StatusAlarmColor = [0.91 0.27 0.38]; otherwise % 'light' (also: legacy aliases default/industrial/scientific/ocean) - d.DashboardBackground = [0.96 0.96 0.97]; + % Clean-modern light preset: white widget surfaces on a soft neutral + % canvas, hairline cool borders, blue #2563EB drag/drop accent. + d.DashboardBackground = [0.961 0.965 0.973]; d.WidgetBackground = [1.00 1.00 1.00]; - d.WidgetBorderColor = [0.85 0.85 0.87]; - d.ToolbarBackground = [0.94 0.94 0.95]; - d.ToolbarFontColor = [0.20 0.20 0.25]; - d.DragHandleColor = [0.20 0.60 0.86]; - d.DropZoneColor = [0.85 0.85 0.87]; - d.GridLineColor = [0.82 0.82 0.85]; - d.GroupHeaderBg = [0.90 0.92 0.95]; - d.GroupHeaderFg = [0.15 0.15 0.15]; - d.GroupBorderColor = [0.80 0.82 0.85]; - d.TabActiveBg = [0.90 0.92 0.95]; - d.TabInactiveBg = [0.82 0.84 0.88]; + d.WidgetBorderColor = [0.898 0.910 0.925]; + d.ToolbarBackground = [0.976 0.980 0.984]; + d.ToolbarFontColor = [0.392 0.455 0.545]; + d.DragHandleColor = [0.145 0.388 0.922]; + d.DropZoneColor = [0.918 0.945 0.996]; + d.GridLineColor = [0.914 0.929 0.949]; + d.GroupHeaderBg = [0.933 0.949 0.969]; + d.GroupHeaderFg = [0.118 0.161 0.231]; + d.GroupBorderColor = [0.898 0.910 0.925]; + d.TabActiveBg = [0.918 0.945 0.996]; + d.TabInactiveBg = [0.850 0.870 0.900]; % darker gray so the accent-soft active tab clearly stands out (>=0.05 luminance delta) d.MarkerPlantLog = [0 0 0]; % Phase 1031 PLOG-VIZ-09: black plant-log slider markers d.CurrentViewBoxColor = [0.85 0.45 0.05]; % Phase 1039: dark amber, contrasts with the dark-blue Selection on light bg + % Crisp semantic status trio, tuned for readability on white. + d.StatusOkColor = [0.086 0.639 0.290]; + d.StatusWarnColor = [0.961 0.620 0.043]; + d.StatusAlarmColor = [0.937 0.267 0.267]; end % Axis label/tick color — derive from toolbar font (readable on widget bg) @@ -94,11 +105,27 @@ % Shared defaults across all presets d.WidgetBorderWidth = 1; + % Grid breathing room (Phase 1 UI refresh): inter-widget gutters + outer + % canvas padding, in normalized units. DashboardLayout.computePosition + % consumes GapH/GapV/Padding; DashboardEngine wires these in before + % allocatePanels so widgets read as separated cards. DashboardPad is + % [left bottom right top]. + d.WidgetGapH = 0.006; + d.WidgetGapV = 0.010; + d.DashboardPad = [0.008 0.010 0.008 0.010]; d.HeaderFontSize = 14; d.WidgetTitleFontSize = 11; - d.StatusOkColor = [0.31 0.80 0.64]; - d.StatusWarnColor = [0.91 0.63 0.27]; - d.StatusAlarmColor = [0.91 0.27 0.38]; + % Status colors are now set per-preset above; guard so this shared block + % only supplies a fallback and never clobbers a preset's own values. + if ~isfield(d, 'StatusOkColor') + d.StatusOkColor = [0.31 0.80 0.64]; + end + if ~isfield(d, 'StatusWarnColor') + d.StatusWarnColor = [0.91 0.63 0.27]; + end + if ~isfield(d, 'StatusAlarmColor') + d.StatusAlarmColor = [0.91 0.27 0.38]; + end d.InfoColor = [0.27 0.52 0.85]; d.GaugeArcWidth = 8; d.KpiFontSize = 28; diff --git a/libs/Dashboard/DashboardToolbar.m b/libs/Dashboard/DashboardToolbar.m index ea450285..7ebc9323 100644 --- a/libs/Dashboard/DashboardToolbar.m +++ b/libs/Dashboard/DashboardToolbar.m @@ -29,6 +29,17 @@ hInfoBtn = [] Engine = [] Theme_ = [] + + % Custom-drawn compatibility-shim visual layer (light-mode redesign). + % Native controls above stay valid but hidden; this axes+patch + % layer draws the brand, flat action buttons/toggles, and the + % last-update label, routing every click back to the existing + % handlers so behavior + the handle contract survive unchanged. + hAxes_ = [] % custom-drawn axes, Tag 'DashboardToolbarAxes' + hBrandSquare_ = [] % brand patch (left) + hBrandText_ = [] % bold dashboard name text (left) + hLastUpdateText_ = [] % custom right-aligned last-update text + CustomBtns_ = struct() % .(key).Patch / .(key).Label per button end methods @@ -199,20 +210,40 @@ theme.ToolbarBackground * 0.4, ... 'BackgroundColor', theme.ToolbarBackground, ... 'HorizontalAlignment', 'right'); + + % --- Compatibility shim: hide the native chrome above and + % paint the custom light-mode toolbar layer over it. All + % handles created above remain valid (never deleted) so + % DashboardEngine.applyTheme, DashboardConfigDialog, and the + % existing test suites keep working unchanged. + obj.hideNativeControls_(); + obj.buildCustomLayer_(); + + % Prime the custom pills to match the hidden controls' initial + % Values (Events starts active by default). + obj.setLiveActiveIndicator(logical(get(obj.hLiveBtn, 'Value'))); + obj.setFollowActiveIndicator(logical(get(obj.hFollowBtn, 'Value'))); + obj.setEventsActiveIndicator(logical(get(obj.hEventsBtn, 'Value'))); end function setLastUpdateTime(obj, t) %SETLASTUPDATETIME Update the last-update label with a timestamp. % Hot-path note: called on every live tick. Uses datevec (no format % string parsing) instead of datestr to avoid timefun/private overhead. + % Dual-writes both the hidden hLastUpdate and the custom + % hLastUpdateText_ so the visible custom layer stays in sync. + try + dv = datevec(t); + timeStr = sprintf('%02d:%02d:%02d', dv(4), dv(5), floor(dv(6))); + catch + timeStr = datestr(t, 'HH:MM:SS'); + end + fullStr = ['Last update: ' timeStr]; if ~isempty(obj.hLastUpdate) && ishandle(obj.hLastUpdate) - try - dv = datevec(t); - timeStr = sprintf('%02d:%02d:%02d', dv(4), dv(5), floor(dv(6))); - catch - timeStr = datestr(t, 'HH:MM:SS'); - end - set(obj.hLastUpdate, 'String', ['Last update: ' timeStr]); + set(obj.hLastUpdate, 'String', fullStr); + end + if ~isempty(obj.hLastUpdateText_) && ishandle(obj.hLastUpdateText_) + set(obj.hLastUpdateText_, 'String', fullStr); end end @@ -233,15 +264,16 @@ function onLiveToggle(obj, src) end function setLiveActiveIndicator(obj, isActive) - %SETLIVEACTIVEINDICATOR Show a blue surround when live mode is active. - if isempty(obj.hLivePanel) || ~ishandle(obj.hLivePanel) - return; - end - if isActive - set(obj.hLivePanel, 'HighlightColor', obj.Theme_.InfoColor); - else - set(obj.hLivePanel, 'HighlightColor', obj.Theme_.ToolbarBackground); + %SETLIVEACTIVEINDICATOR Show a blue surround when live mode is active, + % and repaint the custom Live pill (StatusOkColor when active). + if ~isempty(obj.hLivePanel) && ishandle(obj.hLivePanel) + if isActive + set(obj.hLivePanel, 'HighlightColor', obj.Theme_.InfoColor); + else + set(obj.hLivePanel, 'HighlightColor', obj.Theme_.ToolbarBackground); + end end + obj.paintToggleActive_('live', isActive, obj.Theme_.StatusOkColor); end function onFollowToggle(obj, src) @@ -281,15 +313,16 @@ function onFollowToggle(obj, src) end function setFollowActiveIndicator(obj, isActive) - %SETFOLLOWACTIVEINDICATOR Show a blue surround when Follow is active. - if isempty(obj.hFollowPanel) || ~ishandle(obj.hFollowPanel) - return; - end - if isActive - set(obj.hFollowPanel, 'HighlightColor', obj.Theme_.InfoColor); - else - set(obj.hFollowPanel, 'HighlightColor', obj.Theme_.ToolbarBackground); + %SETFOLLOWACTIVEINDICATOR Show a blue surround when Follow is active, + % and repaint the custom Follow pill (InfoColor when active). + if ~isempty(obj.hFollowPanel) && ishandle(obj.hFollowPanel) + if isActive + set(obj.hFollowPanel, 'HighlightColor', obj.Theme_.InfoColor); + else + set(obj.hFollowPanel, 'HighlightColor', obj.Theme_.ToolbarBackground); + end end + obj.paintToggleActive_('follow', isActive, obj.Theme_.InfoColor); end function applyFollowToWidgets_(obj, widgets, mode, snap) @@ -350,14 +383,14 @@ function setEventsActiveIndicator(obj, isActive) % Matches the Live button's visual treatment so the toolbar % reads consistently. Keeps the button label constant — the % border colour is the active indicator; the tooltip explains - % the function. - if isempty(obj.hEventsPanel) || ~ishandle(obj.hEventsPanel) - return; - end - if isActive - set(obj.hEventsPanel, 'HighlightColor', obj.Theme_.InfoColor); - else - set(obj.hEventsPanel, 'HighlightColor', obj.Theme_.ToolbarBackground); + % the function. Also repaints the custom Events pill (InfoColor + % when active). + if ~isempty(obj.hEventsPanel) && ishandle(obj.hEventsPanel) + if isActive + set(obj.hEventsPanel, 'HighlightColor', obj.Theme_.InfoColor); + else + set(obj.hEventsPanel, 'HighlightColor', obj.Theme_.ToolbarBackground); + end end if ~isempty(obj.hEventsBtn) && ishandle(obj.hEventsBtn) if isActive @@ -366,6 +399,7 @@ function setEventsActiveIndicator(obj, isActive) set(obj.hEventsBtn, 'String', 'Events', 'Value', 0); end end + obj.paintToggleActive_('events', isActive, obj.Theme_.InfoColor); end function onConfig(obj) @@ -466,4 +500,208 @@ function onInfo(obj) 1, 1 - obj.Engine.BannerHeight - obj.Height - timePanelH]; end end + + methods (Access = private) + function hideNativeControls_(obj) + %HIDENATIVECONTROLS_ Hide (never delete) every native uicontrol/uipanel. + % All 14 handles remain valid — DashboardEngine.applyTheme, + % DashboardConfigDialog, and existing tests read .Value / + % .HighlightColor / .TooltipString / .String / .Position / + % .BackgroundColor on them directly. + nativeHandles = {obj.hTitleText, obj.hLastUpdate, obj.hInfoBtn, ... + obj.hExportBtn, obj.hImageBtn, obj.hConfigBtn, obj.hSyncBtn, ... + obj.hResetBtn, obj.hLiveBtn, obj.hLivePanel, obj.hFollowBtn, ... + obj.hFollowPanel, obj.hEventsBtn, obj.hEventsPanel}; + for k = 1:numel(nativeHandles) + h = nativeHandles{k}; + if ~isempty(h) && ishandle(h) + set(h, 'Visible', 'off'); + end + end + end + + function buildCustomLayer_(obj) + %BUILDCUSTOMLAYER_ Paint the custom light-mode toolbar visual layer. + % Cheap-render only: flat fill rectangles, 1px hairline borders, + % text. No gradients/shadows/blur/rounded corners. All colors + % and font sizes come from obj.Theme_. + theme = obj.Theme_; + + obj.hAxes_ = axes('Parent', obj.hPanel, ... + 'Tag', 'DashboardToolbarAxes', ... + 'Units', 'normalized', ... + 'Position', [0 0 1 1], ... + 'XLim', [0 1], ... + 'YLim', [0 1], ... + 'XTick', [], ... + 'YTick', [], ... + 'Box', 'off', ... + 'Color', theme.ToolbarBackground, ... + 'YDir', 'normal', ... + 'HitTest', 'on', ... + 'PickableParts', 'visible', ... + 'Visible', 'off'); + hold(obj.hAxes_, 'on'); + + % Brand: small filled square + bold dashboard name (left). + brandX = 0.01; brandSqW = 0.012; brandSqH = 0.6; brandY = 0.2; + obj.hBrandSquare_ = patch(obj.hAxes_, ... + [brandX, brandX + brandSqW, brandX + brandSqW, brandX], ... + [brandY, brandY, brandY + brandSqH, brandY + brandSqH], ... + theme.DragHandleColor, ... + 'EdgeColor', 'none', ... + 'HitTest', 'off'); + obj.hBrandText_ = text(obj.hAxes_, brandX + brandSqW + 0.01, 0.5, ... + obj.Engine.Name, ... + 'Color', theme.GroupHeaderFg, ... + 'FontSize', theme.HeaderFontSize, ... + 'FontWeight', 'bold', ... + 'HorizontalAlignment', 'left', ... + 'VerticalAlignment', 'middle', ... + 'HitTest', 'on', ... + 'PickableParts', 'visible', ... + 'ButtonDownFcn', @(src, evt) obj.onNameClick_()); + + % Right-aligned group of flat action buttons + toggles, drawn + % left-to-right: Info, Sync, Redraw, Config, Image, Export, + % Live, Follow, Events — then the last-update text right- + % aligned at the far right edge. + btnW = 0.06; + step = btnW + 0.005; + x = 0.29; + obj.addCustomButton_('info', 'Info', x, btnW, @() obj.onInfo()); + x = x + step; + obj.addCustomButton_('sync', 'Sync', x, btnW, ... + @() obj.Engine.resetGlobalTime()); + x = x + step; + obj.addCustomButton_('redraw', 'Redraw', x, btnW, @() obj.onReset()); + x = x + step; + obj.addCustomButton_('config', 'Config', x, btnW, @() obj.onConfig()); + x = x + step; + obj.addCustomButton_('image', 'Image', x, btnW, @() obj.onImage()); + x = x + step; + obj.addCustomButton_('export', 'Export', x, btnW, @() obj.onExport()); + x = x + step; + obj.addCustomButton_('live', 'Live', x, btnW, ... + @() obj.customLiveClick_()); + x = x + step; + obj.addCustomButton_('follow', 'Follow', x, btnW, ... + @() obj.customFollowClick_()); + x = x + step; + obj.addCustomButton_('events', 'Events', x, btnW, ... + @() obj.customEventsClick_()); + + % Last-update text: right-aligned at the far right, lightened + % font color (scalar blend toward ToolbarBackground — same + % pattern as the hidden hLastUpdate label). + fadedFg = theme.ToolbarFontColor * 0.6 + theme.ToolbarBackground * 0.4; + obj.hLastUpdateText_ = text(obj.hAxes_, 0.99, 0.5, 'Last update: —', ... + 'Color', fadedFg, ... + 'FontSize', theme.HeaderFontSize - 3, ... + 'HorizontalAlignment', 'right', ... + 'VerticalAlignment', 'middle', ... + 'HitTest', 'off'); + + hold(obj.hAxes_, 'off'); + end + + function addCustomButton_(obj, key, label, xLeft, width, onClickFcn) + %ADDCUSTOMBUTTON_ Draw one flat bordered "button" (patch + label). + % Both the patch and the label route clicks to onClickFcn — the + % SAME existing handler the equivalent native control calls. + theme = obj.Theme_; + btnY = 0.15; + btnH = 0.7; + hPatch = patch(obj.hAxes_, ... + [xLeft, xLeft + width, xLeft + width, xLeft], ... + [btnY, btnY, btnY + btnH, btnY + btnH], ... + theme.WidgetBackground, ... + 'EdgeColor', theme.WidgetBorderColor, ... + 'LineWidth', 1, ... + 'HitTest', 'on', ... + 'PickableParts', 'visible', ... + 'ButtonDownFcn', @(src, evt) onClickFcn()); + hLabel = text(obj.hAxes_, xLeft + width / 2, btnY + btnH / 2, label, ... + 'Color', theme.ToolbarFontColor, ... + 'FontSize', theme.HeaderFontSize - 3, ... + 'HorizontalAlignment', 'center', ... + 'VerticalAlignment', 'middle', ... + 'HitTest', 'on', ... + 'PickableParts', 'visible', ... + 'ButtonDownFcn', @(src, evt) onClickFcn()); + obj.CustomBtns_.(key) = struct('Patch', hPatch, 'Label', hLabel); + end + + function paintToggleActive_(obj, key, isActive, activeColor) + %PAINTTOGGLEACTIVE_ Repaint a custom toggle pill for active/inactive. + % Active: patch fills with activeColor, label turns white (the + % one explicit exception to theme-only colors — the semantic + % "on-fill foreground", not a themed surface color). + % Inactive: patch/label revert to WidgetBackground/ToolbarFontColor. + if ~isfield(obj.CustomBtns_, key) + return; + end + btn = obj.CustomBtns_.(key); + if isempty(btn.Patch) || ~ishandle(btn.Patch) + return; + end + if isActive + set(btn.Patch, 'FaceColor', activeColor); + set(btn.Label, 'Color', [1 1 1]); + else + set(btn.Patch, 'FaceColor', obj.Theme_.WidgetBackground); + set(btn.Label, 'Color', obj.Theme_.ToolbarFontColor); + end + end + + function onNameClick_(obj) + %ONNAMECLICK_ Prompt a rename via inputdlg, applying it exactly like + % the existing onNameEdit path (Engine.Name + figure Name), then + % updating both the custom brand text and the hidden hTitleText. + answer = inputdlg('Dashboard name:', 'Rename Dashboard', 1, ... + {obj.Engine.Name}); + if isempty(answer) || isempty(answer{1}) + return; + end + newName = answer{1}; + obj.Engine.Name = newName; + set(obj.Engine.hFigure, 'Name', newName); + if ~isempty(obj.hBrandText_) && ishandle(obj.hBrandText_) + set(obj.hBrandText_, 'String', newName); + end + if ~isempty(obj.hTitleText) && ishandle(obj.hTitleText) + set(obj.hTitleText, 'String', newName); + end + end + + function customLiveClick_(obj) + %CUSTOMLIVECLICK_ Flip the hidden Live Value, then call onLiveToggle + % with the hidden handle — keeps Engine.startLive/stopLive in sync. + if isempty(obj.hLiveBtn) || ~ishandle(obj.hLiveBtn) + return; + end + set(obj.hLiveBtn, 'Value', ~get(obj.hLiveBtn, 'Value')); + obj.onLiveToggle(obj.hLiveBtn); + end + + function customFollowClick_(obj) + %CUSTOMFOLLOWCLICK_ Flip the hidden Follow Value, then call + % onFollowToggle with the hidden handle. + if isempty(obj.hFollowBtn) || ~ishandle(obj.hFollowBtn) + return; + end + set(obj.hFollowBtn, 'Value', ~get(obj.hFollowBtn, 'Value')); + obj.onFollowToggle(obj.hFollowBtn); + end + + function customEventsClick_(obj) + %CUSTOMEVENTSCLICK_ Flip the hidden Events Value, then call + % onEventsToggle with the hidden handle. + if isempty(obj.hEventsBtn) || ~ishandle(obj.hEventsBtn) + return; + end + set(obj.hEventsBtn, 'Value', ~get(obj.hEventsBtn, 'Value')); + obj.onEventsToggle(obj.hEventsBtn); + end + end end diff --git a/libs/Dashboard/DashboardWidget.m b/libs/Dashboard/DashboardWidget.m index d80f34f4..d398b81b 100644 --- a/libs/Dashboard/DashboardWidget.m +++ b/libs/Dashboard/DashboardWidget.m @@ -246,6 +246,32 @@ function setTimeRange(~, ~, ~) end end end + + function drawPanelTitle_(obj, parentPanel, theme) + %DRAWPANELTITLE_ Render obj.Title as a header band at the top of a widget. + % Phase 3 UI refresh — a consistent title header above the plot area so + % titles never overlap the axes (replaces the floating in-axes title()). + % No-op when Title is empty. Callers should inset their axes so its top + % sits at ~0.85 to leave room for this band. + if isempty(obj.Title) + return; + end + % Delete any prior header first so repeated render() calls (reflow / + % re-realize) never stack duplicate labels. + old = findobj(parentPanel, 'Tag', 'WidgetTitleHeader'); + if ~isempty(old) + delete(old); + end + uicontrol('Parent', parentPanel, 'Style', 'text', ... + 'Tag', 'WidgetTitleHeader', ... + 'Units', 'normalized', 'Position', [0.03 0.80 0.94 0.18], ... + 'String', obj.Title, ... + 'HorizontalAlignment', 'center', ... + 'FontSize', theme.WidgetTitleFontSize, ... + 'FontWeight', 'bold', ... + 'BackgroundColor', theme.WidgetBackground, ... + 'ForegroundColor', theme.ForegroundColor); + end end % NOTE: Conceptually abstract -- every subclass MUST override these methods. diff --git a/libs/Dashboard/EventTimelineWidget.m b/libs/Dashboard/EventTimelineWidget.m index d084c6ba..20251044 100644 --- a/libs/Dashboard/EventTimelineWidget.m +++ b/libs/Dashboard/EventTimelineWidget.m @@ -47,7 +47,7 @@ function render(obj, parentPanel) obj.hAxes = axes('Parent', parentPanel, ... 'Units', 'normalized', ... - 'Position', [0.06 0.15 0.9 0.7], ... + 'Position', [0.06 0.13 0.9 0.66], ... 'FontName', fontName, ... 'FontSize', theme.WidgetTitleFontSize - 1, ... 'XColor', fgColor, ... @@ -56,11 +56,8 @@ function render(obj, parentPanel) 'YDir', 'reverse'); hold(obj.hAxes, 'on'); - if ~isempty(obj.Title) - title(obj.hAxes, obj.Title, ... - 'Color', fgColor, ... - 'FontSize', theme.WidgetTitleFontSize); - end + % Phase 3 — title as a header band above the axes (no in-axes overlap). + obj.drawPanelTitle_(parentPanel, theme); obj.refresh(); diff --git a/libs/Dashboard/HeatmapWidget.m b/libs/Dashboard/HeatmapWidget.m index c6fbb2a6..92b46fc4 100644 --- a/libs/Dashboard/HeatmapWidget.m +++ b/libs/Dashboard/HeatmapWidget.m @@ -28,16 +28,13 @@ function render(obj, parentPanel) obj.hAxes = axes('Parent', parentPanel, ... 'Units', 'normalized', ... - 'Position', [0.1 0.1 0.8 0.8], ... + 'Position', [0.1 0.1 0.8 0.67], ... 'Color', bg, ... 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); - if ~isempty(obj.Title) - title(obj.hAxes, obj.Title, ... - 'Color', theme.ForegroundColor, ... - 'FontSize', theme.WidgetTitleFontSize); - end + % Phase 3 — title as a header band above the axes (no in-axes overlap). + obj.drawPanelTitle_(parentPanel, theme); obj.refresh(); end diff --git a/libs/Dashboard/HistogramWidget.m b/libs/Dashboard/HistogramWidget.m index 481787ae..699a9b6f 100644 --- a/libs/Dashboard/HistogramWidget.m +++ b/libs/Dashboard/HistogramWidget.m @@ -23,15 +23,12 @@ function render(obj, parentPanel) theme = obj.getTheme(); obj.hAxes = axes('Parent', parentPanel, ... 'Units', 'normalized', ... - 'Position', [0.12 0.15 0.82 0.75], ... + 'Position', [0.12 0.13 0.82 0.65], ... 'Color', theme.WidgetBackground, ... 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); - if ~isempty(obj.Title) - title(obj.hAxes, obj.Title, ... - 'Color', theme.ForegroundColor, ... - 'FontSize', theme.WidgetTitleFontSize); - end + % Phase 3 — title as a header band above the axes (no in-axes overlap). + obj.drawPanelTitle_(parentPanel, theme); % A freshly created axes is always empty, so force a repopulate. % Without this, a re-render (resize / theme toggle / page switch) % recreates the axes but refresh() early-returns on ~Dirty (it was @@ -85,16 +82,13 @@ function refresh(obj) % Re-apply theme after plot commands. bar/plot run newplot, which % resets the axes Color (background) and XColor/YColor to their % light-mode defaults — a glaring white box in dark mode. Restore - % the themed colors (and the title) every refresh. + % the themed colors every refresh. The title lives in a sibling + % header band (drawPanelTitle_, immune to newplot), so no in-axes + % title re-apply is needed. theme = obj.getTheme(); set(obj.hAxes, 'Color', theme.WidgetBackground, ... 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); - if ~isempty(obj.Title) - title(obj.hAxes, obj.Title, ... - 'Color', theme.ForegroundColor, ... - 'FontSize', theme.WidgetTitleFontSize); - end obj.Dirty = false; end diff --git a/libs/Dashboard/MultiStatusWidget.m b/libs/Dashboard/MultiStatusWidget.m index 5f174b7e..e9a8349e 100644 --- a/libs/Dashboard/MultiStatusWidget.m +++ b/libs/Dashboard/MultiStatusWidget.m @@ -422,6 +422,14 @@ function relayout_(obj) try t = TagRegistry.get(char(t)); catch, return; end end if ~isa(t, 'Tag'), return; end + % Only monitor-kind tags carry a binary 0/1 alarm signal, so the + % >= 0.5 -> alarm mapping applies to them alone. A plain sensor + % returns its raw reading (e.g. 72.9), which is always >= 0.5 and + % previously painted every sensor alarm-red. getKind() keeps this + % polymorphic (Pitfall 1 — no isa-on-subclass). + isMonitor = false; + try isMonitor = strcmp(t.getKind(), 'monitor'); catch, end + if ~isMonitor, return; end try v = t.valueAt(now); catch @@ -494,10 +502,18 @@ function relayout_(obj) end if isa(sensor, 'Tag') try - theme = obj.getTheme(); - v = sensor.valueAt(now); - if ~isempty(v) && isnumeric(v) && ~any(isnan(v)) && v(1) >= 0.5 - color = theme.StatusAlarmColor; + % Only monitor-kind tags carry a binary 0/1 alarm signal; + % a plain sensor's raw value must not be thresholded at 0.5 + % (that painted every sensor >= 0.5 alarm-red). Non-monitor + % tags with no threshold have no alarm state -> default. + isMonitor = false; + try isMonitor = strcmp(sensor.getKind(), 'monitor'); catch, end + if isMonitor + theme = obj.getTheme(); + v = sensor.valueAt(now); + if ~isempty(v) && isnumeric(v) && ~any(isnan(v)) && v(1) >= 0.5 + color = theme.StatusAlarmColor; + end end catch % Defensive: any Tag-side failure falls through to default. diff --git a/libs/Dashboard/NumberWidget.m b/libs/Dashboard/NumberWidget.m index 29647f6f..57e7447b 100644 --- a/libs/Dashboard/NumberWidget.m +++ b/libs/Dashboard/NumberWidget.m @@ -53,56 +53,72 @@ function render(obj, parentPanel) set(parentPanel, 'Units', oldUnits); pH = pxPos(4); % panel height in pixels - valueFontSz = max(8, min(28, round(pH * 0.45))); - titleFontSz = max(7, min(14, round(pH * 0.22))); - trendFontSz = max(6, min(16, round(pH * 0.25))); + mutedColor = fgColor * 0.55 + bgColor * 0.45; - % Horizontal layout: [Title | Value+Trend | Units] - obj.hTitleText = uicontrol('Parent', parentPanel, ... - 'Style', 'text', ... - 'String', obj.Title, ... - 'Units', 'normalized', ... - 'Position', [0.02 0.02 0.28 0.96], ... - 'FontName', fontName, ... - 'FontSize', titleFontSz, ... - 'FontWeight', 'bold', ... - 'ForegroundColor', fgColor * 0.7 + bgColor * 0.3, ... - 'BackgroundColor', bgColor, ... - 'HorizontalAlignment', 'right'); + % Phase 4 UI refresh — adaptive KPI layout. Cells with real height + % get a proper card (title top-left muted, big left-aligned value + + % inline unit, trend top-right). Very short cells (a couple of grid + % rows, e.g. an ~11px content panel) fall back to the proven compact + % single-row layout so nothing clips. + if pH >= 55 + valueFontSz = max(12, min(30, round(pH * 0.40))); + titleFontSz = max(8, min(13, round(pH * 0.16))); + trendFontSz = max(7, min(14, round(pH * 0.18))); - obj.hValueText = uicontrol('Parent', parentPanel, ... - 'Style', 'text', ... - 'String', '--', ... - 'Units', 'normalized', ... - 'Position', [0.31 0.02 0.40 0.96], ... - 'FontName', fontName, ... - 'FontSize', valueFontSz, ... - 'FontWeight', 'bold', ... - 'ForegroundColor', fgColor, ... - 'BackgroundColor', bgColor, ... - 'HorizontalAlignment', 'center'); - - obj.hTrendText = uicontrol('Parent', parentPanel, ... - 'Style', 'text', ... - 'String', '', ... - 'Units', 'normalized', ... - 'Position', [0.72 0.02 0.08 0.96], ... - 'FontName', fontName, ... - 'FontSize', trendFontSz, ... - 'ForegroundColor', fgColor, ... - 'BackgroundColor', bgColor, ... - 'HorizontalAlignment', 'center'); + obj.hTitleText = uicontrol('Parent', parentPanel, 'Style', 'text', ... + 'String', obj.Title, 'Units', 'normalized', ... + 'Position', [0.07 0.62 0.71 0.30], ... + 'FontName', fontName, 'FontSize', titleFontSz, 'FontWeight', 'bold', ... + 'ForegroundColor', mutedColor, 'BackgroundColor', bgColor, ... + 'HorizontalAlignment', 'left'); + obj.hTrendText = uicontrol('Parent', parentPanel, 'Style', 'text', ... + 'String', '', 'Units', 'normalized', ... + 'Position', [0.80 0.62 0.14 0.30], ... + 'FontName', fontName, 'FontSize', trendFontSz, ... + 'ForegroundColor', theme.DragHandleColor, 'BackgroundColor', bgColor, ... + 'HorizontalAlignment', 'right'); + obj.hValueText = uicontrol('Parent', parentPanel, 'Style', 'text', ... + 'String', '--', 'Units', 'normalized', ... + 'Position', [0.06 0.05 0.64 0.56], ... + 'FontName', fontName, 'FontSize', valueFontSz, 'FontWeight', 'bold', ... + 'ForegroundColor', fgColor, 'BackgroundColor', bgColor, ... + 'HorizontalAlignment', 'left'); + obj.hUnitText = uicontrol('Parent', parentPanel, 'Style', 'text', ... + 'String', obj.Units, 'Units', 'normalized', ... + 'Position', [0.69 0.10 0.26 0.30], ... + 'FontName', fontName, 'FontSize', titleFontSz, ... + 'ForegroundColor', mutedColor, 'BackgroundColor', bgColor, ... + 'HorizontalAlignment', 'left'); + else + valueFontSz = max(8, min(28, round(pH * 0.45))); + titleFontSz = max(7, min(14, round(pH * 0.22))); + trendFontSz = max(6, min(16, round(pH * 0.25))); - obj.hUnitText = uicontrol('Parent', parentPanel, ... - 'Style', 'text', ... - 'String', obj.Units, ... - 'Units', 'normalized', ... - 'Position', [0.80 0.02 0.18 0.96], ... - 'FontName', fontName, ... - 'FontSize', titleFontSz, ... - 'ForegroundColor', fgColor * 0.5 + bgColor * 0.5, ... - 'BackgroundColor', bgColor, ... - 'HorizontalAlignment', 'left'); + obj.hTitleText = uicontrol('Parent', parentPanel, 'Style', 'text', ... + 'String', obj.Title, 'Units', 'normalized', ... + 'Position', [0.02 0.02 0.28 0.96], ... + 'FontName', fontName, 'FontSize', titleFontSz, 'FontWeight', 'bold', ... + 'ForegroundColor', fgColor * 0.7 + bgColor * 0.3, 'BackgroundColor', bgColor, ... + 'HorizontalAlignment', 'right'); + obj.hValueText = uicontrol('Parent', parentPanel, 'Style', 'text', ... + 'String', '--', 'Units', 'normalized', ... + 'Position', [0.31 0.02 0.40 0.96], ... + 'FontName', fontName, 'FontSize', valueFontSz, 'FontWeight', 'bold', ... + 'ForegroundColor', fgColor, 'BackgroundColor', bgColor, ... + 'HorizontalAlignment', 'center'); + obj.hTrendText = uicontrol('Parent', parentPanel, 'Style', 'text', ... + 'String', '', 'Units', 'normalized', ... + 'Position', [0.72 0.02 0.08 0.96], ... + 'FontName', fontName, 'FontSize', trendFontSz, ... + 'ForegroundColor', theme.DragHandleColor, 'BackgroundColor', bgColor, ... + 'HorizontalAlignment', 'center'); + obj.hUnitText = uicontrol('Parent', parentPanel, 'Style', 'text', ... + 'String', obj.Units, 'Units', 'normalized', ... + 'Position', [0.80 0.02 0.18 0.96], ... + 'FontName', fontName, 'FontSize', titleFontSz, ... + 'ForegroundColor', fgColor * 0.5 + bgColor * 0.5, 'BackgroundColor', bgColor, ... + 'HorizontalAlignment', 'left'); + end obj.refresh(); end diff --git a/libs/Dashboard/ScatterWidget.m b/libs/Dashboard/ScatterWidget.m index dec1b5ae..c3237bb5 100644 --- a/libs/Dashboard/ScatterWidget.m +++ b/libs/Dashboard/ScatterWidget.m @@ -25,15 +25,12 @@ function render(obj, parentPanel) theme = obj.getTheme(); obj.hAxes = axes('Parent', parentPanel, ... 'Units', 'normalized', ... - 'Position', [0.12 0.15 0.82 0.75], ... + 'Position', [0.12 0.13 0.82 0.65], ... 'Color', theme.WidgetBackground, ... 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); - if ~isempty(obj.Title) - title(obj.hAxes, obj.Title, ... - 'Color', theme.ForegroundColor, ... - 'FontSize', theme.WidgetTitleFontSize); - end + % Phase 3 — title as a header band above the axes (no in-axes overlap). + obj.drawPanelTitle_(parentPanel, theme); obj.refresh(); end diff --git a/libs/FastSense/FastSenseTheme.m b/libs/FastSense/FastSenseTheme.m index 06d24bac..d44d5c62 100644 --- a/libs/FastSense/FastSenseTheme.m +++ b/libs/FastSense/FastSenseTheme.m @@ -91,19 +91,21 @@ % Throws FastSenseTheme:unknownPreset if the name is not recognized. switch lower(name) case {'default', 'industrial', 'scientific', 'ocean', 'light'} + % Clean-modern light preset: crisp white axes on a soft neutral + % canvas, cool low-contrast grid, semantic-red threshold. t = struct( ... - 'Background', [0.98 0.98 0.98], ... + 'Background', [0.961 0.965 0.973], ... 'AxesColor', [1 1 1], ... - 'ForegroundColor', [0.25 0.25 0.25], ... - 'GridColor', [0.8 0.8 0.8], ... - 'GridAlpha', 0.3, ... + 'ForegroundColor', [0.208 0.255 0.333], ... + 'GridColor', [0.914 0.929 0.949], ... + 'GridAlpha', 0.7, ... 'GridStyle', '-', ... 'FontName', 'Helvetica', ... 'FontSize', 10, ... 'TitleFontSize', 12, ... 'LineWidth', 1.0, ... 'LineColorOrder', 'muted', ... - 'ThresholdColor', [0.7 0 0], ... + 'ThresholdColor', [0.937 0.267 0.267], ... 'ThresholdStyle', '--', ... 'ViolationMarker', 'o', ... 'ViolationSize', 4, ... diff --git a/tests/suite/TestFastSenseTheme.m b/tests/suite/TestFastSenseTheme.m index 63faa0f2..24043a1a 100644 --- a/tests/suite/TestFastSenseTheme.m +++ b/tests/suite/TestFastSenseTheme.m @@ -24,8 +24,8 @@ function testThemeConstructorStruct(testCase) function testDefaultThemeWhenNoneSpecified(testCase) fp = FastSense(); testCase.verifyTrue(isstruct(fp.Theme), 'testDefaultTheme: must have theme'); - % Default theme is 'light'; Background is [0.98 0.98 0.98] since PR #68. - testCase.verifyEqual(fp.Theme.Background, [0.98 0.98 0.98], 'testDefaultTheme: default bg'); + % Default theme is 'light'; clean-modern Background is [0.961 0.965 0.973]. + testCase.verifyEqual(fp.Theme.Background, [0.961 0.965 0.973], 'testDefaultTheme: default bg'); end function testThemeAppliedOnRender(testCase) diff --git a/tests/suite/TestTheme.m b/tests/suite/TestTheme.m index 511c3298..cc84e4f6 100644 --- a/tests/suite/TestTheme.m +++ b/tests/suite/TestTheme.m @@ -10,8 +10,9 @@ function addPaths(testCase) function testDefaultPreset(testCase) t = FastSenseTheme('default'); testCase.verifyTrue(isstruct(t), 'testDefaultPreset: must return struct'); - % 'default' is aliased to 'light' since PR #68; Background is [0.98 0.98 0.98]. - testCase.verifyEqual(t.Background, [0.98 0.98 0.98], 'testDefaultPreset: Background'); + % 'default' is aliased to 'light' since PR #68; the clean-modern light + % preset uses Background [0.961 0.965 0.973]. + testCase.verifyEqual(t.Background, [0.961 0.965 0.973], 'testDefaultPreset: Background'); testCase.verifyTrue(isfield(t, 'AxesColor'), 'testDefaultPreset: AxesColor field'); testCase.verifyTrue(isfield(t, 'ForegroundColor'), 'testDefaultPreset: ForegroundColor field'); testCase.verifyTrue(isfield(t, 'GridColor'), 'testDefaultPreset: GridColor field'); @@ -41,7 +42,7 @@ function testMergeOverrides(testCase) testCase.verifyEqual(t.FontSize, 14, 'testMergeOverrides: FontSize'); testCase.verifyEqual(t.LineWidth, 2.0, 'testMergeOverrides: LineWidth'); % 'default' aliases to 'light'; overrides do not affect Background. - testCase.verifyEqual(t.Background, [0.98 0.98 0.98], 'testMergeOverrides: Background unchanged'); + testCase.verifyEqual(t.Background, [0.961 0.965 0.973], 'testMergeOverrides: Background unchanged'); end function testInvalidPresetErrors(testCase) @@ -86,12 +87,12 @@ function testScientificPreset(testCase) function testOceanPreset(testCase) % 'ocean' is aliased to 'light' since PR #68. The Background is - % now [0.98 0.98 0.98] (light preset) and AxesColor is [1 1 1]. + % now [0.961 0.965 0.973] (light preset) and AxesColor is [1 1 1]. % The 'ocean' palette name still resolves to 8 ocean-blue colours % via getPalette() when used as a LineColorOrder override, but the % preset itself uses the 'muted' palette (light preset default). t = FastSenseTheme('ocean'); - testCase.verifyEqual(t.Background, [0.98 0.98 0.98], 'testOceanPreset: Background matches light preset'); + testCase.verifyEqual(t.Background, [0.961 0.965 0.973], 'testOceanPreset: Background matches light preset'); testCase.verifyEqual(t.AxesColor, [1 1 1], 'testOceanPreset: AxesColor should be white'); testCase.verifyEqual(size(t.LineColorOrder, 2), 3, 'testOceanPreset: LineColorOrder Nx3'); testCase.verifyEqual(size(t.LineColorOrder, 1), 8, 'testOceanPreset: 8 colors in muted palette');