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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/client/components/BatteryIcon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { customElement, property } from 'lit/decorators.js'
@customElement('battery-icon')
export class DashboardMetric extends AppElement {
static styles = css`
:host {
right: 0.2em;
bottom: 0;
position: absolute;
}

.icon {
height: 1.2em;
}
Expand Down
8 changes: 7 additions & 1 deletion app/client/components/DashboardForceCurve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class DashboardForceCurve extends AppElement {
:host {
display: block;
position: relative;
grid-column: span 2;
}

.title {
Expand Down Expand Up @@ -181,7 +182,12 @@ export class DashboardForceCurve extends AppElement {
return html`
${this._chart?.data.datasets[0].data.length ?
'' :
html`<div class="title"> Force Curve </div>`
html`
<div class="title">
Force Curve
<slot></slot>
</div>
`
}
<canvas @click="${this._handleClick}" id="chart"></canvas>
`
Expand Down
14 changes: 7 additions & 7 deletions app/client/components/DashboardMetric.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,21 @@ describe('value display', () => {
})

describe('icon rendering', () => {
test('should use empty class strings when no icon is provided', () => {
test('should use label class when no icon is provided', () => {
const el = new DashboardMetric()
el.icon = ''
const result = (el as any).render()
// When icon is '', the class is '' and font-size style is 200%
expect(result.values).toContain('font-size: 200%;')
// When icon is '', hasIcon is false, so the div class is 'label'
expect(result.values).toContain('label')
})

test('should use label/icon classes when an icon is provided', () => {
test('should use icon class when an icon is provided', () => {
const el = new DashboardMetric()
el.icon = 'some-icon'
const result = (el as any).render()
expect(result.values).toContain('label')
// When icon is not '', hasIcon is true, so the div class is 'icon'
expect(result.values).toContain('icon')
// When icon is not '', there's no inline font-size override
expect(result.values).not.toContain('font-size: 200%;')
// metric-value gets 'with-icon' added via template interpolation
expect(result.values).toContain('with-icon')
})
})
21 changes: 11 additions & 10 deletions app/client/components/DashboardMetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@ export class DashboardMetric extends AppElement {
font-size: 150%;
}

.metric-unit {
font-size: 80%;
.metric-value.with-icon {
font-size: 200%;
}

::slotted(*) {
right: 0.2em;
bottom: 0;
position: absolute;
.metric-unit {
font-size: 80%;
}
`

Expand All @@ -43,15 +41,18 @@ export class DashboardMetric extends AppElement {
value: string | number | undefined

render () {
const hasIcon = this.icon !== ''
return html`
<div class="${this.icon === '' ? '' : 'label'}">
<div class=${this.icon === '' ? '' : 'icon'}>${this.icon}</div>
<div class="${hasIcon ? 'icon' : 'label'}">
${this.icon}
<slot></slot>
</div>
<div class="content">
<span class="metric-value" style="${this.icon === '' ? 'font-size: 200%;' : ''}">${this.value !== undefined ? this.value : '--'}</span>
<span class="metric-value ${hasIcon ? 'with-icon' : ''}">
${this.value !== undefined ? this.value : '--'}
</span>
<span class="metric-unit">${this.unit}</span>
</div>
<slot></slot>
`
}
}
55 changes: 52 additions & 3 deletions app/client/components/DashboardToolbar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ function createToolbar (config: Partial<AppConfig> = {}): DashboardToolbar {
uploadEnabled: false,
shutdownEnabled: false,
guiConfigs: {
dashboardMetrics: [],
landscapeDashboardMetrics: [],
portraitDashboardMetrics: [],
showIcons: true,
maxNumberOfTiles: 8,
trueBlackTheme: false,
forceCurveDivisionMode: 0
forceCurveDivisionMode: 0,
gridConfig: {
landscape: { columns: 4, rows: 2 },
portrait: { columns: 2, rows: 4 }
}
},
...config
}
Expand Down Expand Up @@ -92,3 +96,48 @@ describe('renderOptionalButtons', () => {
expect(buttonsWithoutFullscreen.length).toBe(0)
})
})

describe('retile button style', () => {
test('should default _retileMode to false', () => {
const toolbar = createToolbar()
expect(toolbar._retileMode).toBe(false)
})

test('should include label-button class on the retile toggle button', () => {
const toolbar = createToolbar()

const result = toolbar.render()

const staticParts = (result as unknown as { strings: readonly string[] }).strings.join('')
expect(staticParts).toContain('label-button')
})

test('should include label-button class on the submit button in retile mode', () => {
const toolbar = createToolbar()
toolbar._retileMode = true

const result = toolbar.render()

const staticParts = (result as unknown as { strings: readonly string[] }).strings.join('')
expect(staticParts).toContain('label-button')
})

test('should not include active class on the retile button in normal mode', () => {
const toolbar = createToolbar()

const result = toolbar.render()

const staticParts = (result as unknown as { strings: readonly string[] }).strings.join('')
expect(staticParts).not.toContain('active')
})

test('should not include active class on the submit button in retile mode', () => {
const toolbar = createToolbar()
toolbar._retileMode = true

const result = toolbar.render()

const staticParts = (result as unknown as { strings: readonly string[] }).strings.join('')
expect(staticParts).not.toContain('active')
})
})
47 changes: 43 additions & 4 deletions app/client/components/DashboardToolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import { AppElement, html, css, TemplateResult } from './AppElement'
import { customElement, property, state } from 'lit/decorators.js'
import { iconSettings, iconUndo, iconExpand, iconCompress, iconPoweroff, iconBluetooth, iconUpload, iconHeartbeat, iconAntplus } from '../lib/icons'
import './SettingsDialog'
import './AppDialog'
import type { AppConfig } from '../store/types'

Expand Down Expand Up @@ -51,6 +50,16 @@ export class DashboardToolbar extends AppElement {
filter: brightness(150%);
}

button.active {
background: var(--theme-accent-color, #4a9eff);
filter: brightness(120%);
}

button.label-button {
width: auto;
padding: 0 0.6em;
}

button .text {
position: absolute;
left: 2px;
Expand Down Expand Up @@ -91,12 +100,25 @@ export class DashboardToolbar extends AppElement {
@state()
_dialog?: TemplateResult

@state()
_retileMode = false

render () {
return html`
<div class="button-group">
<button @click=${this.openSettings} title="Settings">
${iconSettings}
</button>
<button @click=${this.toggleRetileMode} title="Retile Dashboard" class="label-button">
${this._retileMode ? 'Submit' : 'Retile'}
</button>
${this._retileMode ?
html`
<button @click=${this.resetToDefault} title="Reset to Default" class="label-button">
Reset
</button>
` :
''}
<button @click=${this.reset} title="Reset">
${iconUndo}
</button>
Expand Down Expand Up @@ -179,9 +201,10 @@ export class DashboardToolbar extends AppElement {
}

openSettings () {
this._dialog = html`<settings-dialog .config=${this.config.guiConfigs} @close=${() => {
this._dialog = undefined
}}></settings-dialog>`
this.dispatchEvent(new CustomEvent('open-settings', {
bubbles: true,
composed: true
}))
}

toggleFullscreen () {
Expand All @@ -199,6 +222,22 @@ export class DashboardToolbar extends AppElement {
this.sendEvent('triggerAction', { command: 'reset' })
}

toggleRetileMode () {
this._retileMode = !this._retileMode
this.dispatchEvent(new CustomEvent('retile-mode-changed', {
bubbles: true,
composed: true,
detail: { active: this._retileMode }
}))
}

resetToDefault () {
this.dispatchEvent(new CustomEvent('reset-layout-to-default', {
bubbles: true,
composed: true
}))
}

switchBlePeripheralMode () {
this.sendEvent('triggerAction', { command: 'switchBlePeripheralMode' })
}
Expand Down
109 changes: 109 additions & 0 deletions app/client/components/PerformanceDashboard.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor

Styles for the PerformanceDashboard component
*/

import { css } from './AppElement'

export const performanceDashboardStyles = css`
:host {
display: grid;
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(var(--grid-columns, 4), 1fr);
grid-template-rows: repeat(var(--grid-rows, 2), 1fr);
min-height: 0; /* prevent grid blowout */
}

/* This should be defined within the component */
dashboard-metric,
dashboard-force-curve {
background: var(--theme-widget-color);
text-align: center;
padding: 0.2em;
border-radius: var(--theme-border-radius);
position: relative;
min-height: 0; /* prevent grid blowout */
}

.retile-controls {
display: flex;
position: relative;
z-index: 10;
}

.retile-select {
font-size: 0.4em;
padding: 4px 8px;
min-width: 80px;
background: var(--theme-button-color);
color: var(--theme-font-color);
cursor: pointer;
border: none;
border-radius: var(--theme-border-radius);
}

.add-tile {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
background: var(--theme-widget-color);
border: 2px dashed var(--theme-font-color);
border-radius: var(--theme-border-radius);
padding: 8px;
min-height: 80px;
max-height: 100%;
overflow-y: auto;
}

.add-tile.empty {
justify-content: center;
align-items: center;
}

.add-tile-message {
font-size: 0.6em;
color: var(--theme-font-color);
opacity: 0.7;
}

.add-tile-option {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
width: 100%;
}

.add-tile-option input[type="radio"] {
cursor: pointer;
width: 1em;
height: 1em;
flex-shrink: 0;
}

.add-tile-option label {
cursor: pointer;
font-size: 0.5em;
-webkit-user-select: none;
user-select: none;
flex: 1;
}
`
Loading
Loading