ember-mug 0.1.3 → 0.1.5
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/README.md +22 -15
- package/dist/cli.js +37 -7
- package/dist/components/App.d.ts +1 -1
- package/dist/components/App.js +65 -54
- package/dist/components/BatteryDisplay.d.ts +9 -3
- package/dist/components/BatteryDisplay.js +8 -19
- package/dist/components/ConnectionStatus.d.ts +7 -2
- package/dist/components/ConnectionStatus.js +5 -4
- package/dist/components/Header.d.ts +7 -2
- package/dist/components/Header.js +11 -3
- package/dist/components/HelpDisplay.d.ts +5 -2
- package/dist/components/HelpDisplay.js +8 -3
- package/dist/components/HorizontalRule.d.ts +2 -0
- package/dist/components/HorizontalRule.js +7 -0
- package/dist/components/Panel.d.ts +18 -0
- package/dist/components/Panel.js +57 -0
- package/dist/components/Presets.d.ts +8 -3
- package/dist/components/Presets.js +13 -8
- package/dist/components/SettingsView.d.ts +9 -3
- package/dist/components/SettingsView.js +90 -16
- package/dist/components/TemperatureControl.d.ts +8 -3
- package/dist/components/TemperatureControl.js +13 -18
- package/dist/components/TemperatureDisplay.d.ts +9 -3
- package/dist/components/TemperatureDisplay.js +9 -36
- package/dist/hooks/useMug.d.ts +3 -1
- package/dist/hooks/useMug.js +69 -22
- package/dist/lib/bluetooth.d.ts +2 -0
- package/dist/lib/bluetooth.js +8 -0
- package/dist/lib/mock-bluetooth.d.ts +65 -0
- package/dist/lib/mock-bluetooth.js +214 -0
- package/dist/lib/settings.d.ts +1 -1
- package/dist/lib/settings.js +20 -20
- package/dist/lib/theme.d.ts +135 -0
- package/dist/lib/theme.js +112 -0
- package/dist/lib/types.d.ts +0 -1
- package/dist/lib/types.js +12 -12
- package/dist/lib/utils.d.ts +7 -4
- package/dist/lib/utils.js +63 -40
- package/package.json +3 -1
- package/dist/components/ColorControl.d.ts +0 -9
- package/dist/components/ColorControl.js +0 -71
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useInput } from
|
|
3
|
-
import { formatTemperature } from
|
|
4
|
-
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { formatTemperature } from "../lib/utils.js";
|
|
4
|
+
import { Panel } from "./Panel.js";
|
|
5
|
+
export function Presets({ presets, selectedIndex, temperatureUnit, onSelect, isActive, width, height, theme, }) {
|
|
5
6
|
useInput((input, key) => {
|
|
6
7
|
if (!isActive)
|
|
7
8
|
return;
|
|
@@ -10,10 +11,14 @@ export function Presets({ presets, selectedIndex, temperatureUnit, onSelect, isA
|
|
|
10
11
|
onSelect(presets[numKey - 1]);
|
|
11
12
|
}
|
|
12
13
|
}, { isActive });
|
|
13
|
-
|
|
14
|
+
// Fixed width for each preset button (12 chars inner content)
|
|
15
|
+
const boxWidth = 12;
|
|
16
|
+
return (_jsx(Panel, { title: "Temperature Presets", titleColor: theme.primary, borderColor: theme.border, width: width, height: height, children: _jsx(Box, { justifyContent: "center", gap: 1, children: presets.map((preset, index) => (_jsx(PresetButton, { preset: preset, index: index, isSelected: selectedIndex === index, temperatureUnit: temperatureUnit, theme: theme, boxWidth: boxWidth }, preset.id))) }) }));
|
|
14
17
|
}
|
|
15
|
-
function PresetButton({ preset, index, isSelected, temperatureUnit, }) {
|
|
16
|
-
const borderColor = isSelected ?
|
|
17
|
-
const textColor = isSelected ?
|
|
18
|
-
|
|
18
|
+
function PresetButton({ preset, index, isSelected, temperatureUnit, theme, boxWidth, }) {
|
|
19
|
+
const borderColor = isSelected ? theme.primary : theme.dimText;
|
|
20
|
+
const textColor = isSelected ? theme.text : theme.dimText;
|
|
21
|
+
const nameDisplay = preset.name.substring(0, boxWidth).padEnd(boxWidth);
|
|
22
|
+
const tempDisplay = formatTemperature(preset.temperature, temperatureUnit).padStart(boxWidth);
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsxs(Text, { color: borderColor, children: ["+", "-".repeat(boxWidth), "+"] }), _jsxs(Text, { color: borderColor, children: ["|", _jsx(Text, { color: textColor, bold: true, children: nameDisplay }), "|"] }), _jsxs(Text, { color: borderColor, children: ["|", _jsx(Text, { color: isSelected ? theme.primary : theme.text, children: tempDisplay }), "|"] }), _jsxs(Text, { color: borderColor, children: ["+", "-".repeat(boxWidth), "+"] }), _jsxs(Text, { color: theme.primary, bold: true, children: ["[", index + 1, "]"] })] }));
|
|
19
24
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { TemperatureUnit, Preset } from
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TemperatureUnit, Preset, RGBColor } from "../lib/types.js";
|
|
3
|
+
import { TERMINAL_COLORS } from "../lib/theme.js";
|
|
4
|
+
type TerminalTheme = (typeof TERMINAL_COLORS)[keyof typeof TERMINAL_COLORS];
|
|
3
5
|
interface SettingsViewProps {
|
|
4
6
|
presets: Preset[];
|
|
5
7
|
temperatureUnit: TemperatureUnit;
|
|
8
|
+
ledColor: RGBColor;
|
|
6
9
|
onTemperatureUnitChange: (unit: TemperatureUnit) => void;
|
|
10
|
+
onPresetUpdate: (preset: Preset) => void;
|
|
11
|
+
onColorChange: (color: RGBColor) => void;
|
|
7
12
|
onClose: () => void;
|
|
8
13
|
isActive: boolean;
|
|
14
|
+
theme: TerminalTheme;
|
|
9
15
|
}
|
|
10
|
-
export declare function SettingsView({ presets, temperatureUnit, onTemperatureUnitChange, onClose, isActive, }: SettingsViewProps): React.ReactElement;
|
|
16
|
+
export declare function SettingsView({ presets, temperatureUnit, ledColor, onTemperatureUnitChange, onPresetUpdate, onColorChange, onClose, isActive, theme, }: SettingsViewProps): React.ReactElement;
|
|
11
17
|
export {};
|
|
@@ -1,34 +1,108 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from
|
|
3
|
-
import { Box, Text, useInput } from
|
|
4
|
-
import { TemperatureUnit } from
|
|
5
|
-
import { formatTemperature } from
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
4
|
+
import { TemperatureUnit } from "../lib/types.js";
|
|
5
|
+
import { formatTemperature, rgbToHex } from "../lib/utils.js";
|
|
6
|
+
import { Panel } from "./Panel.js";
|
|
7
|
+
const PRESET_COLORS = [
|
|
8
|
+
{ name: "Orange", color: { r: 255, g: 147, b: 41, a: 255 } },
|
|
9
|
+
{ name: "Red", color: { r: 255, g: 0, b: 0, a: 255 } },
|
|
10
|
+
{ name: "Green", color: { r: 0, g: 255, b: 0, a: 255 } },
|
|
11
|
+
{ name: "Blue", color: { r: 0, g: 128, b: 255, a: 255 } },
|
|
12
|
+
{ name: "Purple", color: { r: 128, g: 0, b: 255, a: 255 } },
|
|
13
|
+
{ name: "Pink", color: { r: 255, g: 105, b: 180, a: 255 } },
|
|
14
|
+
{ name: "White", color: { r: 255, g: 255, b: 255, a: 255 } },
|
|
15
|
+
{ name: "Teal", color: { r: 0, g: 255, b: 200, a: 255 } },
|
|
16
|
+
];
|
|
17
|
+
export function SettingsView({ presets, temperatureUnit, ledColor, onTemperatureUnitChange, onPresetUpdate, onColorChange, onClose, isActive, theme, }) {
|
|
18
|
+
// 0 = Unit Toggle, 1 = LED Color, 2...N = Presets
|
|
19
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
20
|
+
// Calculate total items: 1 (unit) + 1 (color) + presets count
|
|
21
|
+
const totalItems = 2 + presets.length;
|
|
22
|
+
const { stdout } = useStdout();
|
|
23
|
+
const terminalWidth = stdout?.columns || 80;
|
|
24
|
+
const panelWidth = Math.floor(terminalWidth * 0.6); // 60% of terminal width
|
|
8
25
|
useInput((input, key) => {
|
|
9
26
|
if (!isActive)
|
|
10
27
|
return;
|
|
11
|
-
if (key.escape || input ===
|
|
28
|
+
if (key.escape || input === "q") {
|
|
12
29
|
onClose();
|
|
13
30
|
return;
|
|
14
31
|
}
|
|
15
|
-
if (key.upArrow || input ===
|
|
16
|
-
|
|
32
|
+
if (key.upArrow || input === "k") {
|
|
33
|
+
setSelectedIndex((current) => Math.max(0, current - 1));
|
|
17
34
|
}
|
|
18
|
-
|
|
19
|
-
|
|
35
|
+
if (key.downArrow || input === "j") {
|
|
36
|
+
setSelectedIndex((current) => Math.min(totalItems - 1, current + 1));
|
|
20
37
|
}
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
// Toggle Unit
|
|
39
|
+
if (selectedIndex === 0) {
|
|
40
|
+
if (key.return || input === " ") {
|
|
23
41
|
const newUnit = temperatureUnit === TemperatureUnit.Celsius
|
|
24
42
|
? TemperatureUnit.Fahrenheit
|
|
25
43
|
: TemperatureUnit.Celsius;
|
|
26
44
|
onTemperatureUnitChange(newUnit);
|
|
27
45
|
}
|
|
28
46
|
}
|
|
47
|
+
else if (selectedIndex === 1) {
|
|
48
|
+
// LED Color
|
|
49
|
+
if (key.leftArrow || input === "h" || key.rightArrow || input === "l") {
|
|
50
|
+
const currentColorIndex = ledColor
|
|
51
|
+
? PRESET_COLORS.findIndex((p) => p.color.r === ledColor.r &&
|
|
52
|
+
p.color.g === ledColor.g &&
|
|
53
|
+
p.color.b === ledColor.b)
|
|
54
|
+
: -1;
|
|
55
|
+
let nextIndex = currentColorIndex;
|
|
56
|
+
if (key.leftArrow || input === "h") {
|
|
57
|
+
nextIndex = currentColorIndex - 1;
|
|
58
|
+
if (nextIndex < 0)
|
|
59
|
+
nextIndex = PRESET_COLORS.length - 1;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
nextIndex = currentColorIndex + 1;
|
|
63
|
+
if (nextIndex >= PRESET_COLORS.length)
|
|
64
|
+
nextIndex = 0;
|
|
65
|
+
}
|
|
66
|
+
onColorChange(PRESET_COLORS[nextIndex].color);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Edit Preset
|
|
71
|
+
const presetIndex = selectedIndex - 2;
|
|
72
|
+
const preset = presets[presetIndex];
|
|
73
|
+
let delta = 0;
|
|
74
|
+
if (key.leftArrow || input === "h") {
|
|
75
|
+
delta = -0.5;
|
|
76
|
+
}
|
|
77
|
+
else if (key.rightArrow || input === "l") {
|
|
78
|
+
delta = 0.5;
|
|
79
|
+
}
|
|
80
|
+
if (delta !== 0 && preset) {
|
|
81
|
+
onPresetUpdate({
|
|
82
|
+
...preset,
|
|
83
|
+
temperature: preset.temperature + delta,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
29
87
|
}, { isActive });
|
|
30
|
-
|
|
88
|
+
const currentColorPreset = ledColor
|
|
89
|
+
? PRESET_COLORS.find((p) => p.color.r === ledColor.r &&
|
|
90
|
+
p.color.g === ledColor.g &&
|
|
91
|
+
p.color.b === ledColor.b)
|
|
92
|
+
: undefined;
|
|
93
|
+
const colorDisplayValue = currentColorPreset
|
|
94
|
+
? currentColorPreset.name
|
|
95
|
+
: ledColor
|
|
96
|
+
? rgbToHex(ledColor.r, ledColor.g, ledColor.b)
|
|
97
|
+
: "Unknown";
|
|
98
|
+
return (_jsx(Box, { justifyContent: "center", marginY: 1, children: _jsxs(Panel, { title: "[=] Settings", titleColor: theme.primary, borderColor: theme.border, width: panelWidth, height: 16, children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(SettingRow, { label: "Temperature Unit", value: temperatureUnit === TemperatureUnit.Celsius
|
|
99
|
+
? "Celsius (°C)"
|
|
100
|
+
: "Fahrenheit (°F)", isSelected: selectedIndex === 0, hint: "Enter/Space to toggle", theme: theme }), _jsx(SettingRow, { label: "LED Color", value: colorDisplayValue, valueColor: ledColor
|
|
101
|
+
? rgbToHex(ledColor.r, ledColor.g, ledColor.b)
|
|
102
|
+
: undefined, isSelected: selectedIndex === 1, hint: "Left/Right to change", theme: theme }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.primary, bold: true, children: "Presets:" }), presets.map((preset, index) => (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.dimText, children: [_jsxs(Text, { color: selectedIndex === index + 2
|
|
103
|
+
? theme.primary
|
|
104
|
+
: theme.dimText, children: [selectedIndex === index + 2 ? "> " : " ", index + 1, "."] }), " ", preset.name, ":", " ", _jsx(Text, { color: selectedIndex === index + 2 ? theme.text : theme.dimText, bold: selectedIndex === index + 2, children: formatTemperature(preset.temperature, temperatureUnit) }), selectedIndex === index + 2 && (_jsx(Text, { dimColor: true, children: " (Left/Right to edit)" }))] }) }, preset.id)))] })] }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsxs(Text, { color: theme.dimText, children: [_jsx(Text, { color: theme.primary, bold: true, children: "[Esc]" }), " ", "or", " ", _jsx(Text, { color: theme.primary, bold: true, children: "[q]" }), " ", "to close"] }) }), _jsx(Box, { justifyContent: "center", children: _jsx(Text, { color: theme.dimText, dimColor: true, children: "(Use Arrow Keys to navigate and edit)" }) })] }) }));
|
|
31
105
|
}
|
|
32
|
-
function SettingRow({ label, value, isSelected, hint, }) {
|
|
33
|
-
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ?
|
|
106
|
+
function SettingRow({ label, value, valueColor, isSelected, hint, theme, }) {
|
|
107
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.primary : theme.text, children: [isSelected ? "> " : " ", label, ":", " ", _jsx(Text, { bold: true, color: valueColor, children: value }), hint && isSelected && _jsxs(Text, { dimColor: true, children: [" (", hint, ")"] })] }) }));
|
|
34
108
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { TemperatureUnit } from
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TemperatureUnit } from "../lib/types.js";
|
|
3
|
+
import { TERMINAL_COLORS } from "../lib/theme.js";
|
|
4
|
+
type TerminalTheme = (typeof TERMINAL_COLORS)[keyof typeof TERMINAL_COLORS];
|
|
3
5
|
interface TemperatureControlProps {
|
|
4
6
|
targetTemp: number;
|
|
5
7
|
temperatureUnit: TemperatureUnit;
|
|
6
8
|
onTempChange: (temp: number) => void;
|
|
7
9
|
isActive: boolean;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
theme: TerminalTheme;
|
|
8
13
|
}
|
|
9
|
-
export declare function TemperatureControl({ targetTemp, temperatureUnit, onTempChange, isActive, }: TemperatureControlProps): React.ReactElement;
|
|
14
|
+
export declare function TemperatureControl({ targetTemp, temperatureUnit, onTempChange, isActive, width, height, theme, }: TemperatureControlProps): React.ReactElement;
|
|
10
15
|
export {};
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useInput } from
|
|
3
|
-
import { MIN_TEMP_CELSIUS, MAX_TEMP_CELSIUS } from
|
|
4
|
-
import { formatTemperature, clampTemperature } from
|
|
5
|
-
|
|
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
|
+
import { Panel } from "./Panel.js";
|
|
6
|
+
export function TemperatureControl({ targetTemp, temperatureUnit, onTempChange, isActive, width, height, theme, }) {
|
|
6
7
|
useInput((input, key) => {
|
|
7
8
|
if (!isActive)
|
|
8
9
|
return;
|
|
9
10
|
let delta = 0;
|
|
10
|
-
if (key.leftArrow
|
|
11
|
+
if (key.leftArrow) {
|
|
11
12
|
delta = -0.5;
|
|
12
13
|
}
|
|
13
|
-
else if (key.rightArrow
|
|
14
|
+
else if (key.rightArrow) {
|
|
14
15
|
delta = 0.5;
|
|
15
16
|
}
|
|
16
|
-
else if (key.upArrow || input === 'k') {
|
|
17
|
-
delta = 1;
|
|
18
|
-
}
|
|
19
|
-
else if (key.downArrow || input === 'j') {
|
|
20
|
-
delta = -1;
|
|
21
|
-
}
|
|
22
17
|
if (delta !== 0) {
|
|
23
18
|
const newTemp = clampTemperature(targetTemp + delta);
|
|
24
19
|
onTempChange(newTemp);
|
|
@@ -26,13 +21,13 @@ export function TemperatureControl({ targetTemp, temperatureUnit, onTempChange,
|
|
|
26
21
|
}, { isActive });
|
|
27
22
|
const minTempDisplay = formatTemperature(MIN_TEMP_CELSIUS, temperatureUnit);
|
|
28
23
|
const maxTempDisplay = formatTemperature(MAX_TEMP_CELSIUS, temperatureUnit);
|
|
29
|
-
return (_jsxs(
|
|
24
|
+
return (_jsxs(Panel, { title: "Temperature Adjust", titleColor: theme.primary, borderColor: theme.border, width: width, height: height, children: [_jsxs(Box, { justifyContent: "center", marginY: 1, children: [_jsx(Text, { color: theme.dimText, children: minTempDisplay }), _jsx(Text, { children: " " }), _jsx(TemperatureSlider, { value: targetTemp, min: MIN_TEMP_CELSIUS, max: MAX_TEMP_CELSIUS, isActive: isActive, theme: theme }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.dimText, children: maxTempDisplay })] }), _jsx(Box, { justifyContent: "center", children: _jsx(Text, { color: theme.dimText, children: _jsx(Text, { color: theme.primary, children: "\u2190/\u2192" }) }) })] }));
|
|
30
25
|
}
|
|
31
|
-
function TemperatureSlider({ value, min, max, isActive, }) {
|
|
32
|
-
const totalWidth =
|
|
26
|
+
function TemperatureSlider({ value, min, max, isActive, theme, }) {
|
|
27
|
+
const totalWidth = 16;
|
|
33
28
|
const normalizedValue = (value - min) / (max - min);
|
|
34
29
|
const position = Math.round(normalizedValue * (totalWidth - 1));
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
return (_jsx(Text, { color: isActive ?
|
|
30
|
+
const leftPart = "━".repeat(position);
|
|
31
|
+
const rightPart = "━".repeat(totalWidth - position - 1);
|
|
32
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: isActive ? theme.primary : theme.dimText, children: leftPart }), _jsx(Text, { color: theme.text, bold: true, children: "\u25C9" }), _jsx(Text, { color: theme.dimText, children: rightPart })] }));
|
|
38
33
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { LiquidState, TemperatureUnit } from
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { LiquidState, TemperatureUnit } from "../lib/types.js";
|
|
3
|
+
import { TERMINAL_COLORS } from "../lib/theme.js";
|
|
4
|
+
type TerminalTheme = (typeof TERMINAL_COLORS)[keyof typeof TERMINAL_COLORS];
|
|
3
5
|
interface TemperatureDisplayProps {
|
|
4
6
|
currentTemp: number;
|
|
5
7
|
targetTemp: number;
|
|
6
8
|
liquidState: LiquidState;
|
|
7
9
|
temperatureUnit: TemperatureUnit;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
theme: TerminalTheme;
|
|
13
|
+
tempRate?: number;
|
|
8
14
|
}
|
|
9
|
-
export declare function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, }: TemperatureDisplayProps): React.ReactElement;
|
|
15
|
+
export declare function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, width, height, theme, tempRate, }: TemperatureDisplayProps): React.ReactElement;
|
|
10
16
|
export {};
|
|
@@ -1,40 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import { LiquidState } from
|
|
4
|
-
import { formatTemperature, getLiquidStateText,
|
|
5
|
-
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { LiquidState } from "../lib/types.js";
|
|
4
|
+
import { formatTemperature, getLiquidStateText, estimateTimeToTargetTemp, formatDuration, } from "../lib/utils.js";
|
|
5
|
+
import { Panel } from "./Panel.js";
|
|
6
|
+
export function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, width, height, theme, tempRate, }) {
|
|
6
7
|
const isEmpty = liquidState === LiquidState.Empty;
|
|
7
8
|
const isAtTarget = Math.abs(currentTemp - targetTemp) < 0.5 && !isEmpty;
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
}
|
|
9
|
+
const timeToTarget = estimateTimeToTargetTemp(currentTemp, targetTemp, liquidState, tempRate);
|
|
10
|
+
return (_jsxs(Panel, { title: "Temperature", titleColor: theme.primary, borderColor: theme.border, width: width, height: height, children: [_jsxs(Box, { marginY: 1, justifyContent: "center", width: "100%", children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", minWidth: 16, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { color: theme.dimText, children: "Current" }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: theme.primary, children: isEmpty
|
|
11
|
+
? "---"
|
|
12
|
+
: formatTemperature(currentTemp, temperatureUnit) }) })] }), _jsx(Box, { marginX: 2, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: theme.primary, bold: true, children: "-->" }) }), _jsxs(Box, { flexDirection: "column", alignItems: "center", minWidth: 16, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { color: theme.dimText, children: "Target" }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: theme.text, children: formatTemperature(targetTemp, temperatureUnit) }) })] })] }), _jsx(Box, { justifyContent: "center", children: _jsxs(Text, { children: [_jsx(Text, { color: theme.primary, bold: true, children: getLiquidStateText(liquidState) }), isAtTarget && (_jsxs(Text, { color: theme.primary, bold: true, children: [" ", "*"] })), timeToTarget !== null && timeToTarget > 0 && (_jsxs(Text, { color: theme.dimText, children: [" • [~] ", _jsx(Text, { color: theme.primary, children: formatDuration(timeToTarget) }), _jsx(Text, { color: theme.dimText, children: " to target" })] }))] }) })] }));
|
|
40
13
|
}
|
package/dist/hooks/useMug.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MugState, TemperatureUnit, RGBColor } from
|
|
1
|
+
import { MugState, TemperatureUnit, RGBColor } from "../lib/types.js";
|
|
2
2
|
interface UseMugReturn {
|
|
3
3
|
state: MugState;
|
|
4
4
|
isScanning: boolean;
|
|
@@ -9,6 +9,8 @@ interface UseMugReturn {
|
|
|
9
9
|
setTemperatureUnit: (unit: TemperatureUnit) => Promise<void>;
|
|
10
10
|
setLedColor: (color: RGBColor) => Promise<void>;
|
|
11
11
|
disconnect: () => Promise<void>;
|
|
12
|
+
tempRate: number;
|
|
13
|
+
batteryRate: number;
|
|
12
14
|
}
|
|
13
15
|
export declare function useMug(): UseMugReturn;
|
|
14
16
|
export {};
|
package/dist/hooks/useMug.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from
|
|
2
|
-
import { getBluetoothManager } from
|
|
3
|
-
import { LiquidState, TemperatureUnit } from
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { getBluetoothManager } from "../lib/bluetooth.js";
|
|
3
|
+
import { LiquidState, TemperatureUnit, } from "../lib/types.js";
|
|
4
|
+
import { calculateRate } from "../lib/utils.js";
|
|
4
5
|
const initialState = {
|
|
5
6
|
connected: false,
|
|
6
7
|
batteryLevel: 0,
|
|
@@ -10,13 +11,55 @@ const initialState = {
|
|
|
10
11
|
liquidState: LiquidState.Empty,
|
|
11
12
|
temperatureUnit: TemperatureUnit.Celsius,
|
|
12
13
|
color: { r: 255, g: 147, b: 41, a: 255 },
|
|
13
|
-
mugName:
|
|
14
|
+
mugName: "",
|
|
14
15
|
};
|
|
15
16
|
export function useMug() {
|
|
16
17
|
const [state, setState] = useState(initialState);
|
|
17
|
-
const [isScanning, setIsScanning] = useState(
|
|
18
|
+
const [isScanning, setIsScanning] = useState(true);
|
|
18
19
|
const [error, setError] = useState(null);
|
|
19
20
|
const [foundMugName, setFoundMugName] = useState(null);
|
|
21
|
+
const [tempHistory, setTempHistory] = useState([]);
|
|
22
|
+
const [batteryHistory, setBatteryHistory] = useState([]);
|
|
23
|
+
// Update temp history
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (state.connected && state.liquidState !== LiquidState.Empty) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
setTempHistory((prev) => {
|
|
28
|
+
const last = prev[prev.length - 1];
|
|
29
|
+
if (last &&
|
|
30
|
+
last.value === state.currentTemp &&
|
|
31
|
+
now - last.time < 10000) {
|
|
32
|
+
return prev;
|
|
33
|
+
}
|
|
34
|
+
const next = [...prev, { value: state.currentTemp, time: now }];
|
|
35
|
+
return next.filter((item) => now - item.time < 5 * 60 * 1000);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else if (!state.connected || state.liquidState === LiquidState.Empty) {
|
|
39
|
+
if (tempHistory.length > 0)
|
|
40
|
+
setTempHistory([]);
|
|
41
|
+
}
|
|
42
|
+
}, [state.currentTemp, state.connected, state.liquidState]);
|
|
43
|
+
// Update battery history
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (state.connected) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
setBatteryHistory((prev) => {
|
|
48
|
+
const last = prev[prev.length - 1];
|
|
49
|
+
if (last &&
|
|
50
|
+
last.value === state.batteryLevel &&
|
|
51
|
+
now - last.time < 30000) {
|
|
52
|
+
return prev;
|
|
53
|
+
}
|
|
54
|
+
const next = [...prev, { value: state.batteryLevel, time: now }];
|
|
55
|
+
return next.filter((item) => now - item.time < 10 * 60 * 1000);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else if (!state.connected) {
|
|
59
|
+
if (batteryHistory.length > 0)
|
|
60
|
+
setBatteryHistory([]);
|
|
61
|
+
}
|
|
62
|
+
}, [state.batteryLevel, state.connected]);
|
|
20
63
|
useEffect(() => {
|
|
21
64
|
const manager = getBluetoothManager();
|
|
22
65
|
const handleStateChange = (newState) => {
|
|
@@ -37,19 +80,19 @@ export function useMug() {
|
|
|
37
80
|
const handleDisconnected = () => {
|
|
38
81
|
setFoundMugName(null);
|
|
39
82
|
};
|
|
40
|
-
manager.on(
|
|
41
|
-
manager.on(
|
|
42
|
-
manager.on(
|
|
43
|
-
manager.on(
|
|
44
|
-
manager.on(
|
|
45
|
-
manager.on(
|
|
83
|
+
manager.on("stateChange", handleStateChange);
|
|
84
|
+
manager.on("scanning", handleScanning);
|
|
85
|
+
manager.on("error", handleError);
|
|
86
|
+
manager.on("mugFound", handleMugFound);
|
|
87
|
+
manager.on("connected", handleConnected);
|
|
88
|
+
manager.on("disconnected", handleDisconnected);
|
|
46
89
|
return () => {
|
|
47
|
-
manager.off(
|
|
48
|
-
manager.off(
|
|
49
|
-
manager.off(
|
|
50
|
-
manager.off(
|
|
51
|
-
manager.off(
|
|
52
|
-
manager.off(
|
|
90
|
+
manager.off("stateChange", handleStateChange);
|
|
91
|
+
manager.off("scanning", handleScanning);
|
|
92
|
+
manager.off("error", handleError);
|
|
93
|
+
manager.off("mugFound", handleMugFound);
|
|
94
|
+
manager.off("connected", handleConnected);
|
|
95
|
+
manager.off("disconnected", handleDisconnected);
|
|
53
96
|
};
|
|
54
97
|
}, []);
|
|
55
98
|
const startScanning = useCallback(async () => {
|
|
@@ -59,7 +102,7 @@ export function useMug() {
|
|
|
59
102
|
await manager.startScanning();
|
|
60
103
|
}
|
|
61
104
|
catch (err) {
|
|
62
|
-
setError(err instanceof Error ? err.message :
|
|
105
|
+
setError(err instanceof Error ? err.message : "Failed to start scanning");
|
|
63
106
|
}
|
|
64
107
|
}, []);
|
|
65
108
|
const setTargetTemp = useCallback(async (temp) => {
|
|
@@ -68,7 +111,7 @@ export function useMug() {
|
|
|
68
111
|
await manager.setTargetTemp(temp);
|
|
69
112
|
}
|
|
70
113
|
catch (err) {
|
|
71
|
-
setError(err instanceof Error ? err.message :
|
|
114
|
+
setError(err instanceof Error ? err.message : "Failed to set temperature");
|
|
72
115
|
}
|
|
73
116
|
}, []);
|
|
74
117
|
const setTemperatureUnit = useCallback(async (unit) => {
|
|
@@ -77,7 +120,7 @@ export function useMug() {
|
|
|
77
120
|
await manager.setTemperatureUnit(unit);
|
|
78
121
|
}
|
|
79
122
|
catch (err) {
|
|
80
|
-
setError(err instanceof Error ? err.message :
|
|
123
|
+
setError(err instanceof Error ? err.message : "Failed to set temperature unit");
|
|
81
124
|
}
|
|
82
125
|
}, []);
|
|
83
126
|
const setLedColor = useCallback(async (color) => {
|
|
@@ -86,7 +129,7 @@ export function useMug() {
|
|
|
86
129
|
await manager.setLedColor(color);
|
|
87
130
|
}
|
|
88
131
|
catch (err) {
|
|
89
|
-
setError(err instanceof Error ? err.message :
|
|
132
|
+
setError(err instanceof Error ? err.message : "Failed to set LED color");
|
|
90
133
|
}
|
|
91
134
|
}, []);
|
|
92
135
|
const disconnect = useCallback(async () => {
|
|
@@ -95,9 +138,11 @@ export function useMug() {
|
|
|
95
138
|
await manager.disconnect();
|
|
96
139
|
}
|
|
97
140
|
catch (err) {
|
|
98
|
-
setError(err instanceof Error ? err.message :
|
|
141
|
+
setError(err instanceof Error ? err.message : "Failed to disconnect");
|
|
99
142
|
}
|
|
100
143
|
}, []);
|
|
144
|
+
const tempRate = calculateRate(tempHistory);
|
|
145
|
+
const batteryRate = calculateRate(batteryHistory, 5 * 60 * 1000);
|
|
101
146
|
return {
|
|
102
147
|
state,
|
|
103
148
|
isScanning,
|
|
@@ -108,5 +153,7 @@ export function useMug() {
|
|
|
108
153
|
setTemperatureUnit,
|
|
109
154
|
setLedColor,
|
|
110
155
|
disconnect,
|
|
156
|
+
tempRate,
|
|
157
|
+
batteryRate,
|
|
111
158
|
};
|
|
112
159
|
}
|
package/dist/lib/bluetooth.d.ts
CHANGED
|
@@ -40,4 +40,6 @@ export declare class BluetoothManager extends EventEmitter {
|
|
|
40
40
|
private emitState;
|
|
41
41
|
disconnect(): Promise<void>;
|
|
42
42
|
}
|
|
43
|
+
export declare function isMockMode(): boolean;
|
|
43
44
|
export declare function getBluetoothManager(): BluetoothManager;
|
|
45
|
+
export declare function setBluetoothManager(manager: BluetoothManager): void;
|
package/dist/lib/bluetooth.js
CHANGED
|
@@ -326,9 +326,17 @@ export class BluetoothManager extends EventEmitter {
|
|
|
326
326
|
}
|
|
327
327
|
// Singleton instance
|
|
328
328
|
let instance = null;
|
|
329
|
+
// Check if mock mode is enabled
|
|
330
|
+
export function isMockMode() {
|
|
331
|
+
return process.env.EMBER_MOCK === 'true' || process.env.EMBER_MOCK === '1';
|
|
332
|
+
}
|
|
329
333
|
export function getBluetoothManager() {
|
|
330
334
|
if (!instance) {
|
|
331
335
|
instance = new BluetoothManager();
|
|
332
336
|
}
|
|
333
337
|
return instance;
|
|
334
338
|
}
|
|
339
|
+
// Set a custom manager (used for mock mode)
|
|
340
|
+
export function setBluetoothManager(manager) {
|
|
341
|
+
instance = manager;
|
|
342
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { MugState, LiquidState, TemperatureUnit, RGBColor } from './types.js';
|
|
3
|
+
export interface MockBluetoothManagerEvents {
|
|
4
|
+
stateChange: (state: MugState) => void;
|
|
5
|
+
connected: () => void;
|
|
6
|
+
disconnected: () => void;
|
|
7
|
+
scanning: (isScanning: boolean) => void;
|
|
8
|
+
error: (error: Error) => void;
|
|
9
|
+
mugFound: (name: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface MockConfig {
|
|
12
|
+
/** Initial battery level (0-100) */
|
|
13
|
+
initialBattery?: number;
|
|
14
|
+
/** Initial current temperature in Celsius */
|
|
15
|
+
initialCurrentTemp?: number;
|
|
16
|
+
/** Initial target temperature in Celsius */
|
|
17
|
+
initialTargetTemp?: number;
|
|
18
|
+
/** Initial liquid state */
|
|
19
|
+
initialLiquidState?: LiquidState;
|
|
20
|
+
/** Whether the mug starts on the charger */
|
|
21
|
+
initiallyCharging?: boolean;
|
|
22
|
+
/** Simulated mug name */
|
|
23
|
+
mugName?: string;
|
|
24
|
+
/** Delay before "finding" the mug during scan (ms) */
|
|
25
|
+
scanDelay?: number;
|
|
26
|
+
/** Delay before "connecting" after finding (ms) */
|
|
27
|
+
connectionDelay?: number;
|
|
28
|
+
/** How fast temperature changes (degrees per second, default 0.5) */
|
|
29
|
+
tempChangeRate?: number;
|
|
30
|
+
/** How often to update simulation (ms) */
|
|
31
|
+
updateInterval?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare class MockBluetoothManager extends EventEmitter {
|
|
34
|
+
private config;
|
|
35
|
+
private isConnected;
|
|
36
|
+
private simulationInterval;
|
|
37
|
+
private lastUpdateTime;
|
|
38
|
+
private state;
|
|
39
|
+
constructor(config?: MockConfig);
|
|
40
|
+
startScanning(): Promise<void>;
|
|
41
|
+
stopScanning(): Promise<void>;
|
|
42
|
+
private connect;
|
|
43
|
+
private startSimulation;
|
|
44
|
+
private updateSimulation;
|
|
45
|
+
setTargetTemp(temp: number): Promise<void>;
|
|
46
|
+
setTemperatureUnit(unit: TemperatureUnit): Promise<void>;
|
|
47
|
+
setLedColor(color: RGBColor): Promise<void>;
|
|
48
|
+
getState(): MugState;
|
|
49
|
+
private emitState;
|
|
50
|
+
disconnect(): Promise<void>;
|
|
51
|
+
/** Simulate placing the mug on the charger */
|
|
52
|
+
simulateStartCharging(): void;
|
|
53
|
+
/** Simulate removing the mug from the charger */
|
|
54
|
+
simulateStopCharging(): void;
|
|
55
|
+
/** Simulate the mug becoming empty */
|
|
56
|
+
simulateEmpty(): void;
|
|
57
|
+
/** Simulate filling the mug with liquid at a given temperature */
|
|
58
|
+
simulateFill(temperature: number): void;
|
|
59
|
+
/** Simulate a connection drop */
|
|
60
|
+
simulateDisconnect(): void;
|
|
61
|
+
/** Set battery level directly (for testing low battery scenarios) */
|
|
62
|
+
simulateBatteryLevel(level: number): void;
|
|
63
|
+
}
|
|
64
|
+
export declare function getMockBluetoothManager(config?: MockConfig): MockBluetoothManager;
|
|
65
|
+
export declare function resetMockBluetoothManager(): void;
|