@hashsome/ui 0.1.0

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 (192) hide show
  1. package/LICENSE +674 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/entities/action-button.d.ts +17 -0
  4. package/dist/entities/action-button.js +56 -0
  5. package/dist/entities/action-button.js.map +1 -0
  6. package/dist/entities/artwork-ring.d.ts +17 -0
  7. package/dist/entities/artwork-ring.js +89 -0
  8. package/dist/entities/artwork-ring.js.map +1 -0
  9. package/dist/entities/climate-tile.d.ts +20 -0
  10. package/dist/entities/climate-tile.js +98 -0
  11. package/dist/entities/climate-tile.js.map +1 -0
  12. package/dist/entities/light-color.d.ts +52 -0
  13. package/dist/entities/light-color.js +148 -0
  14. package/dist/entities/light-color.js.map +1 -0
  15. package/dist/entities/light-tile.d.ts +24 -0
  16. package/dist/entities/light-tile.js +139 -0
  17. package/dist/entities/light-tile.js.map +1 -0
  18. package/dist/entities/media-browser.d.ts +15 -0
  19. package/dist/entities/media-browser.js +67 -0
  20. package/dist/entities/media-browser.js.map +1 -0
  21. package/dist/entities/media-controls.d.ts +20 -0
  22. package/dist/entities/media-controls.js +53 -0
  23. package/dist/entities/media-controls.js.map +1 -0
  24. package/dist/entities/media-player-bar.d.ts +14 -0
  25. package/dist/entities/media-player-bar.js +78 -0
  26. package/dist/entities/media-player-bar.js.map +1 -0
  27. package/dist/entities/media-player-body.d.ts +16 -0
  28. package/dist/entities/media-player-body.js +18 -0
  29. package/dist/entities/media-player-body.js.map +1 -0
  30. package/dist/entities/media-player-column.d.ts +18 -0
  31. package/dist/entities/media-player-column.js +34 -0
  32. package/dist/entities/media-player-column.js.map +1 -0
  33. package/dist/entities/media-player-page.d.ts +12 -0
  34. package/dist/entities/media-player-page.js +13 -0
  35. package/dist/entities/media-player-page.js.map +1 -0
  36. package/dist/entities/media-progress.d.ts +7 -0
  37. package/dist/entities/media-progress.js +30 -0
  38. package/dist/entities/media-progress.js.map +1 -0
  39. package/dist/entities/media-seek.d.ts +9 -0
  40. package/dist/entities/media-seek.js +19 -0
  41. package/dist/entities/media-seek.js.map +1 -0
  42. package/dist/entities/nav-dock.d.ts +13 -0
  43. package/dist/entities/nav-dock.js +22 -0
  44. package/dist/entities/nav-dock.js.map +1 -0
  45. package/dist/entities/nav-rail.d.ts +19 -0
  46. package/dist/entities/nav-rail.js +18 -0
  47. package/dist/entities/nav-rail.js.map +1 -0
  48. package/dist/entities/now-playing.d.ts +20 -0
  49. package/dist/entities/now-playing.js +34 -0
  50. package/dist/entities/now-playing.js.map +1 -0
  51. package/dist/entities/scene-button.d.ts +13 -0
  52. package/dist/entities/scene-button.js +6 -0
  53. package/dist/entities/scene-button.js.map +1 -0
  54. package/dist/entities/seek-line.d.ts +9 -0
  55. package/dist/entities/seek-line.js +71 -0
  56. package/dist/entities/seek-line.js.map +1 -0
  57. package/dist/entities/sensor-readout.d.ts +28 -0
  58. package/dist/entities/sensor-readout.js +95 -0
  59. package/dist/entities/sensor-readout.js.map +1 -0
  60. package/dist/entities/top-bar.d.ts +68 -0
  61. package/dist/entities/top-bar.js +248 -0
  62. package/dist/entities/top-bar.js.map +1 -0
  63. package/dist/entity-handle.d.ts +21 -0
  64. package/dist/entity-handle.js +16 -0
  65. package/dist/entity-handle.js.map +1 -0
  66. package/dist/gallery/fixtures.d.ts +4 -0
  67. package/dist/gallery/fixtures.js +119 -0
  68. package/dist/gallery/fixtures.js.map +1 -0
  69. package/dist/gallery/gallery.d.ts +9 -0
  70. package/dist/gallery/gallery.js +355 -0
  71. package/dist/gallery/gallery.js.map +1 -0
  72. package/dist/gallery/index.d.ts +2 -0
  73. package/dist/gallery/index.js +2 -0
  74. package/dist/gallery/index.js.map +1 -0
  75. package/dist/gallery/props-data.d.ts +10 -0
  76. package/dist/gallery/props-data.js +718 -0
  77. package/dist/gallery/props-data.js.map +1 -0
  78. package/dist/hooks.d.ts +19 -0
  79. package/dist/hooks.js +53 -0
  80. package/dist/hooks.js.map +1 -0
  81. package/dist/icon-data.d.ts +6 -0
  82. package/dist/icon-data.js +59193 -0
  83. package/dist/icon-data.js.map +1 -0
  84. package/dist/icon.d.ts +9 -0
  85. package/dist/icon.js +28 -0
  86. package/dist/icon.js.map +1 -0
  87. package/dist/index.d.ts +30 -0
  88. package/dist/index.js +24 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/layout/cover.d.ts +5 -0
  91. package/dist/layout/cover.js +8 -0
  92. package/dist/layout/cover.js.map +1 -0
  93. package/dist/layout/date-time-field.d.ts +15 -0
  94. package/dist/layout/date-time-field.js +108 -0
  95. package/dist/layout/date-time-field.js.map +1 -0
  96. package/dist/layout/detail-provider.d.ts +22 -0
  97. package/dist/layout/detail-provider.js +26 -0
  98. package/dist/layout/detail-provider.js.map +1 -0
  99. package/dist/layout/drawer-controls.d.ts +46 -0
  100. package/dist/layout/drawer-controls.js +93 -0
  101. package/dist/layout/drawer-controls.js.map +1 -0
  102. package/dist/layout/energy-chart.d.ts +37 -0
  103. package/dist/layout/energy-chart.js +85 -0
  104. package/dist/layout/energy-chart.js.map +1 -0
  105. package/dist/layout/energy-usage.d.ts +6 -0
  106. package/dist/layout/energy-usage.js +18 -0
  107. package/dist/layout/energy-usage.js.map +1 -0
  108. package/dist/layout/entity-drawer.d.ts +15 -0
  109. package/dist/layout/entity-drawer.js +77 -0
  110. package/dist/layout/entity-drawer.js.map +1 -0
  111. package/dist/layout/fade-scroll.d.ts +14 -0
  112. package/dist/layout/fade-scroll.js +59 -0
  113. package/dist/layout/fade-scroll.js.map +1 -0
  114. package/dist/layout/grid.d.ts +9 -0
  115. package/dist/layout/grid.js +6 -0
  116. package/dist/layout/grid.js.map +1 -0
  117. package/dist/layout/history-section.d.ts +19 -0
  118. package/dist/layout/history-section.js +39 -0
  119. package/dist/layout/history-section.js.map +1 -0
  120. package/dist/layout/hold-progress.d.ts +2 -0
  121. package/dist/layout/hold-progress.js +9 -0
  122. package/dist/layout/hold-progress.js.map +1 -0
  123. package/dist/layout/icon-button-styles.d.ts +9 -0
  124. package/dist/layout/icon-button-styles.js +28 -0
  125. package/dist/layout/icon-button-styles.js.map +1 -0
  126. package/dist/layout/page.d.ts +17 -0
  127. package/dist/layout/page.js +29 -0
  128. package/dist/layout/page.js.map +1 -0
  129. package/dist/layout/plain-button.d.ts +11 -0
  130. package/dist/layout/plain-button.js +17 -0
  131. package/dist/layout/plain-button.js.map +1 -0
  132. package/dist/layout/room-header.d.ts +12 -0
  133. package/dist/layout/room-header.js +11 -0
  134. package/dist/layout/room-header.js.map +1 -0
  135. package/dist/layout/round-button.d.ts +13 -0
  136. package/dist/layout/round-button.js +19 -0
  137. package/dist/layout/round-button.js.map +1 -0
  138. package/dist/layout/sensor-history.d.ts +15 -0
  139. package/dist/layout/sensor-history.js +91 -0
  140. package/dist/layout/sensor-history.js.map +1 -0
  141. package/dist/layout/sensor-stats.d.ts +21 -0
  142. package/dist/layout/sensor-stats.js +79 -0
  143. package/dist/layout/sensor-stats.js.map +1 -0
  144. package/dist/layout/series-chart.d.ts +25 -0
  145. package/dist/layout/series-chart.js +46 -0
  146. package/dist/layout/series-chart.js.map +1 -0
  147. package/dist/layout/stepper.d.ts +10 -0
  148. package/dist/layout/stepper.js +12 -0
  149. package/dist/layout/stepper.js.map +1 -0
  150. package/dist/layout/swatch-row.d.ts +19 -0
  151. package/dist/layout/swatch-row.js +20 -0
  152. package/dist/layout/swatch-row.js.map +1 -0
  153. package/dist/layout/tile.d.ts +67 -0
  154. package/dist/layout/tile.js +207 -0
  155. package/dist/layout/tile.js.map +1 -0
  156. package/dist/layout/use-drawer.d.ts +33 -0
  157. package/dist/layout/use-drawer.js +34 -0
  158. package/dist/layout/use-drawer.js.map +1 -0
  159. package/dist/layout/use-hold.d.ts +21 -0
  160. package/dist/layout/use-hold.js +61 -0
  161. package/dist/layout/use-hold.js.map +1 -0
  162. package/dist/provider.d.ts +37 -0
  163. package/dist/provider.js +95 -0
  164. package/dist/provider.js.map +1 -0
  165. package/dist/status.d.ts +8 -0
  166. package/dist/status.js +27 -0
  167. package/dist/status.js.map +1 -0
  168. package/dist/theme/density.d.ts +16 -0
  169. package/dist/theme/density.js +7 -0
  170. package/dist/theme/density.js.map +1 -0
  171. package/dist/theme/global-styles.d.ts +5 -0
  172. package/dist/theme/global-styles.js +44 -0
  173. package/dist/theme/global-styles.js.map +1 -0
  174. package/dist/theme/index.d.ts +19 -0
  175. package/dist/theme/index.js +106 -0
  176. package/dist/theme/index.js.map +1 -0
  177. package/dist/theme/overrides.d.ts +28 -0
  178. package/dist/theme/overrides.js +24 -0
  179. package/dist/theme/overrides.js.map +1 -0
  180. package/dist/theme/tokens.d.ts +78 -0
  181. package/dist/theme/tokens.js +1 -0
  182. package/dist/theme/tokens.js.map +1 -0
  183. package/dist/theme/use-typography.d.ts +4 -0
  184. package/dist/theme/use-typography.js +9 -0
  185. package/dist/theme/use-typography.js.map +1 -0
  186. package/dist/use-entity-history.d.ts +12 -0
  187. package/dist/use-entity-history.js +43 -0
  188. package/dist/use-entity-history.js.map +1 -0
  189. package/dist/use-media-browser.d.ts +31 -0
  190. package/dist/use-media-browser.js +87 -0
  191. package/dist/use-media-browser.js.map +1 -0
  192. package/package.json +57 -0
package/dist/icon.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { type IconName } from './icon-data.ts';
2
+ export interface IconProps {
3
+ /** Prefixed icon id, e.g. `lu:lightbulb`, `tb:vacuum-cleaner`. */
4
+ name: IconName;
5
+ /** Overrides the density's icon size, in px. */
6
+ size?: number;
7
+ }
8
+ /** An inline SVG icon, from a plain prefixed id (`lu:` Lucide, `tb:` Tabler outline). Takes the surrounding text color. */
9
+ export declare function Icon({ name, size }: IconProps): import("@emotion/react/jsx-runtime").JSX.Element;
package/dist/icon.js ADDED
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "@emotion/react/jsx-runtime";
2
+ /** @jsxImportSource @emotion/react */
3
+ import { createElement } from 'react';
4
+ import { ICON_NODES } from './icon-data.js';
5
+ const STROKE_WIDTH = 1.5;
6
+ /** Packs whose icons are drawn as outlines (`fill: none`, shapes stroked in). A pack not listed
7
+ * here (e.g. a future filled pack) renders solid instead. */
8
+ const STROKE_PACKS = new Set(['lu', 'tb']);
9
+ /** An inline SVG icon, from a plain prefixed id (`lu:` Lucide, `tb:` Tabler outline). Takes the surrounding text color. */
10
+ export function Icon({ name, size }) {
11
+ const stroke = STROKE_PACKS.has(name.slice(0, name.indexOf(':')));
12
+ const nodes = ICON_NODES[name];
13
+ if (!nodes) {
14
+ console.warn(`Could not find icon "${name}"`);
15
+ }
16
+ return (_jsx("svg", { viewBox: "0 0 24 24", css: ({ density }) => ({
17
+ width: size ?? density.iconSize,
18
+ height: size ?? density.iconSize,
19
+ flex: 'none',
20
+ }), fill: stroke ? 'none' : 'currentColor', ...(stroke
21
+ ? {
22
+ stroke: 'currentColor',
23
+ strokeWidth: STROKE_WIDTH,
24
+ strokeLinecap: 'round',
25
+ strokeLinejoin: 'round',
26
+ }
27
+ : {}), "aria-hidden": "true", children: (nodes ?? []).map(([tag, attrs], i) => createElement(tag, { key: i, ...attrs })) }));
28
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon.js","sourceRoot":"","sources":["../src/icon.tsx"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,UAAU,EAAiB,MAAM,gBAAgB,CAAC;AAU3D,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB;6DAC6D;AAC7D,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAE3C,2HAA2H;AAC3H,MAAM,UAAU,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAa;IAC5C,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAClE,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,wBAAwB,IAAI,GAAG,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,CACL,cACE,OAAO,EAAC,WAAW,EACnB,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;YACrB,KAAK,EAAE,IAAI,IAAI,OAAO,CAAC,QAAQ;YAC/B,MAAM,EAAE,IAAI,IAAI,OAAO,CAAC,QAAQ;YAChC,IAAI,EAAE,MAAM;SACb,CAAC,EACF,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,KAClC,CAAC,MAAM;YACT,CAAC,CAAC;gBACE,MAAM,EAAE,cAAc;gBACtB,WAAW,EAAE,YAAY;gBACzB,aAAa,EAAE,OAAO;gBACtB,cAAc,EAAE,OAAO;aACxB;YACH,CAAC,CAAC,EAAE,CAAC,iBACK,MAAM,YAEjB,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,GAC7E,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ export * from './provider.tsx';
2
+ export * from './theme/index.ts';
3
+ export type { Density, DensityTokens } from './theme/density.ts';
4
+ export type { ThemeOverrides } from './theme/overrides.ts';
5
+ export * from './hooks.ts';
6
+ export type { EntityHandle } from './entity-handle.ts';
7
+ export * from './status.ts';
8
+ export type { IconName } from './icon-data.ts';
9
+ export * from './icon.tsx';
10
+ export * from './layout/grid.tsx';
11
+ export * from './layout/page.tsx';
12
+ export * from './layout/room-header.tsx';
13
+ export { Tile, type TileProps } from './layout/tile.tsx';
14
+ export * from './layout/energy-chart.tsx';
15
+ export * from './layout/history-section.tsx';
16
+ export type { SensorSample } from './layout/sensor-history.tsx';
17
+ export * from './entities/light-tile.tsx';
18
+ export type { LightColorPreset } from './entities/light-color.ts';
19
+ export * from './entities/climate-tile.tsx';
20
+ export * from './entities/sensor-readout.tsx';
21
+ export * from './entities/action-button.tsx';
22
+ export * from './entities/scene-button.tsx';
23
+ export * from './entities/media-player-bar.tsx';
24
+ export * from './entities/media-player-column.tsx';
25
+ export * from './entities/media-player-page.tsx';
26
+ export * from './entities/media-browser.tsx';
27
+ export { formatDuration } from './entities/media-progress.ts';
28
+ export * from './entities/nav-rail.tsx';
29
+ export * from './entities/nav-dock.tsx';
30
+ export * from './entities/top-bar.tsx';
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ export * from './provider.js';
2
+ export * from './theme/index.js';
3
+ export * from './hooks.js';
4
+ export * from './status.js';
5
+ export * from './icon.js';
6
+ export * from './layout/grid.js';
7
+ export * from './layout/page.js';
8
+ export * from './layout/room-header.js';
9
+ export { Tile } from './layout/tile.js';
10
+ export * from './layout/energy-chart.js';
11
+ export * from './layout/history-section.js';
12
+ export * from './entities/light-tile.js';
13
+ export * from './entities/climate-tile.js';
14
+ export * from './entities/sensor-readout.js';
15
+ export * from './entities/action-button.js';
16
+ export * from './entities/scene-button.js';
17
+ export * from './entities/media-player-bar.js';
18
+ export * from './entities/media-player-column.js';
19
+ export * from './entities/media-player-page.js';
20
+ export * from './entities/media-browser.js';
21
+ export { formatDuration } from './entities/media-progress.js';
22
+ export * from './entities/nav-rail.js';
23
+ export * from './entities/nav-dock.js';
24
+ export * from './entities/top-bar.js';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AAGjC,cAAc,YAAY,CAAC;AAE3B,cAAc,aAAa,CAAC;AAE5B,cAAc,WAAY,CAAC;AAC3B,cAAc,kBAAmB,CAAC;AAClC,cAAc,kBAAmB,CAAC;AAClC,cAAc,yBAA0B,CAAC;AACzC,OAAO,EAAE,IAAI,EAAkB,MAAM,kBAAmB,CAAC;AACzD,cAAc,0BAA2B,CAAC;AAC1C,cAAc,6BAA8B,CAAC;AAE7C,cAAc,0BAA2B,CAAC;AAE1C,cAAc,4BAA6B,CAAC;AAC5C,cAAc,8BAA+B,CAAC;AAC9C,cAAc,6BAA8B,CAAC;AAC7C,cAAc,4BAA6B,CAAC;AAC5C,cAAc,gCAAiC,CAAC;AAChD,cAAc,mCAAoC,CAAC;AACnD,cAAc,iCAAkC,CAAC;AACjD,cAAc,6BAA8B,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,cAAc,wBAAyB,CAAC;AACxC,cAAc,wBAAyB,CAAC;AACxC,cAAc,uBAAwB,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** A picture (artwork, an avatar) that fills the box it is in, cropped to fit. Decorative: the
2
+ * name of what it shows is always written beside it. */
3
+ export declare function Cover({ src }: {
4
+ src: string;
5
+ }): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "@emotion/react/jsx-runtime";
2
+ /** @jsxImportSource @emotion/react */
3
+ import { Box } from 'e-prim';
4
+ /** A picture (artwork, an avatar) that fills the box it is in, cropped to fit. Decorative: the
5
+ * name of what it shows is always written beside it. */
6
+ export function Cover({ src }) {
7
+ return _jsx(Box, { as: "img", src: src, alt: "", width: "100%", height: "100%", css: { objectFit: 'cover' } });
8
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cover.js","sourceRoot":"","sources":["../../src/layout/cover.tsx"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,OAAO,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAE7B;wDACwD;AACxD,MAAM,UAAU,KAAK,CAAC,EAAE,GAAG,EAAmB;IAC5C,OAAO,KAAC,GAAG,IAAC,EAAE,EAAC,KAAK,EAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAC,EAAE,EAAC,KAAK,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,GAAI,CAAC;AACnG,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A date + time picker that opens as a popover under its field — not a native `<input type="datetime-local">`,
3
+ * whose popup can't be themed and is slow to appear. Day buttons keep the chosen time of day; the
4
+ * time is two 24-hour number fields. `min` / `max` (`YYYY-MM-DDTHH:mm`) disable days outside the bounds and clamp the result.
5
+ */
6
+ export declare function DateTimeField({ label, value, min, max, align, onChange, }: {
7
+ label: string;
8
+ /** `YYYY-MM-DDTHH:mm`, local time. */
9
+ value: string;
10
+ min?: string;
11
+ max?: string;
12
+ /** Which edge of the field the popover lines up with. Default `'left'`. */
13
+ align?: 'left' | 'right';
14
+ onChange: (value: string) => void;
15
+ }): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -0,0 +1,108 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
+ /** @jsxImportSource @emotion/react */
3
+ import { Box, Flex, Grid, Typography } from 'e-prim';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { Icon } from '../icon.js';
6
+ import { RoundButton } from './round-button.js';
7
+ import { fromDateTimeInput, toDateTimeInput } from './sensor-stats.js';
8
+ const WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
9
+ const dayKey = (d) => d.getFullYear() * 10_000 + d.getMonth() * 100 + d.getDate();
10
+ const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
11
+ /** The 6×7 grid of days (Monday first) that shows `month`, including the neighbouring months' spill-over. */
12
+ function gridDays(month) {
13
+ const first = new Date(month.getFullYear(), month.getMonth(), 1);
14
+ const offset = (first.getDay() + 6) % 7;
15
+ return Array.from({ length: 42 }, (_, i) => new Date(first.getFullYear(), first.getMonth(), 1 - offset + i));
16
+ }
17
+ /**
18
+ * A date + time picker that opens as a popover under its field — not a native `<input type="datetime-local">`,
19
+ * whose popup can't be themed and is slow to appear. Day buttons keep the chosen time of day; the
20
+ * time is two 24-hour number fields. `min` / `max` (`YYYY-MM-DDTHH:mm`) disable days outside the bounds and clamp the result.
21
+ */
22
+ export function DateTimeField({ label, value, min, max, align = 'left', onChange, }) {
23
+ const current = fromDateTimeInput(value) ?? new Date();
24
+ const lower = min ? fromDateTimeInput(min) : undefined;
25
+ const upper = max ? fromDateTimeInput(max) : undefined;
26
+ const [open, setOpen] = useState(false);
27
+ const root = useRef(null);
28
+ useEffect(() => {
29
+ if (!open) {
30
+ return;
31
+ }
32
+ const onPointerDown = (event) => {
33
+ if (root.current && !root.current.contains(event.target)) {
34
+ setOpen(false);
35
+ }
36
+ };
37
+ const onKeyDown = (event) => {
38
+ if (event.key === 'Escape') {
39
+ event.stopPropagation();
40
+ setOpen(false);
41
+ }
42
+ };
43
+ document.addEventListener('pointerdown', onPointerDown);
44
+ window.addEventListener('keydown', onKeyDown, true);
45
+ return () => {
46
+ document.removeEventListener('pointerdown', onPointerDown);
47
+ window.removeEventListener('keydown', onKeyDown, true);
48
+ };
49
+ }, [open]);
50
+ const [month, setMonth] = useState(() => new Date(current.getFullYear(), current.getMonth(), 1));
51
+ const commit = (next) => {
52
+ const clamped = lower && next < lower ? lower : upper && next > upper ? upper : next;
53
+ onChange(toDateTimeInput(clamped));
54
+ };
55
+ const pickDay = (day) => commit(new Date(day.getFullYear(), day.getMonth(), day.getDate(), current.getHours(), current.getMinutes()));
56
+ const setTime = (hours, minutes) => commit(new Date(current.getFullYear(), current.getMonth(), current.getDate(), hours, minutes));
57
+ const shiftMonth = (delta) => setMonth((m) => new Date(m.getFullYear(), m.getMonth() + delta, 1));
58
+ const dayDisabled = (day) => (lower !== undefined && day < startOfDay(lower)) ||
59
+ (upper !== undefined &&
60
+ day > new Date(upper.getFullYear(), upper.getMonth(), upper.getDate(), 23, 59, 59));
61
+ const today = new Date();
62
+ const selectedKey = dayKey(current);
63
+ return (_jsxs(Flex, { ref: root, align: "center", gap: 2, position: "relative", children: [_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: label }), _jsx(Flex, { position: "relative", children: _jsxs(Flex, { as: "button", type: "button", "aria-label": label, "aria-expanded": open, onClick: () => {
64
+ setMonth(new Date(current.getFullYear(), current.getMonth(), 1));
65
+ setOpen((o) => !o);
66
+ }, align: "center", gap: 2, radius: "small", cursor: "pointer", color: "text", background: "surface", border: true, py: 1.5, px: 3, children: [_jsxs(Typography, { as: "span", variant: "label", children: [current.toLocaleDateString(undefined, {
67
+ day: 'numeric',
68
+ month: 'short',
69
+ year: 'numeric',
70
+ }), ' · ', current.toLocaleTimeString(undefined, {
71
+ hour: '2-digit',
72
+ minute: '2-digit',
73
+ hour12: false,
74
+ })] }), _jsx(Icon, { name: "lu:calendar", size: 14 })] }) }), open ? (_jsxs(Flex, { direction: "column", gap: 2, background: "surfaceRaised", border: true, radius: "row", shadow: "drawer", p: 3, position: "absolute", width: 288, zIndex: "popover", role: "group", "aria-label": `${label} picker`, css: ({ spacing }) => ({ top: `calc(100% + ${spacing(2)})`, [align]: 0 }), children: [_jsxs(Flex, { align: "center", justify: "space-between", children: [_jsx(NavButton, { icon: "lu:chevron-left", label: "Previous month", onClick: () => shiftMonth(-1) }), _jsx(Typography, { as: "span", variant: "bodyStrong", children: month.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }) }), _jsx(NavButton, { icon: "lu:chevron-right", label: "Next month", onClick: () => shiftMonth(1) })] }), _jsxs(Grid, { columns: [7], gap: 0.5, css: { textAlign: 'center' }, children: [WEEKDAYS.map((d) => (_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", py: 1, children: d }, d))), gridDays(month).map((day) => {
75
+ const selected = dayKey(day) === selectedKey;
76
+ return (_jsx(Box, { as: "button", type: "button", width: 32, height: 32, radius: "full", p: 0, background: "transparent", cursor: "pointer", css: ({ palette }) => ({
77
+ justifySelf: 'center',
78
+ '&:hover:not(:disabled)': { background: palette.surfaceRaised },
79
+ "&[data-outside='true']": { color: palette.textMuted, opacity: 0.55 },
80
+ "&[data-today='true']": { boxShadow: `inset 0 0 0 1px ${palette.line}` },
81
+ "&[aria-pressed='true']": {
82
+ background: palette.accent,
83
+ color: palette.accentText,
84
+ opacity: 1,
85
+ },
86
+ '&:disabled': { opacity: 0.25, cursor: 'default' },
87
+ }), "aria-label": day.toLocaleDateString(undefined, {
88
+ month: 'long',
89
+ day: 'numeric',
90
+ year: 'numeric',
91
+ }), "aria-pressed": selected, "data-outside": day.getMonth() !== month.getMonth(), "data-today": dayKey(day) === dayKey(today), disabled: dayDisabled(day), onClick: () => pickDay(day), children: _jsx(Typography, { as: "span", variant: "body", children: day.getDate() }) }, day.toISOString()));
92
+ })] }), _jsxs(Flex, { align: "center", justify: "space-between", children: [_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: "Time" }), _jsxs(Flex, { align: "center", gap: 1.5, children: [_jsx(TimeInput, { label: `${label} hour`, value: current.getHours(), max: 23, onChange: (h) => setTime(h, current.getMinutes()) }), _jsx("span", { children: ":" }), _jsx(TimeInput, { label: `${label} minute`, value: current.getMinutes(), max: 59, onChange: (m) => setTime(current.getHours(), m) })] })] })] })) : null] }));
93
+ }
94
+ function NavButton({ icon, label, onClick, }) {
95
+ return (_jsx(RoundButton, { size: 28, "aria-label": label, title: label, onClick: onClick, children: _jsx(Icon, { name: icon, size: 14 }) }));
96
+ }
97
+ function TimeInput({ label, value, max, onChange, }) {
98
+ return (_jsx(Box, { as: "input", typography: "body", width: 44, py: 1, px: 1.5, border: true, radius: "small", background: "surfaceRaised", color: "text", css: ({ palette }) => ({
99
+ textAlign: 'center',
100
+ fontVariantNumeric: 'tabular-nums',
101
+ '&:focus-visible': { outline: `2px solid ${palette.accent}`, outlineOffset: 1 },
102
+ }), type: "number", inputMode: "numeric", "aria-label": label, min: 0, max: max, value: String(value).padStart(2, '0'), onChange: (event) => {
103
+ const next = Number(event.target.value);
104
+ if (Number.isFinite(next)) {
105
+ onChange(Math.min(max, Math.max(0, Math.round(next))));
106
+ }
107
+ } }));
108
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date-time-field.js","sourceRoot":"","sources":["../../src/layout/date-time-field.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { type ReactNode } from 'react';
2
+ interface DetailState {
3
+ id: string;
4
+ header: ReactNode;
5
+ body: ReactNode;
6
+ expanded: boolean;
7
+ }
8
+ interface DetailContextValue {
9
+ detail: DetailState | null;
10
+ /** `expanded` opens it at full size instead of the side panel. */
11
+ openDetail: (id: string, header: ReactNode, body: ReactNode, expanded?: boolean) => void;
12
+ /** Keeps an already-open drawer's content live; a no-op for any other `id`. */
13
+ updateDetail: (id: string, header: ReactNode, body: ReactNode) => void;
14
+ closeDetail: () => void;
15
+ toggleExpanded: () => void;
16
+ }
17
+ /** Backs the single shared `EntityDrawer`; instantiated once by `HashsomeProvider`. */
18
+ export declare function DetailProvider({ children }: {
19
+ children: ReactNode;
20
+ }): import("react").JSX.Element;
21
+ export declare function useDetail(): DetailContextValue;
22
+ export {};
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useMemo, useState } from 'react';
3
+ const DetailContext = createContext(null);
4
+ /** Backs the single shared `EntityDrawer`; instantiated once by `HashsomeProvider`. */
5
+ export function DetailProvider({ children }) {
6
+ const [detail, setDetail] = useState(null);
7
+ const openDetail = useCallback((id, header, body, expanded = false) => {
8
+ setDetail({ id, header, body, expanded });
9
+ }, []);
10
+ const updateDetail = useCallback((id, header, body) => {
11
+ setDetail((current) => (current && current.id === id ? { ...current, header, body } : current));
12
+ }, []);
13
+ const closeDetail = useCallback(() => setDetail(null), []);
14
+ const toggleExpanded = useCallback(() => {
15
+ setDetail((current) => (current ? { ...current, expanded: !current.expanded } : current));
16
+ }, []);
17
+ const value = useMemo(() => ({ detail, openDetail, updateDetail, closeDetail, toggleExpanded }), [detail, openDetail, updateDetail, closeDetail, toggleExpanded]);
18
+ return _jsx(DetailContext.Provider, { value: value, children: children });
19
+ }
20
+ export function useDetail() {
21
+ const context = useContext(DetailContext);
22
+ if (!context) {
23
+ throw new Error('useDetail must be used inside <DetailProvider>');
24
+ }
25
+ return context;
26
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detail-provider.js","sourceRoot":"","sources":["../../src/layout/detail-provider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAqBlG,MAAM,aAAa,GAAG,aAAa,CAA4B,IAAI,CAAC,CAAC;AAErE,uFAAuF;AACvF,MAAM,UAAU,cAAc,CAAC,EAAE,QAAQ,EAA2B;IAClE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAqB,IAAI,CAAC,CAAC;IAE/D,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,EAAU,EAAE,MAAiB,EAAE,IAAe,EAAE,QAAQ,GAAG,KAAK,EAAE,EAAE;QACnE,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC5C,CAAC,EACD,EAAE,CACH,CAAC;IAEF,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,EAAU,EAAE,MAAiB,EAAE,IAAe,EAAE,EAAE;QAClF,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAClG,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAC3D,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE;QACtC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5F,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,EACzE,CAAC,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,CAAC,CAChE,CAAC;IAEF,OAAO,KAAC,aAAa,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA0B,CAAC;AACnF,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,MAAM,OAAO,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,46 @@
1
+ import type { IconName } from '../icon-data.ts';
2
+ /** Fine adjustment: a press nudges by `delta` (1%); holding repeats in steps of 10 until released. */
3
+ export declare function StepButton({ icon, label, delta, holdDelta, onStep, disabled, size, }: {
4
+ icon: IconName;
5
+ label: string;
6
+ delta: number;
7
+ /** Amount per repeat while held. Default `delta * 10`. */
8
+ holdDelta?: number;
9
+ onStep: (delta: number) => void;
10
+ disabled?: boolean;
11
+ /** Button diameter in px. Default 36. */
12
+ size?: number;
13
+ }): import("@emotion/react/jsx-runtime").JSX.Element;
14
+ /** A thumbless bar (like the tile's own fill): drag anywhere on it, or use the arrow keys. Plain
15
+ * `fill` shows accent up to the value; a `gradient` track stays vivid up to the value and dims past it instead (no marker or thumb). */
16
+ export declare function ValueBar({ label, value, min, max, keyStep, round, gradient, height, onDrag, onCommit, }: {
17
+ label: string;
18
+ value: number;
19
+ min: number;
20
+ max: number;
21
+ keyStep: number;
22
+ round?: number;
23
+ gradient?: string;
24
+ /** Bar thickness in px. Default 28. */
25
+ height?: number;
26
+ onDrag: (value: number) => void;
27
+ onCommit: (value: number) => void;
28
+ }): import("@emotion/react/jsx-runtime").JSX.Element;
29
+ /** One round power icon button with two states: filled with the accent while on, neutral while off. */
30
+ export declare function PowerButton({ on, onToggle }: {
31
+ on: boolean;
32
+ onToggle: () => void;
33
+ }): import("@emotion/react/jsx-runtime").JSX.Element;
34
+ /** A wrapping row of pill choices (modes, presets) with the selected one filled. With `tabs` it is
35
+ * a tab list (the selected chip is the open tab) that never wraps: too many to fit scroll sideways. */
36
+ export declare function ChipRow({ options, value, onChange, tabs, }: {
37
+ options: {
38
+ value: string;
39
+ label: string;
40
+ icon?: IconName;
41
+ ariaLabel?: string;
42
+ }[];
43
+ value: string | undefined;
44
+ onChange: (value: string) => void;
45
+ tabs?: boolean;
46
+ }): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
+ /** @jsxImportSource @emotion/react */
3
+ import { Box, Flex, Typography } from 'e-prim';
4
+ import { motion } from 'motion/react';
5
+ import { useEffect, useRef } from 'react';
6
+ import { Icon } from '../icon.js';
7
+ import { FadeScroll } from './fade-scroll.js';
8
+ import { RoundButton } from './round-button.js';
9
+ /** Fine adjustment: a press nudges by `delta` (1%); holding repeats in steps of 10 until released. */
10
+ export function StepButton({ icon, label, delta, holdDelta = delta * 10, onStep, disabled, size = 36, }) {
11
+ const timers = useRef({});
12
+ const held = useRef(false);
13
+ const stop = () => {
14
+ clearTimeout(timers.current.delay);
15
+ clearInterval(timers.current.repeat);
16
+ };
17
+ // `stop` only touches refs, so it is safe to run once on unmount.
18
+ // oxlint-disable-next-line react/exhaustive-effect-dependencies
19
+ useEffect(() => stop, []);
20
+ return (_jsx(RoundButton, { size: size, css: { flex: 'none' }, "aria-label": label, title: label, disabled: disabled, onPointerDown: () => {
21
+ held.current = false;
22
+ timers.current.delay = setTimeout(() => {
23
+ held.current = true;
24
+ onStep(holdDelta);
25
+ timers.current.repeat = setInterval(() => onStep(holdDelta), 400);
26
+ }, 400);
27
+ }, onPointerUp: stop, onPointerLeave: stop, onPointerCancel: stop, onClick: () => {
28
+ if (!held.current) {
29
+ onStep(delta);
30
+ }
31
+ held.current = false;
32
+ }, children: _jsx(Icon, { name: icon, size: 16 }) }));
33
+ }
34
+ /** A thumbless bar (like the tile's own fill): drag anywhere on it, or use the arrow keys. Plain
35
+ * `fill` shows accent up to the value; a `gradient` track stays vivid up to the value and dims past it instead (no marker or thumb). */
36
+ export function ValueBar({ label, value, min, max, keyStep, round = 1, gradient, height = 28, onDrag, onCommit, }) {
37
+ const dragging = useRef(false);
38
+ const fraction = max === min ? 0 : (value - min) / (max - min);
39
+ const pct = Math.min(1, Math.max(0, fraction)) * 100;
40
+ const clampValue = (v) => Math.min(max, Math.max(min, v));
41
+ const valueAt = (event) => {
42
+ const rect = event.currentTarget.getBoundingClientRect();
43
+ if (rect.width === 0) {
44
+ return value;
45
+ }
46
+ const f = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
47
+ return clampValue(Math.round((min + f * (max - min)) / round) * round);
48
+ };
49
+ const onKeyDown = (event) => {
50
+ const dir = event.key === 'ArrowRight' || event.key === 'ArrowUp'
51
+ ? 1
52
+ : event.key === 'ArrowLeft' || event.key === 'ArrowDown'
53
+ ? -1
54
+ : 0;
55
+ if (dir !== 0) {
56
+ event.preventDefault();
57
+ onCommit(clampValue(value + dir * keyStep));
58
+ }
59
+ };
60
+ return (_jsx(Box, { role: "slider", tabIndex: 0, "aria-label": label, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": value, position: "relative",
61
+ // Explicit width: the bar has no in-flow content (its fill is absolutely positioned), so as a
62
+ // flex item in a row it would size to 0 and vanish.
63
+ width: "100%", height: height, radius: "full", background: "surface", border: !gradient, cursor: "pointer", overflow: "hidden", css: { touchAction: 'none', ...(gradient ? { backgroundImage: gradient } : {}) }, onPointerDown: (event) => {
64
+ dragging.current = true;
65
+ event.currentTarget.setPointerCapture?.(event.pointerId);
66
+ onDrag(valueAt(event));
67
+ }, onPointerMove: (event) => {
68
+ if (dragging.current) {
69
+ onDrag(valueAt(event));
70
+ }
71
+ }, onPointerUp: (event) => {
72
+ if (dragging.current) {
73
+ dragging.current = false;
74
+ onCommit(valueAt(event));
75
+ }
76
+ }, onKeyDown: onKeyDown, children: gradient ? (_jsx(Box, { as: motion.span, position: "absolute", background: "surface", initial: false, animate: { left: `${Math.max(pct, 4)}%` }, transition: { duration: 0 }, css: { top: 0, bottom: 0, right: 0, opacity: 0.72, pointerEvents: 'none' } })) : (_jsx(Box, { as: motion.span, position: "absolute", background: "accent", initial: false, animate: { width: `${pct}%` }, transition: { duration: 0 }, css: { top: 0, bottom: 0, left: 0 } })) }));
77
+ }
78
+ /** One round power icon button with two states: filled with the accent while on, neutral while off. */
79
+ export function PowerButton({ on, onToggle }) {
80
+ return (_jsx(Flex, { as: "button", type: "button", "aria-pressed": on, "aria-label": on ? 'Turn off' : 'Turn on', title: on ? 'Turn off' : 'Turn on', onClick: onToggle, align: "center", justify: "center", radius: "full", cursor: "pointer", width: 36, height: 36, p: 0, background: on ? 'accent' : 'surface', color: on ? 'accentText' : 'line', css: { flex: 'none', transition: 'background-color 0.2s ease, color 0.2s ease' }, children: _jsx(Icon, { name: "lu:power", size: 16 }) }));
81
+ }
82
+ /** A wrapping row of pill choices (modes, presets) with the selected one filled. With `tabs` it is
83
+ * a tab list (the selected chip is the open tab) that never wraps: too many to fit scroll sideways. */
84
+ export function ChipRow({ options, value, onChange, tabs = false, }) {
85
+ // Tabs scroll sideways when there are more than fit, and fade at the edge that has more.
86
+ const Wrap = tabs ? FadeScroll : Flex;
87
+ return (_jsx(Wrap, { gap: 2, ...(tabs ? { role: 'tablist' } : {}), css: tabs
88
+ ? { flexShrink: 0, scrollbarWidth: 'none', '&::-webkit-scrollbar': { display: 'none' } }
89
+ : { flexWrap: 'wrap' }, children: options.map((option) => {
90
+ const selected = option.value === value;
91
+ return (_jsxs(Flex, { as: "button", type: "button", ...(tabs ? { role: 'tab', 'aria-selected': selected } : { 'aria-pressed': selected }), ...(option.ariaLabel ? { 'aria-label': option.ariaLabel } : {}), onClick: () => onChange(option.value), align: "center", gap: 1.5, radius: "full", cursor: "pointer", py: 1.5, px: 3, background: selected ? 'accent' : 'surface', color: selected ? 'accentText' : 'text', css: { flex: 'none', whiteSpace: 'nowrap' }, children: [option.icon ? _jsx(Icon, { name: option.icon, size: 14 }) : null, _jsx(Typography, { as: "span", variant: "body", noWrap: true, textOverflow: "ellipsis", css: { maxWidth: 220 }, children: option.label })] }, option.value));
92
+ }) }));
93
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawer-controls.js","sourceRoot":"","sources":["../../src/layout/drawer-controls.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /** @jsxImportSource @emotion/react */
2
+ import type { EntityRef } from '@hashsome/core';
3
+ export interface EnergySample {
4
+ /** ISO 8601. */
5
+ timestamp: string;
6
+ watts: number;
7
+ }
8
+ export type EnergyRange = '1h' | '1d' | '1w' | '1m';
9
+ /** Energy used (kWh) per period. Collapsed shows today / this week / this month; expanded swaps the last two for the rolling last 7 / last 30 days. */
10
+ export interface EnergyUsage {
11
+ today?: number;
12
+ week?: number;
13
+ month?: number;
14
+ last7d?: number;
15
+ last30d?: number;
16
+ }
17
+ export interface EnergyChartProps {
18
+ /** Live power sensor (current draw, e.g. a Shelly's `sensor.*_power`). Its reading is the headline and updates as it changes; without it the headline falls back to the last chart sample. */
19
+ power?: EntityRef;
20
+ /** Lifetime energy counter (kWh, e.g. `sensor.*_energy`). Shown as a quiet "Lifetime" line when expanded — per-period usage can't be derived from a counter without history, so pass `usage` for that. */
21
+ energy?: EntityRef;
22
+ /** Usage per period in kWh. Left out, it is worked out from `energy`'s history when the backend keeps one (Home Assistant's long-term statistics); pass it to use your own. */
23
+ usage?: EnergyUsage;
24
+ /** Samples for each range the person can pick; `onRangeChange` fires on pick. Left out, they are loaded from `power`'s history when the backend keeps one, for the selected range; pass them to use your own. */
25
+ seriesByRange?: Partial<Record<EnergyRange, EnergySample[]>>;
26
+ /** Called when the person picks another range, so a real data source can load it. */
27
+ onRangeChange?: (range: EnergyRange) => void;
28
+ /** Default `'W'`. */
29
+ unit?: string;
30
+ }
31
+ /**
32
+ * A power-draw chart for an entity's detail drawer, e.g. from a Shelly's energy monitoring.
33
+ * Generic — any entity type can supply it. Deliberately two-tier: a bare sparkline while the
34
+ * drawer is at its normal (side) width, and the full interactive chart — axes, tooltip, a
35
+ * selectable time range — only once it's expanded to fullscreen, where there's room for it.
36
+ */
37
+ export declare function EnergyChart({ power, energy, usage, seriesByRange, onRangeChange, unit, }: EnergyChartProps): import("@emotion/react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
+ import { Flex, Typography } from 'e-prim';
3
+ import { useState } from 'react';
4
+ import { useEntityHandle } from '../hooks.js';
5
+ import { useEntityHistory } from '../use-entity-history.js';
6
+ import { usageFromDaily } from './energy-usage.js';
7
+ import { useDetail } from './detail-provider.js';
8
+ import { RangeSwitcher, SeriesChart } from './series-chart.js';
9
+ const ENERGY_RANGES = [
10
+ { value: '1h', label: '1H' },
11
+ { value: '1d', label: '1D' },
12
+ { value: '1w', label: '1W' },
13
+ { value: '1m', label: '1M' },
14
+ ];
15
+ function formatEnergy(kwh) {
16
+ if (kwh < 1) {
17
+ return `${Math.round(kwh * 1000)} Wh`;
18
+ }
19
+ return `${kwh < 10 ? kwh.toFixed(2) : kwh.toFixed(1)} kWh`;
20
+ }
21
+ /**
22
+ * A power-draw chart for an entity's detail drawer, e.g. from a Shelly's energy monitoring.
23
+ * Generic — any entity type can supply it. Deliberately two-tier: a bare sparkline while the
24
+ * drawer is at its normal (side) width, and the full interactive chart — axes, tooltip, a
25
+ * selectable time range — only once it's expanded to fullscreen, where there's room for it.
26
+ */
27
+ export function EnergyChart({ power, energy, usage, seriesByRange = {}, onRangeChange, unit = 'W', }) {
28
+ const { detail } = useDetail();
29
+ const expanded = detail?.expanded ?? false;
30
+ const [range, setRange] = useState('1d');
31
+ // Sparkline (collapsed) and chart (expanded) both show the selected range, 1D until picked.
32
+ const ownSamples = seriesByRange[range];
33
+ const powerHistory = useEntityHistory(ownSamples ? undefined : power, power ? { range } : undefined);
34
+ const energyHistory = useEntityHistory(usage ? undefined : energy, energy ? { range: '1m', bucket: '1d' } : undefined);
35
+ const samples = ownSamples ??
36
+ (powerHistory.result?.points ?? []).map((point) => ({
37
+ timestamp: point.timestamp,
38
+ watts: point.value,
39
+ }));
40
+ const shownUsage = usage ??
41
+ (energyHistory.result ? usageFromDaily(energyHistory.result.points, new Date()) : undefined);
42
+ if (!power && !shownUsage && !energy && samples.length === 0) {
43
+ return null;
44
+ }
45
+ const fallbackCurrent = samples.at(-1)?.watts ?? 0;
46
+ const selectRange = (next) => {
47
+ setRange(next);
48
+ onRangeChange?.(next);
49
+ };
50
+ return (_jsxs(Flex, { direction: "column", gap: 2, mt: 4, children: [_jsxs(Flex, { justify: "space-between", align: "center", children: [_jsx(Typography, { as: "span", variant: "eyebrow", uppercase: true, color: "textMuted", children: "Power" }), expanded ? (_jsx(RangeSwitcher, { range: range, options: ENERGY_RANGES, onChange: selectRange })) : null] }), expanded ? null : power ? (_jsx(PowerReading, { entity: power, fallbackUnit: unit })) : (_jsxs(Typography, { as: "span", variant: "stat", children: [fallbackCurrent, " ", unit] })), shownUsage ? _jsx(UsageTiles, { usage: shownUsage, expanded: expanded }) : null, expanded && energy ? _jsx(LifetimeEnergy, { entity: energy }) : null, samples.length === 0 ? null : (_jsx(SeriesChart, { samples: samples.map((sample) => ({ timestamp: sample.timestamp, value: sample.watts })), range: range, expanded: expanded, unit: unit }))] }));
51
+ }
52
+ function PowerReading({ entity, fallbackUnit }) {
53
+ const handle = useEntityHandle('sensor', entity);
54
+ const sensor = handle.entity;
55
+ const unit = sensor?.unit ?? fallbackUnit;
56
+ return (_jsx(Typography, { as: "span", variant: "stat", "aria-live": "off", children: handle.status === 'ready' && sensor?.numeric !== undefined
57
+ ? `${sensor.numeric} ${unit}`
58
+ : '—' }));
59
+ }
60
+ function LifetimeEnergy({ entity }) {
61
+ const handle = useEntityHandle('sensor', entity);
62
+ const sensor = handle.entity;
63
+ if (handle.status !== 'ready' || sensor?.numeric === undefined) {
64
+ return null;
65
+ }
66
+ return (_jsxs(Typography, { as: "span", variant: "secondary", color: "textMuted", children: ["Lifetime ", sensor.numeric, " ", sensor.unit ?? 'kWh'] }));
67
+ }
68
+ function UsageTiles({ usage, expanded }) {
69
+ const items = expanded
70
+ ? [
71
+ { label: 'Today', value: usage.today },
72
+ { label: 'Last 7 days', value: usage.last7d },
73
+ { label: 'Last 30 days', value: usage.last30d },
74
+ ]
75
+ : [
76
+ { label: 'Today', value: usage.today },
77
+ { label: 'This week', value: usage.week },
78
+ { label: 'This month', value: usage.month },
79
+ ];
80
+ const shown = items.filter((item) => item.value !== undefined);
81
+ if (shown.length === 0) {
82
+ return null;
83
+ }
84
+ return (_jsx(Flex, { gap: 2, children: shown.map((item) => (_jsxs(Flex, { direction: "column", grow: 1, background: "surfaceRaised", radius: "row", py: 2.5, px: 3, css: { flexBasis: 0 }, children: [_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: item.label }), _jsx(Typography, { as: "span", variant: "bodyStrong", children: formatEnergy(item.value ?? 0) })] }, item.label))) }));
85
+ }