@moldable-ai/ui 0.2.17 → 0.2.19

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 (232) hide show
  1. package/dist/components/chat/chat-panel.d.ts +18 -12
  2. package/dist/components/chat/chat-panel.d.ts.map +1 -1
  3. package/dist/components/chat/chat-panel.js +73 -19
  4. package/dist/components/chat/index.d.ts +2 -1
  5. package/dist/components/chat/index.d.ts.map +1 -1
  6. package/dist/components/chat/index.js +2 -1
  7. package/dist/components/chat/model-effort-power-effects.d.ts +7 -0
  8. package/dist/components/chat/model-effort-power-effects.d.ts.map +1 -0
  9. package/dist/components/chat/model-effort-power-effects.js +54 -0
  10. package/dist/components/chat/model-effort-selector.d.ts +26 -0
  11. package/dist/components/chat/model-effort-selector.d.ts.map +1 -0
  12. package/dist/components/chat/model-effort-selector.js +179 -0
  13. package/dist/components/chat/model-effort-ultra-effects.d.ts +5 -0
  14. package/dist/components/chat/model-effort-ultra-effects.d.ts.map +1 -0
  15. package/dist/components/chat/model-effort-ultra-effects.js +56 -0
  16. package/dist/components/chat/tool-handlers.d.ts.map +1 -1
  17. package/dist/components/hardware/bluetooth-panel-helpers.d.ts +9 -0
  18. package/dist/components/hardware/bluetooth-panel-helpers.d.ts.map +1 -0
  19. package/dist/components/hardware/bluetooth-panel-helpers.js +37 -0
  20. package/dist/components/hardware/bluetooth-panel.d.ts +56 -0
  21. package/dist/components/hardware/bluetooth-panel.d.ts.map +1 -0
  22. package/dist/components/hardware/bluetooth-panel.js +470 -0
  23. package/dist/components/hardware/camera-preview.d.ts +39 -0
  24. package/dist/components/hardware/camera-preview.d.ts.map +1 -0
  25. package/dist/components/hardware/camera-preview.js +228 -0
  26. package/dist/components/hardware/capability-badge.d.ts +23 -0
  27. package/dist/components/hardware/capability-badge.d.ts.map +1 -0
  28. package/dist/components/hardware/capability-badge.js +39 -0
  29. package/dist/components/hardware/capability-matrix.d.ts +14 -0
  30. package/dist/components/hardware/capability-matrix.d.ts.map +1 -0
  31. package/dist/components/hardware/capability-matrix.js +97 -0
  32. package/dist/components/hardware/clipboard-button.d.ts +26 -0
  33. package/dist/components/hardware/clipboard-button.d.ts.map +1 -0
  34. package/dist/components/hardware/clipboard-button.js +114 -0
  35. package/dist/components/hardware/device-io-controls.d.ts +4 -0
  36. package/dist/components/hardware/device-io-controls.d.ts.map +1 -0
  37. package/dist/components/hardware/device-io-controls.js +28 -0
  38. package/dist/components/hardware/device-list.d.ts +20 -0
  39. package/dist/components/hardware/device-list.d.ts.map +1 -0
  40. package/dist/components/hardware/device-list.js +14 -0
  41. package/dist/components/hardware/display-map.d.ts +23 -0
  42. package/dist/components/hardware/display-map.d.ts.map +1 -0
  43. package/dist/components/hardware/display-map.js +114 -0
  44. package/dist/components/hardware/haptic-button.d.ts +26 -0
  45. package/dist/components/hardware/haptic-button.d.ts.map +1 -0
  46. package/dist/components/hardware/haptic-button.js +94 -0
  47. package/dist/components/hardware/hardware-panel.d.ts +11 -0
  48. package/dist/components/hardware/hardware-panel.d.ts.map +1 -0
  49. package/dist/components/hardware/hardware-panel.js +28 -0
  50. package/dist/components/hardware/hid-panel.d.ts +40 -0
  51. package/dist/components/hardware/hid-panel.d.ts.map +1 -0
  52. package/dist/components/hardware/hid-panel.js +373 -0
  53. package/dist/components/hardware/index.d.ts +27 -0
  54. package/dist/components/hardware/index.d.ts.map +1 -0
  55. package/dist/components/hardware/index.js +26 -0
  56. package/dist/components/hardware/live-indicator.d.ts +15 -0
  57. package/dist/components/hardware/live-indicator.d.ts.map +1 -0
  58. package/dist/components/hardware/live-indicator.js +26 -0
  59. package/dist/components/hardware/local-auth-button.d.ts +33 -0
  60. package/dist/components/hardware/local-auth-button.d.ts.map +1 -0
  61. package/dist/components/hardware/local-auth-button.js +115 -0
  62. package/dist/components/hardware/location-panel.d.ts +29 -0
  63. package/dist/components/hardware/location-panel.d.ts.map +1 -0
  64. package/dist/components/hardware/location-panel.js +154 -0
  65. package/dist/components/hardware/microphone-meter.d.ts +35 -0
  66. package/dist/components/hardware/microphone-meter.d.ts.map +1 -0
  67. package/dist/components/hardware/microphone-meter.js +244 -0
  68. package/dist/components/hardware/midi-monitor.d.ts +54 -0
  69. package/dist/components/hardware/midi-monitor.d.ts.map +1 -0
  70. package/dist/components/hardware/midi-monitor.js +534 -0
  71. package/dist/components/hardware/notification-panel.d.ts +24 -0
  72. package/dist/components/hardware/notification-panel.d.ts.map +1 -0
  73. package/dist/components/hardware/notification-panel.js +124 -0
  74. package/dist/components/hardware/power-blocker-controller.d.ts +19 -0
  75. package/dist/components/hardware/power-blocker-controller.d.ts.map +1 -0
  76. package/dist/components/hardware/power-blocker-controller.js +83 -0
  77. package/dist/components/hardware/power-panel.d.ts +67 -0
  78. package/dist/components/hardware/power-panel.d.ts.map +1 -0
  79. package/dist/components/hardware/power-panel.js +309 -0
  80. package/dist/components/hardware/readout.d.ts +10 -0
  81. package/dist/components/hardware/readout.d.ts.map +1 -0
  82. package/dist/components/hardware/readout.js +11 -0
  83. package/dist/components/hardware/screen-share-preview.d.ts +36 -0
  84. package/dist/components/hardware/screen-share-preview.d.ts.map +1 -0
  85. package/dist/components/hardware/screen-share-preview.js +191 -0
  86. package/dist/components/hardware/secure-storage-panel.d.ts +36 -0
  87. package/dist/components/hardware/secure-storage-panel.d.ts.map +1 -0
  88. package/dist/components/hardware/secure-storage-panel.js +193 -0
  89. package/dist/components/hardware/serial-console.d.ts +64 -0
  90. package/dist/components/hardware/serial-console.d.ts.map +1 -0
  91. package/dist/components/hardware/serial-console.js +399 -0
  92. package/dist/components/hardware/shortcut-recorder.d.ts +35 -0
  93. package/dist/components/hardware/shortcut-recorder.d.ts.map +1 -0
  94. package/dist/components/hardware/shortcut-recorder.js +217 -0
  95. package/dist/components/hardware/stream-log.d.ts +35 -0
  96. package/dist/components/hardware/stream-log.d.ts.map +1 -0
  97. package/dist/components/hardware/stream-log.js +65 -0
  98. package/dist/components/hardware/system-audio-monitor.d.ts +34 -0
  99. package/dist/components/hardware/system-audio-monitor.d.ts.map +1 -0
  100. package/dist/components/hardware/system-audio-monitor.js +275 -0
  101. package/dist/components/hardware/usb-panel.d.ts +54 -0
  102. package/dist/components/hardware/usb-panel.d.ts.map +1 -0
  103. package/dist/components/hardware/usb-panel.js +363 -0
  104. package/dist/components/hardware/use-capability.d.ts +20 -0
  105. package/dist/components/hardware/use-capability.d.ts.map +1 -0
  106. package/dist/components/hardware/use-capability.js +61 -0
  107. package/dist/components/ui/input-group.d.ts +1 -1
  108. package/dist/components/ui/slider.d.ts +4 -1
  109. package/dist/components/ui/slider.d.ts.map +1 -1
  110. package/dist/components/ui/slider.js +2 -2
  111. package/dist/index.d.ts +3 -1
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +5 -1
  114. package/dist/lib/commands.d.ts +59 -0
  115. package/dist/lib/commands.d.ts.map +1 -1
  116. package/dist/lib/commands.js +97 -0
  117. package/dist/lib/native-hardware/bluetooth-access.d.ts +14 -0
  118. package/dist/lib/native-hardware/bluetooth-access.d.ts.map +1 -0
  119. package/dist/lib/native-hardware/bluetooth-access.js +40 -0
  120. package/dist/lib/native-hardware/bluetooth-connection.d.ts +19 -0
  121. package/dist/lib/native-hardware/bluetooth-connection.d.ts.map +1 -0
  122. package/dist/lib/native-hardware/bluetooth-connection.js +88 -0
  123. package/dist/lib/native-hardware/bluetooth-gatt.d.ts +7 -0
  124. package/dist/lib/native-hardware/bluetooth-gatt.d.ts.map +1 -0
  125. package/dist/lib/native-hardware/bluetooth-gatt.js +77 -0
  126. package/dist/lib/native-hardware/bluetooth-internal.d.ts +10 -0
  127. package/dist/lib/native-hardware/bluetooth-internal.d.ts.map +1 -0
  128. package/dist/lib/native-hardware/bluetooth-internal.js +105 -0
  129. package/dist/lib/native-hardware/bluetooth-native-fallback.d.ts +33 -0
  130. package/dist/lib/native-hardware/bluetooth-native-fallback.d.ts.map +1 -0
  131. package/dist/lib/native-hardware/bluetooth-native-fallback.js +214 -0
  132. package/dist/lib/native-hardware/bluetooth-types.d.ts +85 -0
  133. package/dist/lib/native-hardware/bluetooth-types.d.ts.map +1 -0
  134. package/dist/lib/native-hardware/bluetooth-types.js +2 -0
  135. package/dist/lib/native-hardware/bluetooth.d.ts +7 -0
  136. package/dist/lib/native-hardware/bluetooth.d.ts.map +1 -0
  137. package/dist/lib/native-hardware/bluetooth.js +4 -0
  138. package/dist/lib/native-hardware/bridge.d.ts +23 -0
  139. package/dist/lib/native-hardware/bridge.d.ts.map +1 -0
  140. package/dist/lib/native-hardware/bridge.js +266 -0
  141. package/dist/lib/native-hardware/capabilities.d.ts +7 -0
  142. package/dist/lib/native-hardware/capabilities.d.ts.map +1 -0
  143. package/dist/lib/native-hardware/capabilities.js +95 -0
  144. package/dist/lib/native-hardware/clipboard.d.ts +11 -0
  145. package/dist/lib/native-hardware/clipboard.d.ts.map +1 -0
  146. package/dist/lib/native-hardware/clipboard.js +55 -0
  147. package/dist/lib/native-hardware/device-io-errors.d.ts +11 -0
  148. package/dist/lib/native-hardware/device-io-errors.d.ts.map +1 -0
  149. package/dist/lib/native-hardware/device-io-errors.js +82 -0
  150. package/dist/lib/native-hardware/displays.d.ts +38 -0
  151. package/dist/lib/native-hardware/displays.d.ts.map +1 -0
  152. package/dist/lib/native-hardware/displays.js +12 -0
  153. package/dist/lib/native-hardware/global-shortcuts.d.ts +86 -0
  154. package/dist/lib/native-hardware/global-shortcuts.d.ts.map +1 -0
  155. package/dist/lib/native-hardware/global-shortcuts.js +351 -0
  156. package/dist/lib/native-hardware/haptics.d.ts +25 -0
  157. package/dist/lib/native-hardware/haptics.d.ts.map +1 -0
  158. package/dist/lib/native-hardware/haptics.js +18 -0
  159. package/dist/lib/native-hardware/hid-native.d.ts +26 -0
  160. package/dist/lib/native-hardware/hid-native.d.ts.map +1 -0
  161. package/dist/lib/native-hardware/hid-native.js +229 -0
  162. package/dist/lib/native-hardware/hid.d.ts +52 -0
  163. package/dist/lib/native-hardware/hid.d.ts.map +1 -0
  164. package/dist/lib/native-hardware/hid.js +160 -0
  165. package/dist/lib/native-hardware/index.d.ts +38 -0
  166. package/dist/lib/native-hardware/index.d.ts.map +1 -0
  167. package/dist/lib/native-hardware/index.js +20 -0
  168. package/dist/lib/native-hardware/local-authentication.d.ts +32 -0
  169. package/dist/lib/native-hardware/local-authentication.d.ts.map +1 -0
  170. package/dist/lib/native-hardware/local-authentication.js +46 -0
  171. package/dist/lib/native-hardware/location.d.ts +49 -0
  172. package/dist/lib/native-hardware/location.d.ts.map +1 -0
  173. package/dist/lib/native-hardware/location.js +114 -0
  174. package/dist/lib/native-hardware/media-permissions.d.ts +28 -0
  175. package/dist/lib/native-hardware/media-permissions.d.ts.map +1 -0
  176. package/dist/lib/native-hardware/media-permissions.js +37 -0
  177. package/dist/lib/native-hardware/media.d.ts +17 -0
  178. package/dist/lib/native-hardware/media.d.ts.map +1 -0
  179. package/dist/lib/native-hardware/media.js +99 -0
  180. package/dist/lib/native-hardware/midi-access.d.ts +13 -0
  181. package/dist/lib/native-hardware/midi-access.d.ts.map +1 -0
  182. package/dist/lib/native-hardware/midi-access.js +66 -0
  183. package/dist/lib/native-hardware/midi-internal.d.ts +12 -0
  184. package/dist/lib/native-hardware/midi-internal.d.ts.map +1 -0
  185. package/dist/lib/native-hardware/midi-internal.js +88 -0
  186. package/dist/lib/native-hardware/midi-native-fallback.d.ts +27 -0
  187. package/dist/lib/native-hardware/midi-native-fallback.d.ts.map +1 -0
  188. package/dist/lib/native-hardware/midi-native-fallback.js +127 -0
  189. package/dist/lib/native-hardware/midi-ports.d.ts +6 -0
  190. package/dist/lib/native-hardware/midi-ports.d.ts.map +1 -0
  191. package/dist/lib/native-hardware/midi-ports.js +115 -0
  192. package/dist/lib/native-hardware/midi-types.d.ts +25 -0
  193. package/dist/lib/native-hardware/midi-types.d.ts.map +1 -0
  194. package/dist/lib/native-hardware/midi-types.js +1 -0
  195. package/dist/lib/native-hardware/midi.d.ts +6 -0
  196. package/dist/lib/native-hardware/midi.d.ts.map +1 -0
  197. package/dist/lib/native-hardware/midi.js +3 -0
  198. package/dist/lib/native-hardware/native-device-io.d.ts +28 -0
  199. package/dist/lib/native-hardware/native-device-io.d.ts.map +1 -0
  200. package/dist/lib/native-hardware/native-device-io.js +91 -0
  201. package/dist/lib/native-hardware/native-device-lifecycle.d.ts +13 -0
  202. package/dist/lib/native-hardware/native-device-lifecycle.d.ts.map +1 -0
  203. package/dist/lib/native-hardware/native-device-lifecycle.js +40 -0
  204. package/dist/lib/native-hardware/notifications.d.ts +38 -0
  205. package/dist/lib/native-hardware/notifications.d.ts.map +1 -0
  206. package/dist/lib/native-hardware/notifications.js +50 -0
  207. package/dist/lib/native-hardware/power-session.d.ts +113 -0
  208. package/dist/lib/native-hardware/power-session.d.ts.map +1 -0
  209. package/dist/lib/native-hardware/power-session.js +264 -0
  210. package/dist/lib/native-hardware/secure-storage.d.ts +36 -0
  211. package/dist/lib/native-hardware/secure-storage.d.ts.map +1 -0
  212. package/dist/lib/native-hardware/secure-storage.js +64 -0
  213. package/dist/lib/native-hardware/serial-native.d.ts +23 -0
  214. package/dist/lib/native-hardware/serial-native.d.ts.map +1 -0
  215. package/dist/lib/native-hardware/serial-native.js +181 -0
  216. package/dist/lib/native-hardware/serial.d.ts +76 -0
  217. package/dist/lib/native-hardware/serial.d.ts.map +1 -0
  218. package/dist/lib/native-hardware/serial.js +212 -0
  219. package/dist/lib/native-hardware/system-audio.d.ts +127 -0
  220. package/dist/lib/native-hardware/system-audio.d.ts.map +1 -0
  221. package/dist/lib/native-hardware/system-audio.js +330 -0
  222. package/dist/lib/native-hardware/types.d.ts +62 -0
  223. package/dist/lib/native-hardware/types.d.ts.map +1 -0
  224. package/dist/lib/native-hardware/types.js +27 -0
  225. package/dist/lib/native-hardware/usb-native.d.ts +39 -0
  226. package/dist/lib/native-hardware/usb-native.d.ts.map +1 -0
  227. package/dist/lib/native-hardware/usb-native.js +217 -0
  228. package/dist/lib/native-hardware/usb.d.ts +85 -0
  229. package/dist/lib/native-hardware/usb.d.ts.map +1 -0
  230. package/dist/lib/native-hardware/usb.js +222 -0
  231. package/package.json +1 -1
  232. package/src/styles/index.css +377 -0
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Bell, Send } from 'lucide-react';
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import { NATIVE_NOTIFICATION_MAX_BODY_LENGTH, NATIVE_NOTIFICATION_MAX_TAG_LENGTH, NATIVE_NOTIFICATION_MAX_TITLE_LENGTH, getMoldableNotificationPermission, requestMoldableNotificationPermission, sendMoldableNotification, } from '../../lib/native-hardware';
6
+ import { Button } from '../ui/button';
7
+ import { Input } from '../ui/input';
8
+ import { Spinner } from '../ui/spinner';
9
+ import { Switch } from '../ui/switch';
10
+ import { CapabilityBadge } from './capability-badge';
11
+ import { HardwarePanel, HardwarePanelAction, HardwarePanelContent, HardwarePanelFooter, HardwarePanelHeader, HardwarePanelIcon, HardwarePanelTitle, } from './hardware-panel';
12
+ import { useMoldableCapability, } from './use-capability';
13
+ /**
14
+ * Notification state machine behind `NotificationsPanel`: permission loading,
15
+ * explicit permission requests, and sends with the library's length limits
16
+ * enforced by truncation.
17
+ */
18
+ export function useMoldableNotifications() {
19
+ const [permission, setPermission] = useState('unknown');
20
+ const [status, setStatus] = useState('idle');
21
+ const [sentAt, setSentAt] = useState(null);
22
+ const [error, setError] = useState(null);
23
+ const tokenRef = useRef(0);
24
+ useEffect(() => {
25
+ let live = true;
26
+ getMoldableNotificationPermission()
27
+ .then((result) => {
28
+ if (live)
29
+ setPermission(result.permission);
30
+ })
31
+ .catch(() => {
32
+ // Best-effort: outside Moldable the permission stays unknown.
33
+ });
34
+ return () => {
35
+ live = false;
36
+ };
37
+ }, []);
38
+ const request = useCallback(async () => {
39
+ const token = ++tokenRef.current;
40
+ try {
41
+ const result = await requestMoldableNotificationPermission();
42
+ if (tokenRef.current !== token)
43
+ return null;
44
+ setPermission(result.permission);
45
+ return result.permission;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }, []);
51
+ const send = useCallback(async (options) => {
52
+ const token = ++tokenRef.current;
53
+ setStatus('sending');
54
+ setError(null);
55
+ try {
56
+ await sendMoldableNotification({
57
+ ...options,
58
+ title: options.title.slice(0, NATIVE_NOTIFICATION_MAX_TITLE_LENGTH),
59
+ body: options.body?.slice(0, NATIVE_NOTIFICATION_MAX_BODY_LENGTH),
60
+ tag: options.tag?.slice(0, NATIVE_NOTIFICATION_MAX_TAG_LENGTH),
61
+ });
62
+ if (tokenRef.current !== token)
63
+ return false;
64
+ setSentAt(Date.now());
65
+ setStatus('sent');
66
+ return true;
67
+ }
68
+ catch (cause) {
69
+ if (tokenRef.current !== token)
70
+ return false;
71
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
72
+ setStatus('error');
73
+ return false;
74
+ }
75
+ }, []);
76
+ useEffect(() => () => {
77
+ tokenRef.current++;
78
+ }, []);
79
+ return { permission, status, sentAt, error, request, send };
80
+ }
81
+ const PERMISSION_CHIP = {
82
+ granted: { status: 'supported', label: 'Granted' },
83
+ prompt: { status: 'partial', label: 'Not requested' },
84
+ denied: { status: 'unsupported', label: 'Denied' },
85
+ };
86
+ function formatClock(timestamp) {
87
+ const date = new Date(timestamp);
88
+ const pad = (value) => value.toString().padStart(2, '0');
89
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
90
+ }
91
+ /**
92
+ * Notification composer panel: permission chip with an inline allow flow, a
93
+ * compact title/body/silent composer, and delivery feedback.
94
+ */
95
+ function NotificationsPanel({ className, ...props }) {
96
+ const notifications = useMoldableNotifications();
97
+ const capability = useMoldableCapability('notifications');
98
+ const [title, setTitle] = useState('Hello from Moldable');
99
+ const [body, setBody] = useState('');
100
+ const [silent, setSilent] = useState(false);
101
+ const [requesting, setRequesting] = useState(false);
102
+ const sending = notifications.status === 'sending';
103
+ const canSend = title.trim().length > 0 && !sending;
104
+ const sendNotification = async () => {
105
+ await notifications.send({
106
+ title,
107
+ body: body.trim().length > 0 ? body : undefined,
108
+ tag: 'moldable-hardware',
109
+ silent,
110
+ });
111
+ };
112
+ const requestPermission = async () => {
113
+ setRequesting(true);
114
+ await notifications.request();
115
+ setRequesting(false);
116
+ };
117
+ const chip = notifications.permission === 'unknown'
118
+ ? null
119
+ : PERMISSION_CHIP[notifications.permission];
120
+ return (_jsxs(HardwarePanel, { "data-slot": "notifications-panel", "data-status": notifications.status, "data-permission": notifications.permission, className: className, ...props, children: [_jsxs(HardwarePanelHeader, { children: [_jsx(HardwarePanelIcon, { children: _jsx(Bell, {}) }), _jsx(HardwarePanelTitle, { children: "Notifications" }), _jsx(CapabilityBadge, { capability: "notifications" }), _jsx(HardwarePanelAction, { children: notifications.permission === 'prompt' ? (_jsxs(Button, { size: "sm", variant: "outline", disabled: requesting, onClick: () => void requestPermission(), children: [requesting ? _jsx(Spinner, {}) : null, "Allow"] })) : chip ? (_jsx(CapabilityBadge, { status: chip.status, label: chip.label })) : null })] }), _jsxs(HardwarePanelContent, { className: "space-y-3", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Input, { value: title, maxLength: NATIVE_NOTIFICATION_MAX_TITLE_LENGTH, placeholder: "Title", "aria-label": "Notification title", onChange: (event) => setTitle(event.target.value) }), _jsx(Input, { value: body, maxLength: NATIVE_NOTIFICATION_MAX_BODY_LENGTH, placeholder: "Body (optional)", "aria-label": "Notification body", onChange: (event) => setBody(event.target.value) })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("label", { className: "text-muted-foreground flex items-center gap-2 text-xs", children: [_jsx(Switch, { size: "sm", checked: silent, onCheckedChange: setSilent }), "Silent"] }), _jsxs(Button, { size: "sm", disabled: !canSend, onClick: () => void sendNotification(), children: [sending ? _jsx(Spinner, {}) : _jsx(Send, {}), "Send notification"] })] }), notifications.status === 'sent' && notifications.sentAt !== null ? (_jsxs("p", { className: "text-success animate-in fade-in font-mono text-xs tabular-nums duration-300 motion-reduce:animate-none", children: ["Delivered ", formatClock(notifications.sentAt)] })) : notifications.status === 'error' ? (_jsx("p", { className: "text-status-error text-xs", children: notifications.error?.message ??
121
+ 'The notification could not be sent.' })) : null] }), _jsx(HardwarePanelFooter, { children: _jsx("span", { className: "truncate", children: capability.descriptor?.reason ??
122
+ 'Notifications appear via the OS notification center.' }) })] }));
123
+ }
124
+ export { NotificationsPanel };
@@ -0,0 +1,19 @@
1
+ import { type MoldableSleepBlocker, type MoldableSleepBlockerType } from '../../lib/native-hardware';
2
+ type StartSleepBlocker = (type: MoldableSleepBlockerType) => Promise<MoldableSleepBlocker>;
3
+ declare const BLOCKER_TYPES: readonly ["prevent-display-sleep", "prevent-app-suspension"];
4
+ /**
5
+ * Owns sleep-blocker handles independently from React state. Handles are only
6
+ * discarded after the host confirms they stopped, so a failed cleanup remains
7
+ * retryable.
8
+ */
9
+ export declare class MoldableSleepBlockerController {
10
+ #private;
11
+ private readonly startSleepBlocker;
12
+ constructor(startSleepBlocker?: StartSleepBlocker);
13
+ has(type: MoldableSleepBlockerType): boolean;
14
+ toggle(type: MoldableSleepBlockerType): Promise<boolean>;
15
+ check(type: MoldableSleepBlockerType): Promise<boolean>;
16
+ stopAll(): Promise<void>;
17
+ }
18
+ export { BLOCKER_TYPES };
19
+ //# sourceMappingURL=power-blocker-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"power-blocker-controller.d.ts","sourceRoot":"","sources":["../../../src/components/hardware/power-blocker-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAE9B,MAAM,2BAA2B,CAAA;AAElC,KAAK,iBAAiB,GAAG,CACvB,IAAI,EAAE,wBAAwB,KAC3B,OAAO,CAAC,oBAAoB,CAAC,CAAA;AAElC,QAAA,MAAM,aAAa,8DAGqC,CAAA;AAExD;;;;GAIG;AACH,qBAAa,8BAA8B;;IAkBvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;gBAAjB,iBAAiB,GAAE,iBAA6C;IAGnF,GAAG,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO;IAI5C,MAAM,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,OAAO,CAAC;IAmBxD,KAAK,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,OAAO,CAAC;IAYjD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CA+B/B;AAED,OAAO,EAAE,aAAa,EAAE,CAAA"}
@@ -0,0 +1,83 @@
1
+ import { startMoldableSleepBlocker, } from '../../lib/native-hardware';
2
+ const BLOCKER_TYPES = [
3
+ 'prevent-display-sleep',
4
+ 'prevent-app-suspension',
5
+ ];
6
+ /**
7
+ * Owns sleep-blocker handles independently from React state. Handles are only
8
+ * discarded after the host confirms they stopped, so a failed cleanup remains
9
+ * retryable.
10
+ */
11
+ export class MoldableSleepBlockerController {
12
+ startSleepBlocker;
13
+ #handles = {
14
+ 'prevent-display-sleep': null,
15
+ 'prevent-app-suspension': null,
16
+ };
17
+ #operations = {
18
+ 'prevent-display-sleep': null,
19
+ 'prevent-app-suspension': null,
20
+ };
21
+ constructor(startSleepBlocker = startMoldableSleepBlocker) {
22
+ this.startSleepBlocker = startSleepBlocker;
23
+ }
24
+ has(type) {
25
+ return this.#handles[type] !== null;
26
+ }
27
+ toggle(type) {
28
+ return this.#enqueue(type, async () => {
29
+ const current = this.#handles[type];
30
+ if (current) {
31
+ await current.stop();
32
+ if (this.#handles[type] === current)
33
+ this.#handles[type] = null;
34
+ return false;
35
+ }
36
+ const blocker = await this.startSleepBlocker(type);
37
+ this.#handles[type] = blocker;
38
+ const started = await blocker.isStarted();
39
+ if (!started && this.#handles[type] === blocker) {
40
+ this.#handles[type] = null;
41
+ }
42
+ return started;
43
+ });
44
+ }
45
+ check(type) {
46
+ return this.#enqueue(type, async () => {
47
+ const blocker = this.#handles[type];
48
+ if (!blocker)
49
+ return false;
50
+ const started = await blocker.isStarted();
51
+ if (!started && this.#handles[type] === blocker) {
52
+ this.#handles[type] = null;
53
+ }
54
+ return started;
55
+ });
56
+ }
57
+ async stopAll() {
58
+ const results = await Promise.allSettled(BLOCKER_TYPES.map((type) => this.#enqueue(type, async () => {
59
+ const blocker = this.#handles[type];
60
+ if (!blocker)
61
+ return;
62
+ await blocker.stop();
63
+ if (this.#handles[type] === blocker)
64
+ this.#handles[type] = null;
65
+ })));
66
+ const failed = results.find((result) => result.status === 'rejected');
67
+ if (failed)
68
+ throw failed.reason;
69
+ }
70
+ #enqueue(type, operation) {
71
+ const previous = this.#operations[type] ?? Promise.resolve();
72
+ const current = previous.catch(() => undefined).then(operation);
73
+ this.#operations[type] = current;
74
+ void current
75
+ .finally(() => {
76
+ if (this.#operations[type] === current)
77
+ this.#operations[type] = null;
78
+ })
79
+ .catch(() => undefined);
80
+ return current;
81
+ }
82
+ }
83
+ export { BLOCKER_TYPES };
@@ -0,0 +1,67 @@
1
+ import * as React from 'react';
2
+ import { type MoldablePowerSessionEvent, type MoldablePowerState, type MoldableSleepBlockerType, type MoldableSystemIdleState, type MoldableThermalState } from '../../lib/native-hardware';
3
+ export interface MoldablePowerSessionLogEntry {
4
+ /** Monotonic id, stable across the rolling window. */
5
+ id: number;
6
+ /** Epoch millis parsed from the event's `occurredAt`. */
7
+ timestamp: number;
8
+ event: MoldablePowerSessionEvent;
9
+ }
10
+ export interface MoldablePowerIdleSnapshot {
11
+ state: MoldableSystemIdleState;
12
+ seconds: number;
13
+ }
14
+ export interface UseMoldablePowerOptions {
15
+ /** Poll cadence for power/thermal/idle state. Defaults to 30 000 ms. */
16
+ pollIntervalMs?: number;
17
+ }
18
+ /**
19
+ * Power/session state machine behind `PowerPanel`: polled power + thermal +
20
+ * idle state, live session events, and a managed sleep blocker. Every native
21
+ * failure degrades to nulls so it is safe outside Moldable.
22
+ */
23
+ export declare function useMoldablePower(options?: UseMoldablePowerOptions): {
24
+ power: MoldablePowerState | null;
25
+ thermal: MoldableThermalState | null;
26
+ idle: MoldablePowerIdleSnapshot | null;
27
+ events: MoldablePowerSessionLogEntry[];
28
+ updatedAt: number | null;
29
+ error: Error | null;
30
+ refresh: () => void;
31
+ sleepBlocker: {
32
+ active: boolean;
33
+ type: "prevent-display-sleep" | null;
34
+ toggle: (type?: MoldableSleepBlockerType) => Promise<void>;
35
+ };
36
+ sleepBlockers: Record<MoldableSleepBlockerType, {
37
+ active: boolean;
38
+ busy: boolean;
39
+ toggle: () => Promise<void>;
40
+ check: () => Promise<void>;
41
+ }>;
42
+ };
43
+ export interface BatteryGaugeProps extends React.ComponentProps<'span'> {
44
+ /** Controlled charge percent (0-100). Omit all data props to self-manage. */
45
+ percent?: number;
46
+ /** Controlled charging flag. */
47
+ charging?: boolean;
48
+ /** The machine has no battery — renders an AC power presentation. */
49
+ noBattery?: boolean;
50
+ }
51
+ /**
52
+ * Compact inline battery glyph + percent. Pass `percent`/`charging` (and
53
+ * optionally `noBattery`) to control it, or no data props to let it poll
54
+ * `useMoldablePower` itself.
55
+ */
56
+ declare function BatteryGauge({ percent, charging, noBattery, ...props }: BatteryGaugeProps): import("react/jsx-runtime").JSX.Element;
57
+ export interface PowerPanelProps extends React.ComponentProps<'div'> {
58
+ /** Poll cadence for power/thermal/idle state. Defaults to 30 000 ms. */
59
+ pollIntervalMs?: number;
60
+ }
61
+ /**
62
+ * Power panel: battery gauge, source/thermal/idle readout, live session event
63
+ * log, and a "keep awake" sleep-blocker switch.
64
+ */
65
+ declare function PowerPanel({ className, pollIntervalMs, ...props }: PowerPanelProps): import("react/jsx-runtime").JSX.Element;
66
+ export { PowerPanel, BatteryGauge };
67
+ //# sourceMappingURL=power-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"power-panel.d.ts","sourceRoot":"","sources":["../../../src/components/hardware/power-panel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EACL,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAO1B,MAAM,2BAA2B,CAAA;AAuBlC,MAAM,WAAW,4BAA4B;IAC3C,sDAAsD;IACtD,EAAE,EAAE,MAAM,CAAA;IACV,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,yBAAyB,CAAA;CACjC;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,uBAAuB,CAAA;IAC9B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAMD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B;;;;;;;;;;;wBA0HrD,wBAAwB;;mBAwEhC,MAAM,CACT,wBAAwB,EACxB;QACE,MAAM,EAAE,OAAO,CAAA;QACf,IAAI,EAAE,OAAO,CAAA;QACb,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;KAC3B,CACF;EAEJ;AA0DD,MAAM,WAAW,iBAAkB,SAAQ,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC;IACrE,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AA2DD;;;;GAIG;AACH,iBAAS,YAAY,CAAC,EACpB,OAAO,EACP,QAAQ,EACR,SAAS,EACT,GAAG,KAAK,EACT,EAAE,iBAAiB,2CAgBnB;AAsCD,MAAM,WAAW,eAAgB,SAAQ,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;IAClE,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;GAGG;AACH,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,KAAK,EAAE,EAAE,eAAe,2CA0J3E;AAED,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,CAAA"}
@@ -0,0 +1,309 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { BatteryCharging, Plug, RefreshCw, Zap } from 'lucide-react';
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import { getMoldableCurrentThermalState, getMoldablePowerState, getMoldableSystemIdleState, getMoldableSystemIdleTime, watchMoldablePowerSession, } from '../../lib/native-hardware';
6
+ import { cn } from '../../lib/utils';
7
+ import { Button } from '../ui/button';
8
+ import { Switch } from '../ui/switch';
9
+ import { CapabilityBadge } from './capability-badge';
10
+ import { HardwarePanel, HardwarePanelAction, HardwarePanelContent, HardwarePanelFooter, HardwarePanelHeader, HardwarePanelIcon, HardwarePanelTitle, } from './hardware-panel';
11
+ import { BLOCKER_TYPES, MoldableSleepBlockerController, } from './power-blocker-controller';
12
+ import { Readout, ReadoutItem } from './readout';
13
+ import { StreamLog, useStreamLog } from './stream-log';
14
+ const MAX_POWER_EVENTS = 100;
15
+ function toError(cause) {
16
+ return cause instanceof Error ? cause : new Error(String(cause));
17
+ }
18
+ /**
19
+ * Power/session state machine behind `PowerPanel`: polled power + thermal +
20
+ * idle state, live session events, and a managed sleep blocker. Every native
21
+ * failure degrades to nulls so it is safe outside Moldable.
22
+ */
23
+ export function useMoldablePower(options = {}) {
24
+ const { pollIntervalMs = 30_000 } = options;
25
+ const [power, setPower] = useState(null);
26
+ const [thermal, setThermal] = useState(null);
27
+ const [idle, setIdle] = useState(null);
28
+ const [events, setEvents] = useState([]);
29
+ const [updatedAt, setUpdatedAt] = useState(null);
30
+ const [error, setError] = useState(null);
31
+ const [blockerActive, setBlockerActive] = useState({
32
+ 'prevent-display-sleep': false,
33
+ 'prevent-app-suspension': false,
34
+ });
35
+ const [blockerBusy, setBlockerBusy] = useState({
36
+ 'prevent-display-sleep': false,
37
+ 'prevent-app-suspension': false,
38
+ });
39
+ const generationRef = useRef(0);
40
+ const eventIdRef = useRef(0);
41
+ const mountedRef = useRef(true);
42
+ const blockerControllerRef = useRef(null);
43
+ blockerControllerRef.current ??= new MoldableSleepBlockerController();
44
+ const refresh = useCallback(() => {
45
+ const generation = ++generationRef.current;
46
+ void Promise.allSettled([
47
+ getMoldablePowerState(),
48
+ getMoldableCurrentThermalState(),
49
+ getMoldableSystemIdleState(60),
50
+ getMoldableSystemIdleTime(),
51
+ ]).then(([powerResult, thermalResult, idleStateResult, idleTimeResult]) => {
52
+ if (generationRef.current !== generation)
53
+ return;
54
+ setPower(powerResult.status === 'fulfilled' ? powerResult.value : null);
55
+ setThermal(thermalResult.status === 'fulfilled' ? thermalResult.value : null);
56
+ setIdle(idleStateResult.status === 'fulfilled'
57
+ ? {
58
+ state: idleStateResult.value.state,
59
+ seconds: idleTimeResult.status === 'fulfilled'
60
+ ? idleTimeResult.value
61
+ : idleStateResult.value.seconds,
62
+ }
63
+ : null);
64
+ const failed = [
65
+ powerResult,
66
+ thermalResult,
67
+ idleStateResult,
68
+ idleTimeResult,
69
+ ].find((result) => result.status === 'rejected');
70
+ setError(failed ? toError(failed.reason) : null);
71
+ setUpdatedAt(Date.now());
72
+ });
73
+ }, []);
74
+ useEffect(() => {
75
+ refresh();
76
+ const interval = window.setInterval(refresh, Math.max(1000, pollIntervalMs));
77
+ const liveGeneration = generationRef;
78
+ return () => {
79
+ window.clearInterval(interval);
80
+ liveGeneration.current++;
81
+ };
82
+ }, [pollIntervalMs, refresh]);
83
+ useEffect(() => {
84
+ let cancelled = false;
85
+ let stopWatching = null;
86
+ watchMoldablePowerSession((event) => {
87
+ if (cancelled)
88
+ return;
89
+ const parsed = Date.parse(event.occurredAt);
90
+ const entry = {
91
+ id: ++eventIdRef.current,
92
+ timestamp: Number.isNaN(parsed) ? Date.now() : parsed,
93
+ event,
94
+ };
95
+ setEvents((previous) => {
96
+ const next = [...previous, entry];
97
+ return next.length > MAX_POWER_EVENTS
98
+ ? next.slice(next.length - MAX_POWER_EVENTS)
99
+ : next;
100
+ });
101
+ if (event.kind === 'power-source-change')
102
+ setPower(event.powerState);
103
+ if (event.kind === 'thermal-state-change')
104
+ setThermal(event.state);
105
+ })
106
+ .then((stop) => {
107
+ if (cancelled) {
108
+ // Unmounted before the subscription resolved — stop immediately.
109
+ void stop().catch(() => undefined);
110
+ return;
111
+ }
112
+ stopWatching = stop;
113
+ })
114
+ .catch(() => {
115
+ // Session events are unavailable outside Moldable.
116
+ });
117
+ return () => {
118
+ cancelled = true;
119
+ if (stopWatching)
120
+ void stopWatching().catch(() => undefined);
121
+ };
122
+ }, []);
123
+ useEffect(() => {
124
+ mountedRef.current = true;
125
+ const liveMounted = mountedRef;
126
+ const controller = blockerControllerRef.current;
127
+ return () => {
128
+ liveMounted.current = false;
129
+ if (controller)
130
+ void controller.stopAll().catch(() => undefined);
131
+ };
132
+ }, []);
133
+ const toggleSleepBlocker = useCallback(async (type = 'prevent-display-sleep') => {
134
+ const controller = blockerControllerRef.current;
135
+ if (!controller)
136
+ return;
137
+ setBlockerBusy((previous) => ({ ...previous, [type]: true }));
138
+ try {
139
+ const active = await controller.toggle(type);
140
+ if (!mountedRef.current)
141
+ return;
142
+ setBlockerActive((previous) => ({ ...previous, [type]: active }));
143
+ setError(null);
144
+ }
145
+ catch (cause) {
146
+ if (!mountedRef.current)
147
+ return;
148
+ setBlockerActive((previous) => ({
149
+ ...previous,
150
+ [type]: controller.has(type),
151
+ }));
152
+ setError(toError(cause));
153
+ }
154
+ finally {
155
+ if (mountedRef.current) {
156
+ setBlockerBusy((previous) => ({ ...previous, [type]: false }));
157
+ }
158
+ }
159
+ }, []);
160
+ const checkSleepBlocker = useCallback(async (type) => {
161
+ const controller = blockerControllerRef.current;
162
+ if (!controller)
163
+ return;
164
+ setBlockerBusy((previous) => ({ ...previous, [type]: true }));
165
+ try {
166
+ const active = await controller.check(type);
167
+ if (!mountedRef.current)
168
+ return;
169
+ setBlockerActive((previous) => ({ ...previous, [type]: active }));
170
+ setError(null);
171
+ }
172
+ catch (cause) {
173
+ if (!mountedRef.current)
174
+ return;
175
+ setError(toError(cause));
176
+ }
177
+ finally {
178
+ if (mountedRef.current) {
179
+ setBlockerBusy((previous) => ({ ...previous, [type]: false }));
180
+ }
181
+ }
182
+ }, []);
183
+ return {
184
+ power,
185
+ thermal,
186
+ idle,
187
+ events,
188
+ updatedAt,
189
+ error,
190
+ refresh,
191
+ sleepBlocker: {
192
+ active: blockerActive['prevent-display-sleep'],
193
+ type: blockerActive['prevent-display-sleep']
194
+ ? 'prevent-display-sleep'
195
+ : null,
196
+ toggle: toggleSleepBlocker,
197
+ },
198
+ sleepBlockers: Object.fromEntries(BLOCKER_TYPES.map((type) => [
199
+ type,
200
+ {
201
+ active: blockerActive[type],
202
+ busy: blockerBusy[type],
203
+ toggle: () => toggleSleepBlocker(type),
204
+ check: () => checkSleepBlocker(type),
205
+ },
206
+ ])),
207
+ };
208
+ }
209
+ function BatteryGlyph({ percent, charging, className, }) {
210
+ const clamped = Math.max(0, Math.min(100, percent));
211
+ const fillClass = charging
212
+ ? 'text-status-running'
213
+ : clamped < 15
214
+ ? 'text-status-error'
215
+ : 'text-foreground';
216
+ return (_jsxs("svg", { viewBox: "0 0 28 14", "aria-hidden": true, className: cn('h-[1em] w-[2em] shrink-0', className), children: [_jsx("rect", { x: "1", y: "1.5", width: "23", height: "11", rx: "3", fill: "none", stroke: "currentColor", strokeWidth: "1.5", className: "opacity-50" }), _jsx("rect", { x: "25.5", y: "4.75", width: "2", height: "4.5", rx: "1", fill: "currentColor", className: "opacity-50" }), _jsx("rect", { x: "3.5", y: "4", width: (clamped / 100) * 18, height: "6", rx: "1.5", className: cn('fill-current transition-all duration-500 motion-reduce:transition-none', fillClass) })] }));
217
+ }
218
+ function BatteryGaugeBase({ percent, charging = false, noBattery = false, className, ...props }) {
219
+ return (_jsx("span", { "data-slot": "battery-gauge", "data-status": noBattery ? 'ac' : percent === undefined ? 'unknown' : 'battery', className: cn('inline-flex items-center gap-2 text-sm', className), ...props, children: noBattery ? (_jsxs(_Fragment, { children: [_jsx(Plug, { "aria-hidden": true, className: "text-muted-foreground size-[1.15em]" }), _jsx("span", { className: "font-mono tabular-nums", children: "AC power" })] })) : percent === undefined ? (_jsxs(_Fragment, { children: [_jsx(BatteryGlyph, { percent: 0, charging: false }), _jsx("span", { className: "text-muted-foreground/60 font-mono tabular-nums", children: "\u2014" })] })) : (_jsxs(_Fragment, { children: [_jsx(BatteryGlyph, { percent: percent, charging: charging }), _jsxs("span", { className: "font-mono tabular-nums", children: [Math.round(percent), "%"] }), charging ? (_jsx(Zap, { "aria-label": "Charging", className: "text-status-running size-3 fill-current" })) : null] })) }));
220
+ }
221
+ function ManagedBatteryGauge(props) {
222
+ const { power } = useMoldablePower();
223
+ const battery = power?.batteries[0] ?? null;
224
+ return (_jsx(BatteryGaugeBase, { percent: battery?.chargePercent, charging: battery?.state === 'charging', noBattery: power !== null && power.batteries.length === 0, ...props }));
225
+ }
226
+ /**
227
+ * Compact inline battery glyph + percent. Pass `percent`/`charging` (and
228
+ * optionally `noBattery`) to control it, or no data props to let it poll
229
+ * `useMoldablePower` itself.
230
+ */
231
+ function BatteryGauge({ percent, charging, noBattery, ...props }) {
232
+ if (percent === undefined &&
233
+ charging === undefined &&
234
+ noBattery === undefined) {
235
+ return _jsx(ManagedBatteryGauge, { ...props });
236
+ }
237
+ return (_jsx(BatteryGaugeBase, { percent: percent, charging: charging, noBattery: noBattery, ...props }));
238
+ }
239
+ const POWER_SOURCE_LABEL = {
240
+ ac: 'AC',
241
+ battery: 'Battery',
242
+ unknown: 'Unknown',
243
+ };
244
+ function capitalize(value) {
245
+ return value.charAt(0).toUpperCase() + value.slice(1);
246
+ }
247
+ function formatIdleSeconds(value) {
248
+ const seconds = Math.max(0, Math.floor(value));
249
+ if (seconds < 60)
250
+ return `${seconds}s`;
251
+ if (seconds < 3600)
252
+ return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
253
+ return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
254
+ }
255
+ function formatClock(timestamp) {
256
+ return new Date(timestamp).toLocaleTimeString(undefined, { hour12: false });
257
+ }
258
+ function describePowerSessionEvent(event) {
259
+ switch (event.kind) {
260
+ case 'power-source-change':
261
+ return event.powerState.source === 'battery'
262
+ ? 'on-battery'
263
+ : event.powerState.source === 'ac'
264
+ ? 'on-ac'
265
+ : 'power-source-change';
266
+ case 'thermal-state-change':
267
+ return `thermal-${event.state}`;
268
+ default:
269
+ return event.kind;
270
+ }
271
+ }
272
+ /**
273
+ * Power panel: battery gauge, source/thermal/idle readout, live session event
274
+ * log, and a "keep awake" sleep-blocker switch.
275
+ */
276
+ function PowerPanel({ className, pollIntervalMs, ...props }) {
277
+ const power = useMoldablePower({ pollIntervalMs });
278
+ const log = useStreamLog(MAX_POWER_EVENTS);
279
+ const appendRef = useRef(log.append);
280
+ appendRef.current = log.append;
281
+ const lastEventIdRef = useRef(0);
282
+ const { events } = power;
283
+ useEffect(() => {
284
+ for (const entry of events) {
285
+ if (entry.id <= lastEventIdRef.current)
286
+ continue;
287
+ lastEventIdRef.current = entry.id;
288
+ appendRef.current({
289
+ id: entry.id,
290
+ timestamp: entry.timestamp,
291
+ direction: 'info',
292
+ data: describePowerSessionEvent(entry.event),
293
+ });
294
+ }
295
+ }, [events]);
296
+ const battery = power.power?.batteries[0] ?? null;
297
+ const noBattery = power.power !== null && power.power.batteries.length === 0;
298
+ const status = power.power !== null || power.thermal !== null
299
+ ? 'ready'
300
+ : power.error
301
+ ? 'unavailable'
302
+ : 'loading';
303
+ return (_jsxs(HardwarePanel, { "data-slot": "power-panel", "data-status": status, className: className, ...props, children: [_jsxs(HardwarePanelHeader, { children: [_jsx(HardwarePanelIcon, { children: _jsx(BatteryCharging, {}) }), _jsx(HardwarePanelTitle, { children: "Power" }), _jsx(CapabilityBadge, { capability: "power-monitor" }), _jsx(HardwarePanelAction, { children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("label", { className: "text-muted-foreground flex items-center gap-2 text-xs font-medium", children: ["Display", _jsx(Switch, { size: "sm", checked: power.sleepBlockers['prevent-display-sleep'].active, disabled: power.sleepBlockers['prevent-display-sleep'].busy, onCheckedChange: () => void power.sleepBlockers['prevent-display-sleep'].toggle(), "aria-label": "Keep the display awake" })] }), _jsxs("label", { className: "text-muted-foreground flex items-center gap-2 text-xs font-medium", children: ["App", _jsx(Switch, { size: "sm", checked: power.sleepBlockers['prevent-app-suspension'].active, disabled: power.sleepBlockers['prevent-app-suspension'].busy, onCheckedChange: () => void power.sleepBlockers['prevent-app-suspension'].toggle(), "aria-label": "Keep the app active" })] })] }) })] }), _jsx(HardwarePanelContent, { children: _jsxs("div", { className: "animate-in fade-in space-y-4 duration-300 motion-reduce:animate-none", children: [_jsx(BatteryGauge, { className: "text-xl font-medium tracking-tight sm:text-2xl", percent: battery?.chargePercent, charging: battery?.state === 'charging', noBattery: noBattery }), _jsxs(Readout, { className: "grid-cols-2 sm:grid-cols-4", children: [_jsx(ReadoutItem, { label: "Source", value: power.power ? POWER_SOURCE_LABEL[power.power.source] : null }), _jsx(ReadoutItem, { label: "State", value: battery ? capitalize(battery.state) : null }), _jsx(ReadoutItem, { label: "Thermal", value: power.thermal ? capitalize(power.thermal) : null }), _jsx(ReadoutItem, { label: "Idle", value: power.idle
304
+ ? `${formatIdleSeconds(power.idle.seconds)} · ${power.idle.state}`
305
+ : null })] }), power.error ? (_jsx("p", { className: "text-status-error text-xs", children: power.error.message })) : null, _jsxs("div", { className: "space-y-1.5", children: [_jsx("div", { className: "text-muted-foreground text-[10px] font-medium uppercase tracking-wider", children: "Session events" }), _jsx(StreamLog, { entries: log.entries, maxHeight: 160, emptyLabel: "No session events yet" })] })] }) }), _jsxs(HardwarePanelFooter, { children: [_jsx("span", { className: "truncate font-mono tabular-nums", children: power.updatedAt
306
+ ? `Updated ${formatClock(power.updatedAt)}`
307
+ : 'Waiting for first reading' }), _jsx(Button, { variant: "ghost", size: "icon-xs", "aria-label": "Refresh power state", className: "ml-auto", onClick: power.refresh, children: _jsx(RefreshCw, {}) }), _jsx(Button, { variant: "ghost", size: "xs", className: "gap-1", disabled: BLOCKER_TYPES.some((type) => power.sleepBlockers[type].busy), onClick: () => void Promise.all(BLOCKER_TYPES.map((type) => power.sleepBlockers[type].check())), children: "Check blockers" })] })] }));
308
+ }
309
+ export { PowerPanel, BatteryGauge };
@@ -0,0 +1,10 @@
1
+ import * as React from 'react';
2
+ declare function Readout({ className, ...props }: React.ComponentProps<'div'>): import("react/jsx-runtime").JSX.Element;
3
+ export interface ReadoutItemProps extends React.ComponentProps<'div'> {
4
+ label: React.ReactNode;
5
+ value?: React.ReactNode;
6
+ unit?: React.ReactNode;
7
+ }
8
+ declare function ReadoutItem({ label, value, unit, className, children, ...props }: ReadoutItemProps): import("react/jsx-runtime").JSX.Element;
9
+ export { Readout, ReadoutItem };
10
+ //# sourceMappingURL=readout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readout.d.ts","sourceRoot":"","sources":["../../../src/components/hardware/readout.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,iBAAS,OAAO,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,2CAWpE;AAED,MAAM,WAAW,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;IACnE,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACvB;AAED,iBAAS,WAAW,CAAC,EACnB,KAAK,EACL,KAAK,EACL,IAAI,EACJ,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,gBAAgB,2CA6BlB;AAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from '../../lib/utils';
4
+ function Readout({ className, ...props }) {
5
+ return (_jsx("div", { "data-slot": "readout", className: cn('grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-3', className), ...props }));
6
+ }
7
+ function ReadoutItem({ label, value, unit, className, children, ...props }) {
8
+ const content = children ?? value;
9
+ return (_jsxs("div", { "data-slot": "readout-item", className: cn('min-w-0', className), ...props, children: [_jsx("div", { "data-slot": "readout-label", className: "text-muted-foreground text-[10px] font-medium uppercase tracking-wider", children: label }), _jsxs("div", { "data-slot": "readout-value", className: "mt-1 truncate font-mono text-sm tabular-nums", children: [content === null || content === undefined ? (_jsx("span", { className: "text-muted-foreground/60", children: "\u2014" })) : (content), unit !== undefined && content !== null && content !== undefined ? (_jsx("span", { className: "text-muted-foreground ml-1 text-xs", children: unit })) : null] })] }));
10
+ }
11
+ export { Readout, ReadoutItem };