ember-mug 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +13 -0
- package/dist/components/App.d.ts +2 -0
- package/dist/components/App.js +120 -0
- package/dist/components/BatteryDisplay.d.ts +9 -0
- package/dist/components/BatteryDisplay.js +25 -0
- package/dist/components/ColorControl.d.ts +9 -0
- package/dist/components/ColorControl.js +71 -0
- package/dist/components/ConnectionStatus.d.ts +10 -0
- package/dist/components/ConnectionStatus.js +9 -0
- package/dist/components/Header.d.ts +7 -0
- package/dist/components/Header.js +5 -0
- package/dist/components/HelpDisplay.d.ts +6 -0
- package/dist/components/HelpDisplay.js +5 -0
- package/dist/components/Presets.d.ts +11 -0
- package/dist/components/Presets.js +19 -0
- package/dist/components/SettingsView.d.ts +11 -0
- package/dist/components/SettingsView.js +34 -0
- package/dist/components/TemperatureControl.d.ts +10 -0
- package/dist/components/TemperatureControl.js +38 -0
- package/dist/components/TemperatureDisplay.d.ts +10 -0
- package/dist/components/TemperatureDisplay.js +40 -0
- package/dist/hooks/useMug.d.ts +14 -0
- package/dist/hooks/useMug.js +112 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/bluetooth.d.ts +43 -0
- package/dist/lib/bluetooth.js +334 -0
- package/dist/lib/settings.d.ts +28 -0
- package/dist/lib/settings.js +73 -0
- package/dist/lib/types.d.ts +57 -0
- package/dist/lib/types.js +34 -0
- package/dist/lib/utils.d.ts +20 -0
- package/dist/lib/utils.js +152 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Ember Mug CLI
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ember-mug)
|
|
4
|
+
[](https://github.com/singerbj/ember-cli/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
A TypeScript CLI application for controlling Ember mugs, built with [Ink](https://github.com/vadimdemedes/ink) (React for CLIs).
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
### Global Installation (Recommended)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g ember-mug
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
ember-mug
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Run with npx (No Installation)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx ember-mug
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Local Development
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/singerbj/ember-cli.git
|
|
32
|
+
cd ember-cli
|
|
33
|
+
npm install
|
|
34
|
+
npm run dev
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Temperature Control**: View current temperature and adjust target temperature
|
|
40
|
+
- **Temperature Presets**: Quick-select from predefined temperature presets (Latte, Coffee, Tea)
|
|
41
|
+
- **Battery Monitoring**: Real-time battery level with estimated battery life
|
|
42
|
+
- **Time Estimates**:
|
|
43
|
+
- Estimated time to reach target temperature
|
|
44
|
+
- Estimated battery life based on current mug state
|
|
45
|
+
- **LED Color Control**: Customize your mug's LED color with presets or custom RGB values
|
|
46
|
+
- **Temperature Unit Toggle**: Switch between Celsius and Fahrenheit
|
|
47
|
+
- **Persistent Settings**: Your preferences are saved between sessions
|
|
48
|
+
|
|
49
|
+
## Controls
|
|
50
|
+
|
|
51
|
+
### When Disconnected
|
|
52
|
+
- `s` - Start scanning for Ember mug
|
|
53
|
+
- `r` - Retry scanning (after error)
|
|
54
|
+
- `q` - Quit
|
|
55
|
+
|
|
56
|
+
### When Connected
|
|
57
|
+
- `t` - Enter temperature adjustment mode
|
|
58
|
+
- `←/→` or `h/l` - Adjust by ±0.5°
|
|
59
|
+
- `↑/↓` or `j/k` - Adjust by ±1°
|
|
60
|
+
- `t` or `Enter` - Exit temperature mode
|
|
61
|
+
- `1-3` - Select temperature preset
|
|
62
|
+
- `c` - Toggle LED color control
|
|
63
|
+
- `1-8` - Select preset color
|
|
64
|
+
- `c` - Toggle custom color mode
|
|
65
|
+
- `r/g/b` - Select color channel (in custom mode)
|
|
66
|
+
- `←/→` - Adjust selected channel
|
|
67
|
+
- `u` - Toggle temperature unit (°C/°F)
|
|
68
|
+
- `o` - Open settings
|
|
69
|
+
- `q` - Quit
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
- Node.js 18+
|
|
74
|
+
- Bluetooth adapter with BLE support
|
|
75
|
+
- Ember Mug 2 (other models may work but are untested)
|
|
76
|
+
|
|
77
|
+
### Platform-Specific Notes
|
|
78
|
+
|
|
79
|
+
#### Linux
|
|
80
|
+
You may need to grant Bluetooth permissions:
|
|
81
|
+
```bash
|
|
82
|
+
sudo setcap cap_net_raw+eip $(eval readlink -f `which node`)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### macOS
|
|
86
|
+
Grant Bluetooth permissions to your terminal application in System Preferences > Security & Privacy > Privacy > Bluetooth.
|
|
87
|
+
|
|
88
|
+
#### Windows
|
|
89
|
+
Requires Windows 10 build 15063 or later with Bluetooth 4.0+ adapter.
|
|
90
|
+
|
|
91
|
+
## Technical Details
|
|
92
|
+
|
|
93
|
+
This application uses:
|
|
94
|
+
- [@abandonware/noble](https://github.com/abandonware/noble) for Bluetooth LE communication
|
|
95
|
+
- [Ink](https://github.com/vadimdemedes/ink) for the React-based CLI interface
|
|
96
|
+
- [Conf](https://github.com/sindresorhus/conf) for persistent settings storage
|
|
97
|
+
|
|
98
|
+
The Ember mug Bluetooth protocol was reverse-engineered by [orlopau/ember-mug](https://github.com/orlopau/ember-mug).
|
|
99
|
+
|
|
100
|
+
## Versioning
|
|
101
|
+
|
|
102
|
+
This project uses [Semantic Versioning](https://semver.org/). Releases are published to npm automatically when a new version tag is pushed to the main branch.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import { App } from './components/App.js';
|
|
5
|
+
// Handle graceful shutdown
|
|
6
|
+
process.on('SIGINT', () => {
|
|
7
|
+
process.exit(0);
|
|
8
|
+
});
|
|
9
|
+
process.on('SIGTERM', () => {
|
|
10
|
+
process.exit(0);
|
|
11
|
+
});
|
|
12
|
+
// Render the app
|
|
13
|
+
render(_jsx(App, {}));
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { Box, useApp, useInput } from 'ink';
|
|
4
|
+
import { useMug } from '../hooks/useMug.js';
|
|
5
|
+
import { Header } from './Header.js';
|
|
6
|
+
import { TemperatureDisplay } from './TemperatureDisplay.js';
|
|
7
|
+
import { BatteryDisplay } from './BatteryDisplay.js';
|
|
8
|
+
import { TemperatureControl } from './TemperatureControl.js';
|
|
9
|
+
import { Presets } from './Presets.js';
|
|
10
|
+
import { ColorControl } from './ColorControl.js';
|
|
11
|
+
import { ConnectionStatus } from './ConnectionStatus.js';
|
|
12
|
+
import { SettingsView } from './SettingsView.js';
|
|
13
|
+
import { HelpDisplay } from './HelpDisplay.js';
|
|
14
|
+
import { TemperatureUnit } from '../lib/types.js';
|
|
15
|
+
import { getPresets, getTemperatureUnit, setTemperatureUnit as saveTemperatureUnit, setLastTargetTemp, } from '../lib/settings.js';
|
|
16
|
+
export function App() {
|
|
17
|
+
const { exit } = useApp();
|
|
18
|
+
const { state: mugState, isScanning, error, foundMugName, startScanning, setTargetTemp, setTemperatureUnit: setMugTempUnit, setLedColor, } = useMug();
|
|
19
|
+
const [viewMode, setViewMode] = useState('main');
|
|
20
|
+
const [activeControl, setActiveControl] = useState('none');
|
|
21
|
+
const [presets] = useState(getPresets());
|
|
22
|
+
const [selectedPresetIndex, setSelectedPresetIndex] = useState(-1);
|
|
23
|
+
const [localTempUnit, setLocalTempUnit] = useState(getTemperatureUnit());
|
|
24
|
+
// Start scanning on mount
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
startScanning();
|
|
27
|
+
}, [startScanning]);
|
|
28
|
+
// Sync temperature unit with mug when connected
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (mugState.connected) {
|
|
31
|
+
setLocalTempUnit(mugState.temperatureUnit);
|
|
32
|
+
}
|
|
33
|
+
}, [mugState.connected, mugState.temperatureUnit]);
|
|
34
|
+
const handleTempChange = useCallback(async (temp) => {
|
|
35
|
+
await setTargetTemp(temp);
|
|
36
|
+
setLastTargetTemp(temp);
|
|
37
|
+
setSelectedPresetIndex(-1);
|
|
38
|
+
}, [setTargetTemp]);
|
|
39
|
+
const handlePresetSelect = useCallback(async (preset) => {
|
|
40
|
+
await setTargetTemp(preset.temperature);
|
|
41
|
+
setLastTargetTemp(preset.temperature);
|
|
42
|
+
const index = presets.findIndex((p) => p.id === preset.id);
|
|
43
|
+
setSelectedPresetIndex(index);
|
|
44
|
+
}, [setTargetTemp, presets]);
|
|
45
|
+
const handleColorChange = useCallback(async (color) => {
|
|
46
|
+
await setLedColor(color);
|
|
47
|
+
}, [setLedColor]);
|
|
48
|
+
const handleTemperatureUnitChange = useCallback(async (unit) => {
|
|
49
|
+
setLocalTempUnit(unit);
|
|
50
|
+
saveTemperatureUnit(unit);
|
|
51
|
+
if (mugState.connected) {
|
|
52
|
+
await setMugTempUnit(unit);
|
|
53
|
+
}
|
|
54
|
+
}, [mugState.connected, setMugTempUnit]);
|
|
55
|
+
useInput((input, key) => {
|
|
56
|
+
// Global controls
|
|
57
|
+
if (input === 'q' && viewMode === 'main' && activeControl === 'none') {
|
|
58
|
+
exit();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Handle escape to go back
|
|
62
|
+
if (key.escape) {
|
|
63
|
+
if (activeControl !== 'none') {
|
|
64
|
+
setActiveControl('none');
|
|
65
|
+
}
|
|
66
|
+
else if (viewMode !== 'main') {
|
|
67
|
+
setViewMode('main');
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Only handle these if we're in main view with no active control
|
|
72
|
+
if (viewMode === 'main' && activeControl === 'none') {
|
|
73
|
+
if (input === 's' && !mugState.connected) {
|
|
74
|
+
startScanning();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (input === 'r' && !mugState.connected && error) {
|
|
78
|
+
startScanning();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (mugState.connected) {
|
|
82
|
+
if (input === 't') {
|
|
83
|
+
setActiveControl('temperature');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (input === 'c') {
|
|
87
|
+
setActiveControl('color');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (input === 'o') {
|
|
91
|
+
setViewMode('settings');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (input === 'u') {
|
|
95
|
+
const newUnit = localTempUnit === TemperatureUnit.Celsius
|
|
96
|
+
? TemperatureUnit.Fahrenheit
|
|
97
|
+
: TemperatureUnit.Celsius;
|
|
98
|
+
handleTemperatureUnitChange(newUnit);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Exit temperature control mode
|
|
104
|
+
if (activeControl === 'temperature' && (key.return || input === 't')) {
|
|
105
|
+
setActiveControl('none');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Exit color control mode
|
|
109
|
+
if (activeControl === 'color' && input === 'c') {
|
|
110
|
+
setActiveControl('none');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
// Settings view
|
|
115
|
+
if (viewMode === 'settings') {
|
|
116
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { mugName: mugState.mugName, connected: mugState.connected }), _jsx(SettingsView, { presets: presets, temperatureUnit: localTempUnit, onTemperatureUnitChange: handleTemperatureUnitChange, onClose: () => setViewMode('main'), isActive: true })] }));
|
|
117
|
+
}
|
|
118
|
+
// Main view
|
|
119
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { mugName: mugState.mugName, connected: mugState.connected }), !mugState.connected ? (_jsx(ConnectionStatus, { isScanning: isScanning, isConnected: mugState.connected, foundMugName: foundMugName, error: error, onRetry: startScanning })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(TemperatureDisplay, { currentTemp: mugState.currentTemp, targetTemp: mugState.targetTemp, liquidState: mugState.liquidState, temperatureUnit: localTempUnit }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(BatteryDisplay, { batteryLevel: mugState.batteryLevel, isCharging: mugState.isCharging, liquidState: mugState.liquidState }) })] }), _jsx(TemperatureControl, { targetTemp: mugState.targetTemp, temperatureUnit: localTempUnit, onTempChange: handleTempChange, isActive: activeControl === 'temperature' }), _jsx(Presets, { presets: presets, selectedIndex: selectedPresetIndex, temperatureUnit: localTempUnit, onSelect: handlePresetSelect, isActive: activeControl === 'none' }), activeControl === 'color' && (_jsx(ColorControl, { color: mugState.color, onColorChange: handleColorChange, isActive: true }))] })), _jsx(HelpDisplay, { isConnected: mugState.connected })] }));
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LiquidState } from '../lib/types.js';
|
|
3
|
+
interface BatteryDisplayProps {
|
|
4
|
+
batteryLevel: number;
|
|
5
|
+
isCharging: boolean;
|
|
6
|
+
liquidState: LiquidState;
|
|
7
|
+
}
|
|
8
|
+
export declare function BatteryDisplay({ batteryLevel, isCharging, liquidState, }: BatteryDisplayProps): React.ReactElement;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { formatBatteryLevel, getBatteryIcon, estimateBatteryLife, formatDuration, } from '../lib/utils.js';
|
|
4
|
+
export function BatteryDisplay({ batteryLevel, isCharging, liquidState, }) {
|
|
5
|
+
const batteryColor = getBatteryColor(batteryLevel, isCharging);
|
|
6
|
+
const batteryIcon = getBatteryIcon(batteryLevel, isCharging);
|
|
7
|
+
const batteryTimeEstimate = estimateBatteryLife(batteryLevel, isCharging, liquidState);
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { justifyContent: "center", children: _jsx(Text, { bold: true, children: "Battery" }) }), _jsx(Box, { justifyContent: "center", marginY: 1, children: _jsxs(Text, { color: batteryColor, children: [batteryIcon, " ", formatBatteryLevel(batteryLevel), isCharging && _jsx(Text, { color: "yellow", children: " (Charging)" })] }) }), batteryTimeEstimate !== null && (_jsx(Box, { justifyContent: "center", children: _jsx(Text, { dimColor: true, children: isCharging ? (_jsxs(_Fragment, { children: ['Time to full: ', _jsx(Text, { color: "green", children: formatDuration(batteryTimeEstimate) })] })) : (_jsxs(_Fragment, { children: ['Est. battery life: ', _jsx(Text, { color: batteryLevel < 20 ? 'red' : 'yellow', children: formatDuration(batteryTimeEstimate) })] })) }) })), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(BatteryBar, { level: batteryLevel, isCharging: isCharging }) })] }));
|
|
9
|
+
}
|
|
10
|
+
function BatteryBar({ level, isCharging }) {
|
|
11
|
+
const totalSegments = 20;
|
|
12
|
+
const filledSegments = Math.round((level / 100) * totalSegments);
|
|
13
|
+
const emptySegments = totalSegments - filledSegments;
|
|
14
|
+
const color = getBatteryColor(level, isCharging);
|
|
15
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "[" }), _jsx(Text, { color: color, children: '█'.repeat(filledSegments) }), _jsx(Text, { dimColor: true, children: '░'.repeat(emptySegments) }), _jsx(Text, { dimColor: true, children: "]" }), isCharging && _jsx(Text, { color: "yellow", children: " \u26A1" })] }));
|
|
16
|
+
}
|
|
17
|
+
function getBatteryColor(level, isCharging) {
|
|
18
|
+
if (isCharging)
|
|
19
|
+
return 'yellow';
|
|
20
|
+
if (level >= 50)
|
|
21
|
+
return 'green';
|
|
22
|
+
if (level >= 25)
|
|
23
|
+
return 'yellow';
|
|
24
|
+
return 'red';
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { RGBColor } from '../lib/types.js';
|
|
3
|
+
interface ColorControlProps {
|
|
4
|
+
color: RGBColor;
|
|
5
|
+
onColorChange: (color: RGBColor) => void;
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function ColorControl({ color, onColorChange, isActive, }: ColorControlProps): React.ReactElement;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { rgbToHex } from '../lib/utils.js';
|
|
5
|
+
const PRESET_COLORS = [
|
|
6
|
+
{ name: 'Orange', color: { r: 255, g: 147, b: 41, a: 255 } },
|
|
7
|
+
{ name: 'Red', color: { r: 255, g: 0, b: 0, a: 255 } },
|
|
8
|
+
{ name: 'Green', color: { r: 0, g: 255, b: 0, a: 255 } },
|
|
9
|
+
{ name: 'Blue', color: { r: 0, g: 128, b: 255, a: 255 } },
|
|
10
|
+
{ name: 'Purple', color: { r: 128, g: 0, b: 255, a: 255 } },
|
|
11
|
+
{ name: 'Pink', color: { r: 255, g: 105, b: 180, a: 255 } },
|
|
12
|
+
{ name: 'White', color: { r: 255, g: 255, b: 255, a: 255 } },
|
|
13
|
+
{ name: 'Teal', color: { r: 0, g: 255, b: 200, a: 255 } },
|
|
14
|
+
];
|
|
15
|
+
export function ColorControl({ color, onColorChange, isActive, }) {
|
|
16
|
+
const [selectedChannel, setSelectedChannel] = useState('r');
|
|
17
|
+
const [customMode, setCustomMode] = useState(false);
|
|
18
|
+
useInput((input, key) => {
|
|
19
|
+
if (!isActive)
|
|
20
|
+
return;
|
|
21
|
+
// Number keys for preset colors
|
|
22
|
+
const numKey = parseInt(input, 10);
|
|
23
|
+
if (numKey >= 1 && numKey <= PRESET_COLORS.length) {
|
|
24
|
+
onColorChange(PRESET_COLORS[numKey - 1].color);
|
|
25
|
+
setCustomMode(false);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Toggle custom mode
|
|
29
|
+
if (input === 'c') {
|
|
30
|
+
setCustomMode(!customMode);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (customMode) {
|
|
34
|
+
// Switch channels
|
|
35
|
+
if (input === 'r') {
|
|
36
|
+
setSelectedChannel('r');
|
|
37
|
+
}
|
|
38
|
+
else if (input === 'g') {
|
|
39
|
+
setSelectedChannel('g');
|
|
40
|
+
}
|
|
41
|
+
else if (input === 'b') {
|
|
42
|
+
setSelectedChannel('b');
|
|
43
|
+
}
|
|
44
|
+
// Adjust selected channel
|
|
45
|
+
let delta = 0;
|
|
46
|
+
if (key.leftArrow || input === 'h') {
|
|
47
|
+
delta = -10;
|
|
48
|
+
}
|
|
49
|
+
else if (key.rightArrow || input === 'l') {
|
|
50
|
+
delta = 10;
|
|
51
|
+
}
|
|
52
|
+
else if (key.upArrow || input === 'k') {
|
|
53
|
+
delta = 25;
|
|
54
|
+
}
|
|
55
|
+
else if (key.downArrow || input === 'j') {
|
|
56
|
+
delta = -25;
|
|
57
|
+
}
|
|
58
|
+
if (delta !== 0) {
|
|
59
|
+
const newColor = { ...color };
|
|
60
|
+
newColor[selectedChannel] = Math.max(0, Math.min(255, newColor[selectedChannel] + delta));
|
|
61
|
+
onColorChange(newColor);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, { isActive });
|
|
65
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { justifyContent: "center", children: _jsx(Text, { bold: true, children: "LED Color" }) }), _jsx(Box, { justifyContent: "center", marginY: 1, children: _jsxs(Text, { children: ["Current: ", _jsx(Text, { color: rgbToHex(color.r, color.g, color.b), children: "\u25CF" }), ' ', _jsxs(Text, { dimColor: true, children: ["(", rgbToHex(color.r, color.g, color.b), ")"] })] }) }), _jsx(Box, { justifyContent: "center", gap: 1, marginY: 1, children: PRESET_COLORS.map((preset, index) => (_jsxs(Box, { children: [_jsx(Text, { color: rgbToHex(preset.color.r, preset.color.g, preset.color.b), children: "\u25CF" }), _jsx(Text, { dimColor: true, children: index + 1 })] }, preset.name))) }), customMode && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "Custom Color Mode" }) }), _jsxs(Box, { justifyContent: "center", gap: 2, children: [_jsx(ChannelSlider, { label: "R", value: color.r, isSelected: selectedChannel === 'r', color: "red" }), _jsx(ChannelSlider, { label: "G", value: color.g, isSelected: selectedChannel === 'g', color: "green" }), _jsx(ChannelSlider, { label: "B", value: color.b, isSelected: selectedChannel === 'b', color: "blue" })] })] })), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press ", _jsxs(Text, { color: "cyan", children: ["1-", PRESET_COLORS.length] }), " for presets |", ' ', _jsx(Text, { color: "cyan", children: "c" }), " for custom mode"] }) })] }));
|
|
66
|
+
}
|
|
67
|
+
function ChannelSlider({ label, value, isSelected, color, }) {
|
|
68
|
+
const width = 10;
|
|
69
|
+
const filled = Math.round((value / 255) * width);
|
|
70
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? color : 'gray', children: [label, ": ", '█'.repeat(filled), '░'.repeat(width - filled), " ", value.toString().padStart(3)] }) }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ConnectionStatusProps {
|
|
3
|
+
isScanning: boolean;
|
|
4
|
+
isConnected: boolean;
|
|
5
|
+
foundMugName: string | null;
|
|
6
|
+
error: string | null;
|
|
7
|
+
onRetry: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function ConnectionStatus({ isScanning, isConnected, foundMugName, error, }: ConnectionStatusProps): React.ReactElement;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
export function ConnectionStatus({ isScanning, isConnected, foundMugName, error, }) {
|
|
5
|
+
if (isConnected) {
|
|
6
|
+
return _jsx(_Fragment, {});
|
|
7
|
+
}
|
|
8
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", marginY: 2, children: error ? (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press ", _jsx(Text, { color: "cyan", children: "r" }), " to retry scanning"] }) })] })) : isScanning ? (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Scanning for Ember mug..." })] }), foundMugName && (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "green", children: ["Found: ", foundMugName] }), _jsx(Text, { children: " - Connecting..." })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Make sure your Ember mug is powered on and in range" }) })] })) : (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { children: "No Ember mug connected" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press ", _jsx(Text, { color: "cyan", children: "s" }), " to start scanning"] }) })] })) }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function Header({ mugName, connected }) {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: '☕ Ember Mug CLI ' }), connected ? (_jsxs(Text, { color: "green", children: ["[Connected: ", mugName, "]"] })) : (_jsx(Text, { color: "yellow", children: "[Disconnected]" }))] }), _jsx(Text, { dimColor: true, children: '─'.repeat(50) })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function HelpDisplay({ isConnected }) {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '─'.repeat(50) }), _jsx(Box, { marginY: 1, justifyContent: "center", children: _jsx(Text, { dimColor: true, children: isConnected ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", children: "t" }), ":temp", ' ', _jsx(Text, { color: "cyan", children: "1-3" }), ":presets", ' ', _jsx(Text, { color: "cyan", children: "c" }), ":color", ' ', _jsx(Text, { color: "cyan", children: "u" }), ":unit", ' ', _jsx(Text, { color: "cyan", children: "o" }), ":settings", ' ', _jsx(Text, { color: "cyan", children: "q" }), ":quit"] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", children: "s" }), ":scan", ' ', _jsx(Text, { color: "cyan", children: "r" }), ":retry", ' ', _jsx(Text, { color: "cyan", children: "q" }), ":quit"] })) }) })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Preset, TemperatureUnit } from '../lib/types.js';
|
|
3
|
+
interface PresetsProps {
|
|
4
|
+
presets: Preset[];
|
|
5
|
+
selectedIndex: number;
|
|
6
|
+
temperatureUnit: TemperatureUnit;
|
|
7
|
+
onSelect: (preset: Preset) => void;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function Presets({ presets, selectedIndex, temperatureUnit, onSelect, isActive, }: PresetsProps): React.ReactElement;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { formatTemperature } from '../lib/utils.js';
|
|
4
|
+
export function Presets({ presets, selectedIndex, temperatureUnit, onSelect, isActive, }) {
|
|
5
|
+
useInput((input, key) => {
|
|
6
|
+
if (!isActive)
|
|
7
|
+
return;
|
|
8
|
+
const numKey = parseInt(input, 10);
|
|
9
|
+
if (numKey >= 1 && numKey <= presets.length) {
|
|
10
|
+
onSelect(presets[numKey - 1]);
|
|
11
|
+
}
|
|
12
|
+
}, { isActive });
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { justifyContent: "center", children: _jsx(Text, { bold: true, children: "Presets" }) }), _jsx(Box, { justifyContent: "center", marginY: 1, gap: 2, children: presets.map((preset, index) => (_jsx(PresetButton, { preset: preset, index: index, isSelected: selectedIndex === index, temperatureUnit: temperatureUnit }, preset.id))) }), _jsx(Box, { justifyContent: "center", children: _jsxs(Text, { dimColor: true, children: ["Press ", _jsxs(Text, { color: "cyan", children: ["1-", presets.length] }), " to select a preset"] }) })] }));
|
|
14
|
+
}
|
|
15
|
+
function PresetButton({ preset, index, isSelected, temperatureUnit, }) {
|
|
16
|
+
const borderColor = isSelected ? 'cyan' : 'gray';
|
|
17
|
+
const textColor = isSelected ? 'cyan' : 'white';
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { color: borderColor, children: isSelected ? '┌───────────┐' : '┌───────────┐' }), _jsxs(Text, { color: borderColor, children: ["\u2502 ", _jsxs(Text, { color: textColor, children: [preset.icon, " ", preset.name.padEnd(6)] }), " \u2502"] }), _jsxs(Text, { color: borderColor, children: ["\u2502 ", _jsx(Text, { color: textColor, children: formatTemperature(preset.temperature, temperatureUnit).padStart(7) }), " \u2502"] }), _jsx(Text, { color: borderColor, children: "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518" }), _jsxs(Text, { color: "cyan", children: ["[", index + 1, "]"] })] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TemperatureUnit, Preset } from '../lib/types.js';
|
|
3
|
+
interface SettingsViewProps {
|
|
4
|
+
presets: Preset[];
|
|
5
|
+
temperatureUnit: TemperatureUnit;
|
|
6
|
+
onTemperatureUnitChange: (unit: TemperatureUnit) => void;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function SettingsView({ presets, temperatureUnit, onTemperatureUnitChange, onClose, isActive, }: SettingsViewProps): React.ReactElement;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { TemperatureUnit } from '../lib/types.js';
|
|
5
|
+
import { formatTemperature } from '../lib/utils.js';
|
|
6
|
+
export function SettingsView({ presets, temperatureUnit, onTemperatureUnitChange, onClose, isActive, }) {
|
|
7
|
+
const [selectedOption, setSelectedOption] = useState('unit');
|
|
8
|
+
useInput((input, key) => {
|
|
9
|
+
if (!isActive)
|
|
10
|
+
return;
|
|
11
|
+
if (key.escape || input === 'q') {
|
|
12
|
+
onClose();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (key.upArrow || input === 'k') {
|
|
16
|
+
setSelectedOption('unit');
|
|
17
|
+
}
|
|
18
|
+
else if (key.downArrow || input === 'j') {
|
|
19
|
+
setSelectedOption('presets');
|
|
20
|
+
}
|
|
21
|
+
if (key.return || input === ' ') {
|
|
22
|
+
if (selectedOption === 'unit') {
|
|
23
|
+
const newUnit = temperatureUnit === TemperatureUnit.Celsius
|
|
24
|
+
? TemperatureUnit.Fahrenheit
|
|
25
|
+
: TemperatureUnit.Celsius;
|
|
26
|
+
onTemperatureUnitChange(newUnit);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}, { isActive });
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Settings" }) }), _jsx(Text, { dimColor: true, children: '─'.repeat(40) }), _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(SettingRow, { label: "Temperature Unit", value: temperatureUnit === TemperatureUnit.Celsius ? 'Celsius (°C)' : 'Fahrenheit (°F)', isSelected: selectedOption === 'unit', hint: "Press Enter to toggle" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "Presets:" }) }), presets.map((preset, index) => (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [index + 1, ". ", preset.icon, " ", preset.name, ":", ' ', formatTemperature(preset.temperature, temperatureUnit)] }) }, preset.id)))] }), _jsx(Text, { dimColor: true, children: '─'.repeat(40) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press ", _jsx(Text, { color: "cyan", children: "Esc" }), " or ", _jsx(Text, { color: "cyan", children: "q" }), " to close settings"] }) })] }));
|
|
31
|
+
}
|
|
32
|
+
function SettingRow({ label, value, isSelected, hint, }) {
|
|
33
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? 'cyan' : 'white', children: [isSelected ? '> ' : ' ', label, ": ", _jsx(Text, { bold: true, children: value }), hint && isSelected && _jsxs(Text, { dimColor: true, children: [" (", hint, ")"] })] }) }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TemperatureUnit } from '../lib/types.js';
|
|
3
|
+
interface TemperatureControlProps {
|
|
4
|
+
targetTemp: number;
|
|
5
|
+
temperatureUnit: TemperatureUnit;
|
|
6
|
+
onTempChange: (temp: number) => void;
|
|
7
|
+
isActive: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function TemperatureControl({ targetTemp, temperatureUnit, onTempChange, isActive, }: TemperatureControlProps): React.ReactElement;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { MIN_TEMP_CELSIUS, MAX_TEMP_CELSIUS } from '../lib/types.js';
|
|
4
|
+
import { formatTemperature, clampTemperature } from '../lib/utils.js';
|
|
5
|
+
export function TemperatureControl({ targetTemp, temperatureUnit, onTempChange, isActive, }) {
|
|
6
|
+
useInput((input, key) => {
|
|
7
|
+
if (!isActive)
|
|
8
|
+
return;
|
|
9
|
+
let delta = 0;
|
|
10
|
+
if (key.leftArrow || input === 'h' || input === '-') {
|
|
11
|
+
delta = -0.5;
|
|
12
|
+
}
|
|
13
|
+
else if (key.rightArrow || input === 'l' || input === '+' || input === '=') {
|
|
14
|
+
delta = 0.5;
|
|
15
|
+
}
|
|
16
|
+
else if (key.upArrow || input === 'k') {
|
|
17
|
+
delta = 1;
|
|
18
|
+
}
|
|
19
|
+
else if (key.downArrow || input === 'j') {
|
|
20
|
+
delta = -1;
|
|
21
|
+
}
|
|
22
|
+
if (delta !== 0) {
|
|
23
|
+
const newTemp = clampTemperature(targetTemp + delta);
|
|
24
|
+
onTempChange(newTemp);
|
|
25
|
+
}
|
|
26
|
+
}, { isActive });
|
|
27
|
+
const minTempDisplay = formatTemperature(MIN_TEMP_CELSIUS, temperatureUnit);
|
|
28
|
+
const maxTempDisplay = formatTemperature(MAX_TEMP_CELSIUS, temperatureUnit);
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { justifyContent: "center", children: _jsx(Text, { bold: true, children: "Adjust Temperature" }) }), _jsxs(Box, { justifyContent: "center", marginY: 1, children: [_jsx(Text, { dimColor: true, children: minTempDisplay }), _jsx(Text, { children: " " }), _jsx(TemperatureSlider, { value: targetTemp, min: MIN_TEMP_CELSIUS, max: MAX_TEMP_CELSIUS, isActive: isActive }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: maxTempDisplay })] }), _jsx(Box, { justifyContent: "center", children: _jsx(Text, { dimColor: true, children: isActive ? (_jsxs(Text, { children: ["Use ", _jsx(Text, { color: "cyan", children: "\u2190/\u2192" }), " or ", _jsx(Text, { color: "cyan", children: "h/l" }), " (\u00B10.5\u00B0) |", ' ', _jsx(Text, { color: "cyan", children: "\u2191/\u2193" }), " or ", _jsx(Text, { color: "cyan", children: "j/k" }), " (\u00B11\u00B0)"] })) : (_jsxs(Text, { children: ["Press ", _jsx(Text, { color: "cyan", children: "t" }), " to adjust temperature"] })) }) })] }));
|
|
30
|
+
}
|
|
31
|
+
function TemperatureSlider({ value, min, max, isActive, }) {
|
|
32
|
+
const totalWidth = 20;
|
|
33
|
+
const normalizedValue = (value - min) / (max - min);
|
|
34
|
+
const position = Math.round(normalizedValue * (totalWidth - 1));
|
|
35
|
+
const sliderChars = Array(totalWidth).fill('─');
|
|
36
|
+
sliderChars[position] = '●';
|
|
37
|
+
return (_jsx(Text, { color: isActive ? 'cyan' : 'white', children: sliderChars.join('') }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LiquidState, TemperatureUnit } from '../lib/types.js';
|
|
3
|
+
interface TemperatureDisplayProps {
|
|
4
|
+
currentTemp: number;
|
|
5
|
+
targetTemp: number;
|
|
6
|
+
liquidState: LiquidState;
|
|
7
|
+
temperatureUnit: TemperatureUnit;
|
|
8
|
+
}
|
|
9
|
+
export declare function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, }: TemperatureDisplayProps): React.ReactElement;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { LiquidState } from '../lib/types.js';
|
|
4
|
+
import { formatTemperature, getLiquidStateText, getLiquidStateIcon, estimateTimeToTargetTemp, formatDuration, } from '../lib/utils.js';
|
|
5
|
+
export function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, }) {
|
|
6
|
+
const isEmpty = liquidState === LiquidState.Empty;
|
|
7
|
+
const isAtTarget = Math.abs(currentTemp - targetTemp) < 0.5 && !isEmpty;
|
|
8
|
+
const tempDiff = currentTemp - targetTemp;
|
|
9
|
+
let tempColor;
|
|
10
|
+
if (isEmpty) {
|
|
11
|
+
tempColor = 'gray';
|
|
12
|
+
}
|
|
13
|
+
else if (Math.abs(tempDiff) < 1) {
|
|
14
|
+
tempColor = 'green';
|
|
15
|
+
}
|
|
16
|
+
else if (tempDiff > 0) {
|
|
17
|
+
tempColor = 'red';
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
tempColor = 'blue';
|
|
21
|
+
}
|
|
22
|
+
const timeToTarget = estimateTimeToTargetTemp(currentTemp, targetTemp, liquidState);
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { justifyContent: "center", children: _jsx(Text, { bold: true, children: "Temperature" }) }), _jsxs(Box, { marginY: 1, justifyContent: "center", children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { dimColor: true, children: "Current" }), _jsx(Text, { bold: true, color: tempColor, children: isEmpty ? '---' : formatTemperature(currentTemp, temperatureUnit) })] }), _jsx(Box, { marginX: 3, children: _jsx(Text, { dimColor: true, children: ' → ' }) }), _jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { dimColor: true, children: "Target" }), _jsx(Text, { bold: true, color: "cyan", children: formatTemperature(targetTemp, temperatureUnit) })] })] }), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: getStateColor(liquidState), children: [getLiquidStateIcon(liquidState), " ", getLiquidStateText(liquidState)] }), isAtTarget && _jsx(Text, { color: "green", children: " - Perfect!" })] }) }), timeToTarget !== null && timeToTarget > 0 && (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ['Est. time to target: ', _jsx(Text, { color: "yellow", children: formatDuration(timeToTarget) })] }) })), timeToTarget === 0 && !isEmpty && (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: "green", children: "At target temperature!" }) }))] }));
|
|
24
|
+
}
|
|
25
|
+
function getStateColor(state) {
|
|
26
|
+
switch (state) {
|
|
27
|
+
case LiquidState.Empty:
|
|
28
|
+
return 'gray';
|
|
29
|
+
case LiquidState.Filling:
|
|
30
|
+
return 'cyan';
|
|
31
|
+
case LiquidState.Cooling:
|
|
32
|
+
return 'blue';
|
|
33
|
+
case LiquidState.Heating:
|
|
34
|
+
return 'red';
|
|
35
|
+
case LiquidState.StableTemperature:
|
|
36
|
+
return 'green';
|
|
37
|
+
default:
|
|
38
|
+
return 'white';
|
|
39
|
+
}
|
|
40
|
+
}
|