ember-mug 0.1.3 → 0.1.4

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 (40) hide show
  1. package/dist/cli.js +37 -7
  2. package/dist/components/App.d.ts +1 -1
  3. package/dist/components/App.js +64 -53
  4. package/dist/components/BatteryDisplay.d.ts +8 -3
  5. package/dist/components/BatteryDisplay.js +7 -18
  6. package/dist/components/ConnectionStatus.d.ts +7 -2
  7. package/dist/components/ConnectionStatus.js +5 -4
  8. package/dist/components/Header.d.ts +7 -2
  9. package/dist/components/Header.js +11 -3
  10. package/dist/components/HelpDisplay.d.ts +5 -2
  11. package/dist/components/HelpDisplay.js +8 -3
  12. package/dist/components/HorizontalRule.d.ts +2 -0
  13. package/dist/components/HorizontalRule.js +7 -0
  14. package/dist/components/Panel.d.ts +18 -0
  15. package/dist/components/Panel.js +57 -0
  16. package/dist/components/Presets.d.ts +8 -3
  17. package/dist/components/Presets.js +13 -8
  18. package/dist/components/SettingsView.d.ts +9 -3
  19. package/dist/components/SettingsView.js +82 -16
  20. package/dist/components/TemperatureControl.d.ts +8 -3
  21. package/dist/components/TemperatureControl.js +13 -18
  22. package/dist/components/TemperatureDisplay.d.ts +8 -3
  23. package/dist/components/TemperatureDisplay.js +8 -35
  24. package/dist/hooks/useMug.d.ts +1 -1
  25. package/dist/hooks/useMug.js +22 -22
  26. package/dist/lib/bluetooth.d.ts +2 -0
  27. package/dist/lib/bluetooth.js +8 -0
  28. package/dist/lib/mock-bluetooth.d.ts +65 -0
  29. package/dist/lib/mock-bluetooth.js +214 -0
  30. package/dist/lib/settings.d.ts +1 -1
  31. package/dist/lib/settings.js +20 -20
  32. package/dist/lib/theme.d.ts +135 -0
  33. package/dist/lib/theme.js +112 -0
  34. package/dist/lib/types.d.ts +0 -1
  35. package/dist/lib/types.js +12 -12
  36. package/dist/lib/utils.d.ts +1 -2
  37. package/dist/lib/utils.js +19 -35
  38. package/package.json +2 -1
  39. package/dist/components/ColorControl.d.ts +0 -9
  40. package/dist/components/ColorControl.js +0 -71
@@ -1,34 +1,100 @@
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 = PRESET_COLORS.findIndex((p) => p.color.r === ledColor.r &&
51
+ p.color.g === ledColor.g &&
52
+ p.color.b === ledColor.b);
53
+ let nextIndex = currentColorIndex;
54
+ if (key.leftArrow || input === "h") {
55
+ nextIndex = currentColorIndex - 1;
56
+ if (nextIndex < 0)
57
+ nextIndex = PRESET_COLORS.length - 1;
58
+ }
59
+ else {
60
+ nextIndex = currentColorIndex + 1;
61
+ if (nextIndex >= PRESET_COLORS.length)
62
+ nextIndex = 0;
63
+ }
64
+ onColorChange(PRESET_COLORS[nextIndex].color);
65
+ }
66
+ }
67
+ else {
68
+ // Edit Preset
69
+ const presetIndex = selectedIndex - 2;
70
+ const preset = presets[presetIndex];
71
+ let delta = 0;
72
+ if (key.leftArrow || input === "h") {
73
+ delta = -0.5;
74
+ }
75
+ else if (key.rightArrow || input === "l") {
76
+ delta = 0.5;
77
+ }
78
+ if (delta !== 0 && preset) {
79
+ onPresetUpdate({
80
+ ...preset,
81
+ temperature: preset.temperature + delta,
82
+ });
83
+ }
84
+ }
29
85
  }, { 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"] }) })] }));
86
+ const currentColorPreset = PRESET_COLORS.find((p) => p.color.r === ledColor.r &&
87
+ p.color.g === ledColor.g &&
88
+ p.color.b === ledColor.b);
89
+ const colorDisplayValue = currentColorPreset
90
+ ? currentColorPreset.name
91
+ : rgbToHex(ledColor.r, ledColor.g, ledColor.b);
92
+ 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
93
+ ? "Celsius (°C)"
94
+ : "Fahrenheit (°F)", isSelected: selectedIndex === 0, hint: "Enter/Space to toggle", theme: theme }), _jsx(SettingRow, { label: "LED Color", value: colorDisplayValue, valueColor: rgbToHex(ledColor.r, ledColor.g, ledColor.b), 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
95
+ ? theme.primary
96
+ : 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
97
  }
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, ")"] })] }) }));
98
+ function SettingRow({ label, value, valueColor, isSelected, hint, theme, }) {
99
+ 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
100
  }
@@ -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,15 @@
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;
8
13
  }
9
- export declare function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, }: TemperatureDisplayProps): React.ReactElement;
14
+ export declare function TemperatureDisplay({ currentTemp, targetTemp, liquidState, temperatureUnit, width, height, theme, }: TemperatureDisplayProps): React.ReactElement;
10
15
  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, }) {
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
9
  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
- }
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;
@@ -1,6 +1,6 @@
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
4
  const initialState = {
5
5
  connected: false,
6
6
  batteryLevel: 0,
@@ -10,11 +10,11 @@ const initialState = {
10
10
  liquidState: LiquidState.Empty,
11
11
  temperatureUnit: TemperatureUnit.Celsius,
12
12
  color: { r: 255, g: 147, b: 41, a: 255 },
13
- mugName: '',
13
+ mugName: "",
14
14
  };
15
15
  export function useMug() {
16
16
  const [state, setState] = useState(initialState);
17
- const [isScanning, setIsScanning] = useState(false);
17
+ const [isScanning, setIsScanning] = useState(true);
18
18
  const [error, setError] = useState(null);
19
19
  const [foundMugName, setFoundMugName] = useState(null);
20
20
  useEffect(() => {
@@ -37,19 +37,19 @@ export function useMug() {
37
37
  const handleDisconnected = () => {
38
38
  setFoundMugName(null);
39
39
  };
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);
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);
46
46
  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);
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);
53
53
  };
54
54
  }, []);
55
55
  const startScanning = useCallback(async () => {
@@ -59,7 +59,7 @@ export function useMug() {
59
59
  await manager.startScanning();
60
60
  }
61
61
  catch (err) {
62
- setError(err instanceof Error ? err.message : 'Failed to start scanning');
62
+ setError(err instanceof Error ? err.message : "Failed to start scanning");
63
63
  }
64
64
  }, []);
65
65
  const setTargetTemp = useCallback(async (temp) => {
@@ -68,7 +68,7 @@ export function useMug() {
68
68
  await manager.setTargetTemp(temp);
69
69
  }
70
70
  catch (err) {
71
- setError(err instanceof Error ? err.message : 'Failed to set temperature');
71
+ setError(err instanceof Error ? err.message : "Failed to set temperature");
72
72
  }
73
73
  }, []);
74
74
  const setTemperatureUnit = useCallback(async (unit) => {
@@ -77,7 +77,7 @@ export function useMug() {
77
77
  await manager.setTemperatureUnit(unit);
78
78
  }
79
79
  catch (err) {
80
- setError(err instanceof Error ? err.message : 'Failed to set temperature unit');
80
+ setError(err instanceof Error ? err.message : "Failed to set temperature unit");
81
81
  }
82
82
  }, []);
83
83
  const setLedColor = useCallback(async (color) => {
@@ -86,7 +86,7 @@ export function useMug() {
86
86
  await manager.setLedColor(color);
87
87
  }
88
88
  catch (err) {
89
- setError(err instanceof Error ? err.message : 'Failed to set LED color');
89
+ setError(err instanceof Error ? err.message : "Failed to set LED color");
90
90
  }
91
91
  }, []);
92
92
  const disconnect = useCallback(async () => {
@@ -95,7 +95,7 @@ export function useMug() {
95
95
  await manager.disconnect();
96
96
  }
97
97
  catch (err) {
98
- setError(err instanceof Error ? err.message : 'Failed to disconnect');
98
+ setError(err instanceof Error ? err.message : "Failed to disconnect");
99
99
  }
100
100
  }, []);
101
101
  return {
@@ -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;