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
1 change: 1 addition & 0 deletions angular-client/src/app/context/app-context.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default class AppContextComponent implements OnInit {
this.domSanitizer.bypassSecurityTrustResourceUrl('../assets/icons/battery_charging_2.svg')
)
.addSvgIcon('error', this.domSanitizer.bypassSecurityTrustResourceUrl('../assets/icons/alert-triangle.svg'))
.addSvgIcon('battery', this.domSanitizer.bypassSecurityTrustResourceUrl('../assets/icons/battery.svg'))
.addSvgIcon('linked_camera', this.domSanitizer.bypassSecurityTrustResourceUrl('../assets/icons/linked_camera.svg'))
.addSvgIcon('more_horiz', this.domSanitizer.bypassSecurityTrustResourceUrl('../assets/icons/more_horiz.svg'))
.addSvgIcon('edit', this.domSanitizer.bypassSecurityTrustResourceUrl('../assets/icons/edit.svg'));
Expand Down
3 changes: 3 additions & 0 deletions angular-client/src/assets/icons/battery.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
:host {
display: inline-flex;
align-self: center;
flex-shrink: 0;
}

.battery {
display: flex;
flex-direction: column;
align-items: center;
width: 18px;
height: 44px;
}

/* Small nub on top */
.nub {
width: 10px;
height: 4px;
background: var(--color-battery-shell);
border-radius: 2px 2px 0 0;
}

/* Outer shell */
.shell {
width: 18px;
height: 40px;
background: var(--color-battery-shell);
border-radius: 3px;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
padding: 2px;
}

/* Fill bar — grows from bottom */
.fill {
width: 14px;
border-radius: 2px;
transition: height 0.3s ease;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div class="battery">
<div class="nub"></div>
<div class="shell">
<div class="fill" [style.height]="fillHeight" [style.background-color]="fillColor"></div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Component, Input, OnChanges } from '@angular/core';
import Theme from 'src/services/theme.service';

/**
* Compact battery widget sized for the At A Glance bar.
* Fixed 18×44px footprint. Self-contained — no external wrapper needed.
*/
@Component({
selector: 'glance-battery',
templateUrl: './glance-battery.component.html',
styleUrl: './glance-battery.component.css',
standalone: true
})
export class GlanceBatteryComponent implements OnChanges {
@Input() percentage: number = 0;

fillHeight = '0%';
fillColor = Theme.battteryHigh;

ngOnChanges(): void {
this.fillHeight = Math.max(0, Math.min(100, this.percentage)) + '%';
this.fillColor = this.percentage <= 20 ? Theme.battteryLow : Theme.battteryHigh;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
:host {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
}

/* ---- Header row (Cell/Chip label) ---- */
.header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
white-space: nowrap;
}

.header-icon {
width: 13px;
height: 17px;
flex-shrink: 0;
}

.header-label {
font-family: var(--font-family);
font-size: var(--font-size-sm);
font-weight: 400;
color: var(--color-text-primary);
white-space: nowrap;
}

/* ---- Value + Unit row ---- */
.value-row {
display: flex;
align-items: baseline;
gap: 2px;
white-space: nowrap;
flex-wrap: nowrap;
}

:host(.unit-below-mode) .value-row {
align-items: flex-end;
gap: 6px;
}

.value {
font-family: var(--font-family);
font-size: var(--font-size-xl);
font-weight: 800;
color: var(--color-text-primary);
line-height: 1;
margin: 0;
}

.unit {
font-family: var(--font-family);
font-size: var(--font-size-md);
font-weight: 700;
color: var(--color-text-secondary);
line-height: 1;
align-self: flex-start;
padding-top: 6px;
}

.unit-below {
align-self: flex-end;
padding-top: 0;
padding-bottom: 2px;
font-size: var(--font-size-sm);
}

/* ---- Subtitle ---- */
.subtitle {
font-family: var(--font-family);
font-size: var(--font-size-sm);
font-weight: 700;
color: var(--color-text-subtitle);
text-align: center;
margin-top: 2px;
white-space: nowrap;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@if (headerLabel()) {
<div class="header">
@if (headerIcon()) {
<mat-icon aria-hidden="false" [svgIcon]="headerIcon()" class="header-icon" />
}
<span class="header-label">{{ headerLabel() }}</span>
</div>
}

<div class="value-row">
<span class="value">{{ formattedValue }}</span>
<span class="unit" [class.unit-below]="unitBelow()">{{ unit() }}</span>
<ng-content />
</div>

@if (subtitle()) {
<span class="subtitle">{{ subtitle() }}</span>
}
37 changes: 37 additions & 0 deletions angular-client/src/components/glance-stat/glance-stat.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Component, input, OnChanges } from '@angular/core';
import { MatIcon } from '@angular/material/icon';

/**
* Lightweight stat display component designed for the At A Glance bar.
* Renders value + unit + subtitle with optional header label and widget slot.
* Uses pure CSS — no nested hstack/vstack/typography wrappers.
*/
@Component({
selector: 'glance-stat',
templateUrl: './glance-stat.component.html',
styleUrl: './glance-stat.component.css',
standalone: true,
imports: [MatIcon],
host: {
'[class.unit-below-mode]': 'unitBelow()'
}
})
export class GlanceStatComponent implements OnChanges {
value = input<number>();
unit = input<string>('');
subtitle = input<string>('');
precision = input<number>(1);
/** Optional header label (e.g. "Cell: 114 | Chip: A") */
headerLabel = input<string>('');
/** SVG icon name to show in header (registered via matIconRegistry) */
headerIcon = input<string>('');
/** When true, render the unit below the value instead of inline */
unitBelow = input<boolean>(false);

formattedValue = '-';

ngOnChanges(): void {
const val = this.value();
this.formattedValue = (val?.toFixed(this.precision()) ?? '-') + (this.unit() === 'C' ? '°' : '');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
:host {
display: inline-flex;
align-self: center;
flex-shrink: 0;
}

.thermo {
display: flex;
flex-direction: column;
align-items: center;
width: 24px;
height: 58px;
position: relative;
}

/* Glass tube */
.tube {
position: relative;
width: 12px;
height: 36px;
border-radius: 6px 6px 0 0;
background: var(--color-widget-glass);
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-end;
}

.glass {
position: absolute;
inset: 0;
border-radius: inherit;
}

/* Mercury fill — grows from bottom */
.mercury {
width: 7px;
margin: 0 auto;
border-radius: 4px 4px 0 0;
min-height: 2px;
transition: height 0.3s ease;
}

/* Bulb at bottom */
.bulb {
width: 20px;
height: 20px;
border-radius: 50%;
margin-top: -3px;
border: 3px solid var(--color-widget-glass);
box-sizing: border-box;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="thermo">
<div class="tube">
<div class="glass"></div>
<div class="mercury" [style.height]="mercuryHeight" [style.background-color]="mercuryColor"></div>
</div>
<div class="bulb" [style.background-color]="mercuryColor"></div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Component, Input } from '@angular/core';

/**
* Compact thermometer widget sized for the At A Glance bar.
* Self-contained — no external sizing wrapper needed.
*/
@Component({
selector: 'glance-thermometer',
templateUrl: './glance-thermometer.component.html',
styleUrl: './glance-thermometer.component.css',
standalone: true
})
export class GlanceThermometerComponent {
@Input() temperature: number = 0;
@Input() min: number = 0;
@Input() max: number = 100;

get mercuryHeight(): string {
const clamped = Math.max(this.min, Math.min(this.temperature, this.max));
const pct = ((clamped - this.min) / (this.max - this.min)) * 100;
// Mercury fills 0–70% of the tube area (leaving room for glass top)
return Math.max(5, pct * 0.7) + '%';
}

get mercuryColor(): string {
const range = this.max - this.min;
if (this.temperature < this.min + range / 2) return '#3b82f6';
if (this.temperature < this.min + range / 1.5) return '#eab308';
return '#ef4444';
}
}
35 changes: 35 additions & 0 deletions angular-client/src/components/info-panel/info-panel.component.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.panel {
border-radius: var(--border-radius-panel);
box-sizing: border-box;
}

.panel.full {
width: 100%;
}

.panel.fit {
width: fit-content;
}

/* ---- Title bar ---- */
.title-bar {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 0;
}

.title-icon {
color: white;
font-size: 18px;
width: 18px;
height: 18px;
}

.title-text {
font-family: var(--font-family);
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-text-primary);
white-space: nowrap;
}
20 changes: 20 additions & 0 deletions angular-client/src/components/info-panel/info-panel.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div
class="panel"
[class.full]="widthMode() === 'full'"
[class.fit]="widthMode() === 'fit'"
[style.padding]="padding()"
[style.background-color]="backgroundColor()"
>
@if (title()) {
<div class="title-bar">
@if (icon()) {
<mat-icon aria-hidden="false" [fontIcon]="icon()" class="title-icon" />
}
@if (svgIcon()) {
<mat-icon aria-hidden="false" [svgIcon]="svgIcon()" class="title-icon" />
}
<span class="title-text">{{ title() }}</span>
</div>
}
<ng-content />
</div>
Loading