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.
Files changed (41) hide show
  1. package/README.md +22 -15
  2. package/dist/cli.js +37 -7
  3. package/dist/components/App.d.ts +1 -1
  4. package/dist/components/App.js +65 -54
  5. package/dist/components/BatteryDisplay.d.ts +9 -3
  6. package/dist/components/BatteryDisplay.js +8 -19
  7. package/dist/components/ConnectionStatus.d.ts +7 -2
  8. package/dist/components/ConnectionStatus.js +5 -4
  9. package/dist/components/Header.d.ts +7 -2
  10. package/dist/components/Header.js +11 -3
  11. package/dist/components/HelpDisplay.d.ts +5 -2
  12. package/dist/components/HelpDisplay.js +8 -3
  13. package/dist/components/HorizontalRule.d.ts +2 -0
  14. package/dist/components/HorizontalRule.js +7 -0
  15. package/dist/components/Panel.d.ts +18 -0
  16. package/dist/components/Panel.js +57 -0
  17. package/dist/components/Presets.d.ts +8 -3
  18. package/dist/components/Presets.js +13 -8
  19. package/dist/components/SettingsView.d.ts +9 -3
  20. package/dist/components/SettingsView.js +90 -16
  21. package/dist/components/TemperatureControl.d.ts +8 -3
  22. package/dist/components/TemperatureControl.js +13 -18
  23. package/dist/components/TemperatureDisplay.d.ts +9 -3
  24. package/dist/components/TemperatureDisplay.js +9 -36
  25. package/dist/hooks/useMug.d.ts +3 -1
  26. package/dist/hooks/useMug.js +69 -22
  27. package/dist/lib/bluetooth.d.ts +2 -0
  28. package/dist/lib/bluetooth.js +8 -0
  29. package/dist/lib/mock-bluetooth.d.ts +65 -0
  30. package/dist/lib/mock-bluetooth.js +214 -0
  31. package/dist/lib/settings.d.ts +1 -1
  32. package/dist/lib/settings.js +20 -20
  33. package/dist/lib/theme.d.ts +135 -0
  34. package/dist/lib/theme.js +112 -0
  35. package/dist/lib/types.d.ts +0 -1
  36. package/dist/lib/types.js +12 -12
  37. package/dist/lib/utils.d.ts +7 -4
  38. package/dist/lib/utils.js +63 -40
  39. package/package.json +3 -1
  40. package/dist/components/ColorControl.d.ts +0 -9
  41. 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 'ink';
3
- import { formatTemperature } from '../lib/utils.js';
4
- export function Presets({ presets, selectedIndex, temperatureUnit, onSelect, isActive, }) {
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
- 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
+ // 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 ? '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, "]"] })] }));
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 'react';
2
- import { TemperatureUnit, Preset } from '../lib/types.js';
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 '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');
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 === 'q') {
28
+ if (key.escape || input === "q") {
12
29
  onClose();
13
30
  return;
14
31
  }
15
- if (key.upArrow || input === 'k') {
16
- setSelectedOption('unit');
32
+ if (key.upArrow || input === "k") {
33
+ setSelectedIndex((current) => Math.max(0, current - 1));
17
34
  }
18
- else if (key.downArrow || input === 'j') {
19
- setSelectedOption('presets');
35
+ if (key.downArrow || input === "j") {
36
+ setSelectedIndex((current) => Math.min(totalItems - 1, current + 1));
20
37
  }
21
- if (key.return || input === ' ') {
22
- if (selectedOption === 'unit') {
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
- 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"] }) })] }));
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 ? 'cyan' : 'white', children: [isSelected ? '> ' : ' ', label, ": ", _jsx(Text, { bold: true, children: value }), hint && isSelected && _jsxs(Text, { dimColor: true, children: [" (", hint, ")"] })] }) }));
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 'react';
2
- import { TemperatureUnit } from '../lib/types.js';
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 '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, }) {
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 || input === 'h' || input === '-') {
11
+ if (key.leftArrow) {
11
12
  delta = -0.5;
12
13
  }
13
- else if (key.rightArrow || input === 'l' || input === '+' || input === '=') {
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(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"] })) }) })] }));
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 = 20;
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 sliderChars = Array(totalWidth).fill('─');
36
- sliderChars[position] = '●';
37
- return (_jsx(Text, { color: isActive ? 'cyan' : 'white', children: sliderChars.join('') }));
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 'react';
2
- import { LiquidState, TemperatureUnit } from '../lib/types.js';
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 '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, }) {
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 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
- }
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
  }
@@ -1,4 +1,4 @@
1
- import { MugState, TemperatureUnit, RGBColor } from '../lib/types.js';
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 {};
@@ -1,6 +1,7 @@
1
- import { useState, useEffect, useCallback } from 'react';
2
- import { getBluetoothManager } from '../lib/bluetooth.js';
3
- import { LiquidState, TemperatureUnit } from '../lib/types.js';
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(false);
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('stateChange', handleStateChange);
41
- manager.on('scanning', handleScanning);
42
- manager.on('error', handleError);
43
- manager.on('mugFound', handleMugFound);
44
- manager.on('connected', handleConnected);
45
- manager.on('disconnected', handleDisconnected);
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('stateChange', handleStateChange);
48
- manager.off('scanning', handleScanning);
49
- manager.off('error', handleError);
50
- manager.off('mugFound', handleMugFound);
51
- manager.off('connected', handleConnected);
52
- manager.off('disconnected', handleDisconnected);
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 : 'Failed to start scanning');
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 : 'Failed to set temperature');
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 : 'Failed to set temperature unit');
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 : 'Failed to set LED color');
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 : 'Failed to disconnect');
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
  }
@@ -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;
@@ -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;