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
package/dist/lib/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- import { TemperatureUnit, LiquidState, MIN_TEMP_CELSIUS, MAX_TEMP_CELSIUS, BATTERY_DRAIN_RATE_HEATING, BATTERY_DRAIN_RATE_MAINTAINING, BATTERY_CHARGE_RATE, } from './types.js';
1
+ import { TemperatureUnit, LiquidState, MIN_TEMP_CELSIUS, MAX_TEMP_CELSIUS, BATTERY_DRAIN_RATE_HEATING, BATTERY_DRAIN_RATE_MAINTAINING, BATTERY_CHARGE_RATE, } from "./types.js";
2
2
  export function formatTemperature(temp, unit) {
3
3
  if (unit === TemperatureUnit.Celsius) {
4
4
  return `${temp.toFixed(1)}°C`;
@@ -13,52 +13,36 @@ export function fahrenheitToCelsius(fahrenheit) {
13
13
  return ((fahrenheit - 32) * 5) / 9;
14
14
  }
15
15
  export function formatBatteryLevel(level) {
16
- return `${level}%`;
16
+ return `${Math.round(level)}%`;
17
17
  }
18
18
  export function getBatteryIcon(level, isCharging) {
19
19
  if (isCharging) {
20
- return '⚡';
20
+ return "~";
21
21
  }
22
22
  if (level >= 75)
23
- return '████';
23
+ return "||||";
24
24
  if (level >= 50)
25
- return '███░';
25
+ return "|||.";
26
26
  if (level >= 25)
27
- return '██░░';
27
+ return "||..";
28
28
  if (level >= 10)
29
- return '█░░░';
30
- return '░░░░';
29
+ return "|...";
30
+ return "....";
31
31
  }
32
32
  export function getLiquidStateText(state) {
33
33
  switch (state) {
34
34
  case LiquidState.Empty:
35
- return 'Empty';
35
+ return "Empty";
36
36
  case LiquidState.Filling:
37
- return 'Filling';
37
+ return "Filling";
38
38
  case LiquidState.Cooling:
39
- return 'Cooling';
39
+ return "Cooling";
40
40
  case LiquidState.Heating:
41
- return 'Heating';
41
+ return "Heating";
42
42
  case LiquidState.StableTemperature:
43
- return 'Perfect';
43
+ return "Perfect Temperature";
44
44
  default:
45
- return 'Unknown';
46
- }
47
- }
48
- export function getLiquidStateIcon(state) {
49
- switch (state) {
50
- case LiquidState.Empty:
51
- return '○';
52
- case LiquidState.Filling:
53
- return '◐';
54
- case LiquidState.Cooling:
55
- return '❄';
56
- case LiquidState.Heating:
57
- return '🔥';
58
- case LiquidState.StableTemperature:
59
- return '✓';
60
- default:
61
- return '?';
45
+ return "Unknown";
62
46
  }
63
47
  }
64
48
  export function clampTemperature(temp) {
@@ -66,7 +50,7 @@ export function clampTemperature(temp) {
66
50
  }
67
51
  export function formatDuration(minutes) {
68
52
  if (minutes < 1) {
69
- return '< 1 min';
53
+ return "< 1 min";
70
54
  }
71
55
  if (minutes < 60) {
72
56
  return `${Math.round(minutes)} min`;
@@ -78,7 +62,7 @@ export function formatDuration(minutes) {
78
62
  }
79
63
  return `${hours}h ${mins}m`;
80
64
  }
81
- export function estimateTimeToTargetTemp(currentTemp, targetTemp, liquidState) {
65
+ export function estimateTimeToTargetTemp(currentTemp, targetTemp, liquidState, dynamicRate) {
82
66
  if (liquidState === LiquidState.Empty) {
83
67
  return null;
84
68
  }
@@ -86,15 +70,28 @@ export function estimateTimeToTargetTemp(currentTemp, targetTemp, liquidState) {
86
70
  if (tempDiff < 0.5) {
87
71
  return 0; // Already at target
88
72
  }
89
- // Estimate based on typical Ember mug heating/cooling rates
90
- // Heating: approximately 1°C per minute
91
- // Cooling: approximately 0.5°C per minute (slower due to insulation)
73
+ // Use dynamic rate if provided and valid (greater than a small threshold)
74
+ if (dynamicRate && Math.abs(dynamicRate) > 0.05) {
75
+ // Ensure the rate is in the right direction
76
+ const isHeating = currentTemp < targetTemp;
77
+ const isRateHeating = dynamicRate > 0;
78
+ if (isHeating === isRateHeating) {
79
+ return tempDiff / Math.abs(dynamicRate);
80
+ }
81
+ }
82
+ // Fallback to typical Ember mug heating/cooling rates
92
83
  const isHeating = currentTemp < targetTemp;
93
84
  const ratePerMinute = isHeating ? 1.0 : 0.5;
94
85
  return tempDiff / ratePerMinute;
95
86
  }
96
- export function estimateBatteryLife(batteryLevel, isCharging, liquidState) {
87
+ export function estimateBatteryLife(batteryLevel, isCharging, liquidState, dynamicRate) {
97
88
  if (isCharging) {
89
+ if (dynamicRate && dynamicRate > 0.1) {
90
+ const remaining = 100 - batteryLevel;
91
+ if (remaining <= 0)
92
+ return 0;
93
+ return remaining / dynamicRate;
94
+ }
98
95
  // Estimate time to full charge
99
96
  const remaining = 100 - batteryLevel;
100
97
  if (remaining <= 0)
@@ -104,6 +101,10 @@ export function estimateBatteryLife(batteryLevel, isCharging, liquidState) {
104
101
  if (batteryLevel <= 0) {
105
102
  return 0;
106
103
  }
104
+ // Use dynamic rate if provided and discharging
105
+ if (dynamicRate && dynamicRate < -0.01) {
106
+ return batteryLevel / Math.abs(dynamicRate);
107
+ }
107
108
  // Determine drain rate based on mug state
108
109
  let drainRate;
109
110
  switch (liquidState) {
@@ -119,15 +120,37 @@ export function estimateBatteryLife(batteryLevel, isCharging, liquidState) {
119
120
  }
120
121
  return batteryLevel / drainRate;
121
122
  }
123
+ export function calculateRate(history, windowMs = 2 * 60 * 1000) {
124
+ if (history.length < 2)
125
+ return 0;
126
+ const now = Date.now();
127
+ const windowStart = now - windowMs;
128
+ const windowHistory = history.filter((item) => item.time >= windowStart);
129
+ if (windowHistory.length < 2) {
130
+ // If not enough data in the current window, use the last two points overall if they are recent enough
131
+ const last = history[history.length - 1];
132
+ const secondLast = history[history.length - 2];
133
+ if (now - last.time > windowMs)
134
+ return 0;
135
+ const valDiff = last.value - secondLast.value;
136
+ const timeDiffMin = (last.time - secondLast.time) / (1000 * 60);
137
+ return timeDiffMin > 0 ? valDiff / timeDiffMin : 0;
138
+ }
139
+ const first = windowHistory[0];
140
+ const last = windowHistory[windowHistory.length - 1];
141
+ const valDiff = last.value - first.value;
142
+ const timeDiffMin = (last.time - first.time) / (1000 * 60);
143
+ return timeDiffMin > 0 ? valDiff / timeDiffMin : 0;
144
+ }
122
145
  export function getTemperatureColor(currentTemp, targetTemp) {
123
146
  const diff = currentTemp - targetTemp;
124
147
  if (Math.abs(diff) < 1) {
125
- return 'green'; // At target
148
+ return "green"; // At target
126
149
  }
127
150
  if (diff > 0) {
128
- return 'red'; // Too hot
151
+ return "red"; // Too hot
129
152
  }
130
- return 'blue'; // Too cold
153
+ return "blue"; // Too cold
131
154
  }
132
155
  export function interpolateColor(value, minColor, maxColor) {
133
156
  const clampedValue = Math.max(0, Math.min(1, value));
@@ -138,7 +161,7 @@ export function interpolateColor(value, minColor, maxColor) {
138
161
  ];
139
162
  }
140
163
  export function rgbToHex(r, g, b) {
141
- return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
164
+ return `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
142
165
  }
143
166
  export function hexToRgb(hex) {
144
167
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-mug",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "A CLI app for controlling Ember mugs via Bluetooth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,10 +15,12 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsc",
18
+ "postbuild": "chmod +x dist/cli.js",
18
19
  "clean": "rm -rf dist",
19
20
  "prebuild": "npm run clean",
20
21
  "start": "node dist/cli.js",
21
22
  "dev": "tsx src/cli.tsx",
23
+ "dev-mocked": "EMBER_MOCK=true tsx src/cli.tsx",
22
24
  "prepublishOnly": "npm run build",
23
25
  "version": "npm run build"
24
26
  },
@@ -1,9 +0,0 @@
1
- import React from 'react';
2
- import { RGBColor } from '../lib/types.js';
3
- interface ColorControlProps {
4
- color: RGBColor;
5
- onColorChange: (color: RGBColor) => void;
6
- isActive: boolean;
7
- }
8
- export declare function ColorControl({ color, onColorChange, isActive, }: ColorControlProps): React.ReactElement;
9
- export {};
@@ -1,71 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import { rgbToHex } from '../lib/utils.js';
5
- const PRESET_COLORS = [
6
- { name: 'Orange', color: { r: 255, g: 147, b: 41, a: 255 } },
7
- { name: 'Red', color: { r: 255, g: 0, b: 0, a: 255 } },
8
- { name: 'Green', color: { r: 0, g: 255, b: 0, a: 255 } },
9
- { name: 'Blue', color: { r: 0, g: 128, b: 255, a: 255 } },
10
- { name: 'Purple', color: { r: 128, g: 0, b: 255, a: 255 } },
11
- { name: 'Pink', color: { r: 255, g: 105, b: 180, a: 255 } },
12
- { name: 'White', color: { r: 255, g: 255, b: 255, a: 255 } },
13
- { name: 'Teal', color: { r: 0, g: 255, b: 200, a: 255 } },
14
- ];
15
- export function ColorControl({ color, onColorChange, isActive, }) {
16
- const [selectedChannel, setSelectedChannel] = useState('r');
17
- const [customMode, setCustomMode] = useState(false);
18
- useInput((input, key) => {
19
- if (!isActive)
20
- return;
21
- // Number keys for preset colors
22
- const numKey = parseInt(input, 10);
23
- if (numKey >= 1 && numKey <= PRESET_COLORS.length) {
24
- onColorChange(PRESET_COLORS[numKey - 1].color);
25
- setCustomMode(false);
26
- return;
27
- }
28
- // Toggle custom mode
29
- if (input === 'c') {
30
- setCustomMode(!customMode);
31
- return;
32
- }
33
- if (customMode) {
34
- // Switch channels
35
- if (input === 'r') {
36
- setSelectedChannel('r');
37
- }
38
- else if (input === 'g') {
39
- setSelectedChannel('g');
40
- }
41
- else if (input === 'b') {
42
- setSelectedChannel('b');
43
- }
44
- // Adjust selected channel
45
- let delta = 0;
46
- if (key.leftArrow || input === 'h') {
47
- delta = -10;
48
- }
49
- else if (key.rightArrow || input === 'l') {
50
- delta = 10;
51
- }
52
- else if (key.upArrow || input === 'k') {
53
- delta = 25;
54
- }
55
- else if (key.downArrow || input === 'j') {
56
- delta = -25;
57
- }
58
- if (delta !== 0) {
59
- const newColor = { ...color };
60
- newColor[selectedChannel] = Math.max(0, Math.min(255, newColor[selectedChannel] + delta));
61
- onColorChange(newColor);
62
- }
63
- }
64
- }, { isActive });
65
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { justifyContent: "center", children: _jsx(Text, { bold: true, children: "LED Color" }) }), _jsx(Box, { justifyContent: "center", marginY: 1, children: _jsxs(Text, { children: ["Current: ", _jsx(Text, { color: rgbToHex(color.r, color.g, color.b), children: "\u25CF" }), ' ', _jsxs(Text, { dimColor: true, children: ["(", rgbToHex(color.r, color.g, color.b), ")"] })] }) }), _jsx(Box, { justifyContent: "center", gap: 1, marginY: 1, children: PRESET_COLORS.map((preset, index) => (_jsxs(Box, { children: [_jsx(Text, { color: rgbToHex(preset.color.r, preset.color.g, preset.color.b), children: "\u25CF" }), _jsx(Text, { dimColor: true, children: index + 1 })] }, preset.name))) }), customMode && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "Custom Color Mode" }) }), _jsxs(Box, { justifyContent: "center", gap: 2, children: [_jsx(ChannelSlider, { label: "R", value: color.r, isSelected: selectedChannel === 'r', color: "red" }), _jsx(ChannelSlider, { label: "G", value: color.g, isSelected: selectedChannel === 'g', color: "green" }), _jsx(ChannelSlider, { label: "B", value: color.b, isSelected: selectedChannel === 'b', color: "blue" })] })] })), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press ", _jsxs(Text, { color: "cyan", children: ["1-", PRESET_COLORS.length] }), " for presets |", ' ', _jsx(Text, { color: "cyan", children: "c" }), " for custom mode"] }) })] }));
66
- }
67
- function ChannelSlider({ label, value, isSelected, color, }) {
68
- const width = 10;
69
- const filled = Math.round((value / 255) * width);
70
- return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? color : 'gray', children: [label, ": ", '█'.repeat(filled), '░'.repeat(width - filled), " ", value.toString().padStart(3)] }) }));
71
- }