From 156cb7b14e5857883ad260c9c4084bffb68dba1f Mon Sep 17 00:00:00 2001 From: David C Date: Mon, 26 Jan 2026 12:23:21 -0800 Subject: [PATCH 1/8] First Pass: Refactor settings and controls into separate toolbar component Extracted toolbar functionality from PerformanceDashboard into new DashboardToolbar component. Removed 'actions' from default dashboard metrics. Updated layout to use flexbox with separate toolbar and metrics grid sections. --- app/client/components/DashboardToolbar.js | 253 ++++++++++++++++++ app/client/components/PerformanceDashboard.js | 72 ++--- app/client/store/appState.js | 2 +- app/client/store/dashboardMetrics.js | 5 +- 4 files changed, 277 insertions(+), 55 deletions(-) create mode 100644 app/client/components/DashboardToolbar.js diff --git a/app/client/components/DashboardToolbar.js b/app/client/components/DashboardToolbar.js new file mode 100644 index 0000000000..83ff6dd5e2 --- /dev/null +++ b/app/client/components/DashboardToolbar.js @@ -0,0 +1,253 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor + + Toolbar component combining settings and action buttons +*/ + +import { AppElement, html, css } from './AppElement.js' +import { customElement, property, state } from 'lit/decorators.js' +import { iconSettings, iconUndo, iconExpand, iconCompress, iconPoweroff, iconBluetooth, iconUpload, iconHeartbeat, iconAntplus } from '../lib/icons.js' +import './SettingsDialog.js' +import './AppDialog.js' + +@customElement('dashboard-toolbar') +export class DashboardToolbar extends AppElement { + static styles = css` + :host { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--theme-widget-color); + padding: 0.3em 0.5em; + border-radius: var(--theme-border-radius); + gap: 0.5em; + } + + .button-group { + display: flex; + gap: 0.3em; + align-items: center; + flex-wrap: wrap; + } + + button { + position: relative; + outline: none; + background-color: var(--theme-button-color); + border: 0; + border-radius: var(--theme-border-radius); + color: var(--theme-font-color); + font-size: 0.4em; + text-decoration: none; + display: inline-flex; + width: 2.5em; + min-width: 2.5em; + height: 2.5em; + justify-content: center; + align-items: center; + cursor: pointer; + } + + button:hover { + filter: brightness(150%); + } + + button > div.text { + position: absolute; + left: 2px; + bottom: 2px; + font-size: 40%; + } + + .icon { + height: 1.2em; + } + + .peripheral-mode-container { + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 0.1em; + } + + .peripheral-mode { + font-size: 0.3em; + text-align: center; + } + + #fullscreen-icon { + display: inline-flex; + } + + #windowed-icon { + display: none; + } + + @media (display-mode: fullscreen) { + #fullscreen-icon { + display: none; + } + #windowed-icon { + display: inline-flex; + } + } + ` + + @property({ type: Object }) + config = {} + + @state() + _appMode = 'BROWSER' + + @state() + _dialog + + render () { + return html` +
+ + + ${this.renderOptionalButtons()} +
+ +
+ + +
+ +
${this.blePeripheralMode()}
+
+
+ + ${this._dialog ? this._dialog : ''} + ` + } + + firstUpdated () { + switch (new URLSearchParams(window.location.search).get('mode')) { + case 'standalone': + this._appMode = 'STANDALONE' + break + case 'kiosk': + this._appMode = 'KIOSK' + break + default: + this._appMode = 'BROWSER' + } + } + + renderOptionalButtons () { + const buttons = [] + if (this._appMode === 'BROWSER' && document.documentElement.requestFullscreen) { + buttons.push(html` + + `) + } + if (this._appMode === 'KIOSK' && this.config?.shutdownEnabled) { + buttons.push(html` + + `) + } + if (this.config?.uploadEnabled) { + buttons.push(html` + + `) + } + return buttons + } + + blePeripheralMode () { + const value = this.config?.blePeripheralMode + switch (value) { + case 'PM5': + return 'C2 PM5' + case 'FTMSBIKE': + return 'FTMS Bike' + case 'CSC': + return 'Bike Speed + Cadence' + case 'CPS': + return 'Bike Power' + case 'FTMS': + return 'FTMS Rower' + default: + return 'Off' + } + } + + openSettings () { + this._dialog = html` { + this._dialog = undefined + }}>` + } + + toggleFullscreen () { + const fullscreenElement = document.getElementsByTagName('web-app')[0] + if (!document.fullscreenElement) { + fullscreenElement.requestFullscreen({ navigationUI: 'hide' }) + } else { + if (document.exitFullscreen) { + document.exitFullscreen() + } + } + } + + reset () { + this.sendEvent('triggerAction', { command: 'reset' }) + } + + switchBlePeripheralMode () { + this.sendEvent('triggerAction', { command: 'switchBlePeripheralMode' }) + } + + switchAntPeripheralMode () { + this.sendEvent('triggerAction', { command: 'switchAntPeripheralMode' }) + } + + switchHrmPeripheralMode () { + this.sendEvent('triggerAction', { command: 'switchHrmMode' }) + } + + uploadTraining () { + this._dialog = html` + { + this._dialog = undefined + if (event.detail === 'confirm') { + this.sendEvent('triggerAction', { command: 'upload' }) + } + }}> + ${iconUpload}
Upload training?
+

Do you want to finish your workout and upload it to webservices (Strava, Intervals.icu and RowsAndAll)?

+
+ ` + } + + shutdown () { + this._dialog = html` + { + this._dialog = undefined + if (event.detail === 'confirm') { + this.sendEvent('triggerAction', { command: 'shutdown' }) + } + }}> + ${iconPoweroff}
Shutdown Open Rowing Monitor?
+

Do you want to shutdown the device?

+
+ ` + } +} diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 7d0e932eb3..181897ce93 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -6,64 +6,46 @@ */ import { AppElement, html, css } from './AppElement.js' -import { customElement, property, state } from 'lit/decorators.js' -import './SettingsDialog.js' -import { iconSettings } from '../lib/icons.js' +import { customElement, property } from 'lit/decorators.js' +import './DashboardToolbar.js' import { DASHBOARD_METRICS } from '../store/dashboardMetrics.js' @customElement('performance-dashboard') export class PerformanceDashboard extends AppElement { static styles = css` :host { - display: grid; - height: calc(100vh - 2vw); + display: flex; + flex-direction: column; + height: 100vh; padding: 1vw; + gap: 1vw; + } + + .metrics-grid { + display: grid; + flex: 1; grid-gap: 1vw; grid-template-columns: repeat(4, minmax(0, 1fr)); } @media (orientation: portrait) { - :host { + .metrics-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(4, minmax(0, 1fr)); } } - dashboard-metric, dashboard-actions, dashboard-force-curve { + dashboard-metric, dashboard-force-curve { background: var(--theme-widget-color); text-align: center; position: relative; padding: 0.5em 0.2em 0 0.2em; border-radius: var(--theme-border-radius); } - - dashboard-actions { - padding: 0.5em 0 0 0; - } - - .settings { - padding: 0.1em 0; - position: absolute; - bottom: 0; - right: 0; - z-index: 20; - } - - .settings .icon { - cursor: pointer; - height: 1em; - } - - .settings:hover .icon { - filter: brightness(150%); - } ` @property() appState = {} - @state() - _dialog - dashboardMetricComponentsFactory = (appState) => { const metrics = appState.metrics const configs = appState.config @@ -84,26 +66,16 @@ export class PerformanceDashboard extends AppElement { }, []) return html` - -
- ${iconSettings} - ${this._dialog ? this._dialog : ''} + + +
+ + ${metricConfig}
- - ${metricConfig} ` } - - openSettings () { - this._dialog = html`` - - /* eslint-disable-next-line no-unused-vars -- Standard construct?? */ - function dialogClosed (event) { - this._dialog = undefined - } - } } diff --git a/app/client/store/appState.js b/app/client/store/appState.js index 39ed76d3a0..1d06fa67d3 100644 --- a/app/client/store/appState.js +++ b/app/client/store/appState.js @@ -47,7 +47,7 @@ export const APP_STATE = { // true if remote device shutdown is enabled shutdownEnabled: false, guiConfigs: { - dashboardMetrics: ['distance', 'timer', 'pace', 'power', 'stkRate', 'totalStk', 'calories', 'actions'], + dashboardMetrics: ['distance', 'timer', 'pace', 'power', 'stkRate', 'totalStk', 'calories'], showIcons: true, maxNumberOfTiles: 8 } diff --git a/app/client/store/dashboardMetrics.js b/app/client/store/dashboardMetrics.js index de019120ec..bfae322fa7 100644 --- a/app/client/store/dashboardMetrics.js +++ b/app/client/store/dashboardMetrics.js @@ -7,7 +7,6 @@ import { html } from 'lit' import { formatDistance, formatNumber, secondsToTimeString } from '../lib/helper' import { iconBolt, iconClock, iconAlarmclock, iconFire, iconHeartbeat, iconPaddle, iconRoute, iconStopwatch, rowerIcon } from '../lib/icons' import '../components/DashboardForceCurve.js' -import '../components/DashboardActions.js' import '../components/DashboardMetric.js' import '../components/BatteryIcon.js' @@ -94,9 +93,7 @@ export const DASHBOARD_METRICS = { recoveryDuration: { displayName: 'Recovery duration', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.recoveryDuration, 2), 'sec', config?.guiConfigs?.showIcons ? 'Recovery' : '') }, - forceCurve: { displayName: 'Force curve', size: 2, template: (metrics) => html`` }, - - actions: { displayName: 'Actions', size: 1, template: (_, config) => html`` } + forceCurve: { displayName: 'Force curve', size: 2, template: (metrics) => html`` } } /** From 27d3cbc9ae6e05d024e1733d2a9137aac89f2c77 Mon Sep 17 00:00:00 2001 From: David C Date: Mon, 26 Jan 2026 12:28:38 -0800 Subject: [PATCH 2/8] Fix layout padding calculation to prevent vertical overflow scrolling --- app/client/components/PerformanceDashboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 181897ce93..e82e28c0b7 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -16,8 +16,8 @@ export class PerformanceDashboard extends AppElement { :host { display: flex; flex-direction: column; - height: 100vh; - padding: 1vw; + height: 99vh; + padding: 1vh 1vw; gap: 1vw; } From 434d7f81a38344d9c235c1383b86917e3eb3a8d1 Mon Sep 17 00:00:00 2001 From: David C Date: Tue, 27 Jan 2026 13:21:37 -0800 Subject: [PATCH 3/8] Added migration logic to remove 'actions' from saved dashboardMetrics in localStorage. Remove deprecated DashboardActions component and migrate saved metrics Deleted DashboardActions.js component as functionality has been moved to DashboardToolbar. --- app/client/components/DashboardActions.js | 225 ---------------------- app/client/index.js | 6 + 2 files changed, 6 insertions(+), 225 deletions(-) delete mode 100644 app/client/components/DashboardActions.js diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js deleted file mode 100644 index a24ca248fd..0000000000 --- a/app/client/components/DashboardActions.js +++ /dev/null @@ -1,225 +0,0 @@ -'use strict' -/* - Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor - - Component that renders the action buttons of the dashboard -*/ - -import { AppElement, html, css } from './AppElement.js' -import { customElement, property, state } from 'lit/decorators.js' -import { iconUndo, iconExpand, iconCompress, iconPoweroff, iconBluetooth, iconUpload, iconHeartbeat, iconAntplus } from '../lib/icons.js' -import './AppDialog.js' - -@customElement('dashboard-actions') -export class DashboardActions extends AppElement { - static styles = css` - button { - position: relative; - outline:none; - background-color: var(--theme-button-color); - border: 0; - border-radius: var(--theme-border-radius); - color: var(--theme-font-color); - margin: 0.2em 4px; - font-size: 60%; - text-decoration: none; - display: inline-flex; - width: 3.2em; - min-width: 3.2em; - height: 2.2em; - justify-content: center; - align-items: center; - } - - button:hover { - filter: brightness(150%); - } - - button > div.text { - position: absolute; - left: 2px; - bottom: 2px; - font-size: 40%; - } - - #fullscreen-icon { - display: inline-flex; - } - - .top-button-group { - display: flex; - flex-wrap: wrap; - justify-content: center; - } - - #windowed-icon { - display: none; - } - - .icon { - height: 1.7em; - } - - .peripheral-mode { - font-size: 50%; - } - - @media (display-mode: fullscreen) { - #fullscreen-icon { - display: none; - } - #windowed-icon { - display: inline-flex; - } - } - ` - - @property({ type: Object }) - config = {} - - @state() - _appMode = 'BROWSER' - - @state() - _dialog - - render () { - return html` -
- - ${this.renderOptionalButtons()} - - -
-
- -
${this.blePeripheralMode()}
-
- ${this._dialog ? this._dialog : ''} - ` - } - - firstUpdated () { - switch (new URLSearchParams(window.location.search).get('mode')) { - case 'standalone': - this._appMode = 'STANDALONE' - break - case 'kiosk': - this._appMode = 'KIOSK' - break - default: - this._appMode = 'BROWSER' - } - } - - renderOptionalButtons () { - const buttons = [] - // changing to fullscreen mode only makes sence when the app is openend in a regular - // webbrowser (kiosk and standalone mode are always in fullscreen view) and if the - // browser supports this feature - if (this._appMode === 'BROWSER' && document.documentElement.requestFullscreen) { - buttons.push(html` - - `) - } - // add a button to power down the device, if browser is running on the device in kiosk mode - // and the shutdown feature is enabled - // (might also make sence to enable this for all clients but then we would need visual feedback) - if (this._appMode === 'KIOSK' && this.config?.shutdownEnabled) { - buttons.push(html` - - `) - } - - if (this.config?.uploadEnabled) { - buttons.push(html` - - `) - } - return buttons - } - - blePeripheralMode () { - const value = this.config?.blePeripheralMode - switch (value) { - case 'PM5': - return 'C2 PM5' - case 'FTMSBIKE': - return 'FTMS Bike' - case 'CSC': - return 'Bike Speed + Cadence' - case 'CPS': - return 'Bike Power' - case 'FTMS': - return 'FTMS Rower' - default: - return 'Off' - } - } - - toggleFullscreen () { - const fullscreenElement = document.getElementsByTagName('web-app')[0] - if (!document.fullscreenElement) { - fullscreenElement.requestFullscreen({ navigationUI: 'hide' }) - } else { - if (document.exitFullscreen) { - document.exitFullscreen() - } - } - } - - reset () { - this.sendEvent('triggerAction', { command: 'reset' }) - } - - switchBlePeripheralMode () { - this.sendEvent('triggerAction', { command: 'switchBlePeripheralMode' }) - } - - switchAntPeripheralMode () { - this.sendEvent('triggerAction', { command: 'switchAntPeripheralMode' }) - } - - switchHrmPeripheralMode () { - this.sendEvent('triggerAction', { command: 'switchHrmMode' }) - } - - uploadTraining () { - this._dialog = html` - - ${iconUpload}
Upload training?
-

Do you want to finish your workout and upload it to webservices (Strava, Intervals.icu and RowsAndAll)?

-
- ` - function dialogClosed (event) { - this._dialog = undefined - if (event.detail === 'confirm') { - this.sendEvent('triggerAction', { command: 'upload' }) - } - } - } - - shutdown () { - this._dialog = html` - - ${iconPoweroff}
Shutdown Open Rowing Monitor?
-

Do you want to shutdown the device?

-
- ` - function dialogClosed (event) { - this._dialog = undefined - if (event.detail === 'confirm') { - this.sendEvent('triggerAction', { command: 'shutdown' }) - } - } - } -} diff --git a/app/client/index.js b/app/client/index.js index 4bfc50271d..26a9fa2c64 100644 --- a/app/client/index.js +++ b/app/client/index.js @@ -30,6 +30,12 @@ export class App extends LitElement { config[key] = JSON.parse(localStorage.getItem(key)) ?? config[key] }) + // migrate: remove deprecated 'actions' metric from saved dashboardMetrics + if (config.dashboardMetrics.includes('actions')) { + config.dashboardMetrics = config.dashboardMetrics.filter(m => m !== 'actions') + localStorage.setItem('dashboardMetrics', JSON.stringify(config.dashboardMetrics)) + } + // this is how we implement changes to the global state: // once any child component sends this CustomEvent we update the global state according // to the changes that were passed to us From 69773031753d007b13682e969b761eaf24833998 Mon Sep 17 00:00:00 2001 From: David C Date: Tue, 27 Jan 2026 13:23:13 -0800 Subject: [PATCH 4/8] WIP: Cleaning up Dashboard CSS. Replaced dynamic inline style injection with CSS class-based approach for grid row configuration. Changed metrics container from div to semantic section element. Updated grid layout to use explicit grid-template-rows and added .rows-3 modifier class for 12-tile configuration. --- app/client/components/PerformanceDashboard.js | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index e82e28c0b7..268c75fec5 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -14,32 +14,40 @@ import { DASHBOARD_METRICS } from '../store/dashboardMetrics.js' export class PerformanceDashboard extends AppElement { static styles = css` :host { - display: flex; - flex-direction: column; - height: 99vh; - padding: 1vh 1vw; + display: grid; + grid-template-rows: auto 1fr; + height: 100vh; gap: 1vw; + box-sizing: border-box; } .metrics-grid { display: grid; - flex: 1; - grid-gap: 1vw; - grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1vw; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(2, 1fr); + } + + .metrics-grid.rows-3 { + grid-template-rows: repeat(3, 1fr); } @media (orientation: portrait) { .metrics-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - grid-template-rows: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(4, 1fr); + } + + .metrics-grid.rows-3 { + grid-template-rows: repeat(6, 1fr); } } - dashboard-metric, dashboard-force-curve { + dashboard-metric, + dashboard-force-curve { background: var(--theme-widget-color); text-align: center; - position: relative; - padding: 0.5em 0.2em 0 0.2em; + padding: 0.5em 0.2em 0; border-radius: var(--theme-border-radius); } ` @@ -65,17 +73,11 @@ export class PerformanceDashboard extends AppElement { return prev }, []) + const gridClass = this.appState.config.guiConfigs.maxNumberOfTiles === 12 ? 'rows-3' : '' + return html` - -
- - ${metricConfig} -
+
${metricConfig}
` } } From e09db949ef51f12ca4fae2c5f8724436633a25a6 Mon Sep 17 00:00:00 2001 From: David C Date: Tue, 27 Jan 2026 13:24:26 -0800 Subject: [PATCH 5/8] WIP: Refactor DashboardToolbar CSS for consistency and semantic improvements Reorganized CSS properties for better readability and consistency. Simplified button styles by removing redundant properties and using flex instead of inline-flex. Changed fullscreen/windowed icon containers from div with IDs to span with classes. Consolidated media query icon display rules into single-line declarations. --- app/client/components/DashboardToolbar.js | 48 +++++++++-------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/app/client/components/DashboardToolbar.js b/app/client/components/DashboardToolbar.js index 83ff6dd5e2..8c4c207064 100644 --- a/app/client/components/DashboardToolbar.js +++ b/app/client/components/DashboardToolbar.js @@ -18,34 +18,32 @@ export class DashboardToolbar extends AppElement { display: flex; justify-content: space-between; align-items: center; - background: var(--theme-widget-color); + gap: 0.5em; padding: 0.3em 0.5em; + background: var(--theme-widget-color); border-radius: var(--theme-border-radius); - gap: 0.5em; } .button-group { display: flex; - gap: 0.3em; align-items: center; + gap: 0.3em; flex-wrap: wrap; } button { position: relative; - outline: none; - background-color: var(--theme-button-color); + display: flex; + justify-content: center; + align-items: center; + width: 2.5em; + height: 2.5em; + padding: 0; border: 0; border-radius: var(--theme-border-radius); + background: var(--theme-button-color); color: var(--theme-font-color); font-size: 0.4em; - text-decoration: none; - display: inline-flex; - width: 2.5em; - min-width: 2.5em; - height: 2.5em; - justify-content: center; - align-items: center; cursor: pointer; } @@ -53,7 +51,7 @@ export class DashboardToolbar extends AppElement { filter: brightness(150%); } - button > div.text { + button .text { position: absolute; left: 2px; bottom: 2px; @@ -65,7 +63,7 @@ export class DashboardToolbar extends AppElement { } .peripheral-mode-container { - display: inline-flex; + display: flex; flex-direction: row; align-items: center; gap: 0.1em; @@ -73,24 +71,14 @@ export class DashboardToolbar extends AppElement { .peripheral-mode { font-size: 0.3em; - text-align: center; } - #fullscreen-icon { - display: inline-flex; - } - - #windowed-icon { - display: none; - } + .fullscreen-icon { display: flex; } + .windowed-icon { display: none; } @media (display-mode: fullscreen) { - #fullscreen-icon { - display: none; - } - #windowed-icon { - display: inline-flex; - } + .fullscreen-icon { display: none; } + .windowed-icon { display: flex; } } ` @@ -154,8 +142,8 @@ export class DashboardToolbar extends AppElement { if (this._appMode === 'BROWSER' && document.documentElement.requestFullscreen) { buttons.push(html` `) } From a0bd9320ad1139e648526588e78229f7665d968b Mon Sep 17 00:00:00 2001 From: David C Date: Tue, 27 Jan 2026 13:39:10 -0800 Subject: [PATCH 6/8] Refine and make explicit the layout for PerformanceDashboard. Added explicit grid-template-areas to PerformanceDashboard for clearer layout structure. Added min-height: 0 to metrics-grid and metric-tile to prevent grid blowout issues. --- app/client/components/DashboardToolbar.js | 2 +- app/client/components/PerformanceDashboard.js | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/client/components/DashboardToolbar.js b/app/client/components/DashboardToolbar.js index 8c4c207064..9b7285bbb5 100644 --- a/app/client/components/DashboardToolbar.js +++ b/app/client/components/DashboardToolbar.js @@ -19,7 +19,7 @@ export class DashboardToolbar extends AppElement { justify-content: space-between; align-items: center; gap: 0.5em; - padding: 0.3em 0.5em; + padding: 0.2em 0.3em; background: var(--theme-widget-color); border-radius: var(--theme-border-radius); } diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 268c75fec5..3424025eaf 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -15,17 +15,26 @@ export class PerformanceDashboard extends AppElement { static styles = css` :host { display: grid; - grid-template-rows: auto 1fr; + grid-template: + "toolbar" auto + "metrics" 1fr + / 1fr; height: 100vh; gap: 1vw; box-sizing: border-box; } + dashboard-toolbar { + grid-area: toolbar; + } + .metrics-grid { + grid-area: metrics; display: grid; gap: 1vw; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr); + min-height: 0; /* prevent grid blowout */ } .metrics-grid.rows-3 { @@ -49,6 +58,7 @@ export class PerformanceDashboard extends AppElement { text-align: center; padding: 0.5em 0.2em 0; border-radius: var(--theme-border-radius); + min-height: 0; /* prevent grid blowout */ } ` @property() From 162c0b221c501501c6c8e3c1e65a9976e63b62a7 Mon Sep 17 00:00:00 2001 From: David C Date: Tue, 27 Jan 2026 16:12:17 -0800 Subject: [PATCH 7/8] Make chart font sizes responsive using em-based calculations. --- app/client/components/DashboardForceCurve.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/client/components/DashboardForceCurve.js b/app/client/components/DashboardForceCurve.js index 2e57958c7c..5c3d974843 100644 --- a/app/client/components/DashboardForceCurve.js +++ b/app/client/components/DashboardForceCurve.js @@ -14,7 +14,7 @@ import { Chart, Filler, Legend, LinearScale, LineController, LineElement, PointE export class DashboardForceCurve extends AppElement { static styles = css` canvas { - margin-top: 24px; + margin-top: 0.5em; } ` @@ -31,6 +31,7 @@ export class DashboardForceCurve extends AppElement { firstUpdated () { const ctx = this.renderRoot.querySelector('#chart').getContext('2d') + const baseFontSize = parseFloat(getComputedStyle(this).fontSize) this._chart = new Chart( ctx, { @@ -58,7 +59,7 @@ export class DashboardForceCurve extends AppElement { ...ctx.dataset.data.map((point) => point.y) ) === ctx.dataset.data[ctx.dataIndex].y, font: { - size: 16 + size: baseFontSize * 0.4 }, color: 'rgb(255,255,255)' }, @@ -68,7 +69,7 @@ export class DashboardForceCurve extends AppElement { text: 'Force Curve', color: 'rgb(255,255,255)', font: { - size: 32 + size: baseFontSize * 0.8 }, padding: { } From 8db02274d17d81d41ea178b4291f2b25e330e216 Mon Sep 17 00:00:00 2001 From: David C Date: Tue, 27 Jan 2026 16:15:38 -0800 Subject: [PATCH 8/8] Refactor DashboardForceCurve to use separate title div instead of chart legend. More reliable - Avoids Chart.js legend bugs on iOS and autoscaling issues Better for responsive design - HTML text naturally inherits the fluid typography Added host flex container styles and title styling at 80% font size with center alignment. --- app/client/components/DashboardForceCurve.js | 34 +++++++++----------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/app/client/components/DashboardForceCurve.js b/app/client/components/DashboardForceCurve.js index 5c3d974843..59521cf6d6 100644 --- a/app/client/components/DashboardForceCurve.js +++ b/app/client/components/DashboardForceCurve.js @@ -13,8 +13,20 @@ import { Chart, Filler, Legend, LinearScale, LineController, LineElement, PointE @customElement('dashboard-force-curve') export class DashboardForceCurve extends AppElement { static styles = css` + :host { + display: flex; + flex-direction: column; + } + + .title { + font-size: 80%; + text-align: center; + padding: 0.2em 0; + } + canvas { - margin-top: 0.5em; + flex: 1; + min-height: 0; } ` @@ -64,22 +76,7 @@ export class DashboardForceCurve extends AppElement { color: 'rgb(255,255,255)' }, legend: { - title: { - display: true, - text: 'Force Curve', - color: 'rgb(255,255,255)', - font: { - size: baseFontSize * 0.8 - }, - padding: { - } - }, - labels: { - boxWidth: 0, - font: { - size: 0 - } - } + display: false, } }, scales: { @@ -120,7 +117,8 @@ export class DashboardForceCurve extends AppElement { } return html` - +
Force Curve
+ ` } }