@hashsome/ui 0.5.0 → 0.7.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 (42) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/debug.d.ts +27 -0
  3. package/dist/debug.js +95 -0
  4. package/dist/entities/artwork-ring.d.ts +4 -2
  5. package/dist/entities/artwork-ring.js +78 -61
  6. package/dist/entities/climate-tile.js +2 -1
  7. package/dist/entities/media-browser.d.ts +3 -1
  8. package/dist/entities/media-browser.js +41 -15
  9. package/dist/entities/media-player-bar.js +1 -1
  10. package/dist/entities/media-player-column.d.ts +5 -2
  11. package/dist/entities/media-player-column.js +33 -5
  12. package/dist/entities/media-player-full.d.ts +12 -2
  13. package/dist/entities/media-player-full.js +45 -3
  14. package/dist/entities/now-playing.d.ts +3 -1
  15. package/dist/entities/now-playing.js +8 -3
  16. package/dist/entities/sensor-readout.js +8 -4
  17. package/dist/entities/top-bar.js +6 -2
  18. package/dist/gallery/gallery.js +13 -1
  19. package/dist/gallery/props-data.js +125 -3
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +3 -0
  22. package/dist/layout/animated-number.d.ts +15 -0
  23. package/dist/layout/animated-number.js +53 -0
  24. package/dist/layout/board.d.ts +26 -0
  25. package/dist/layout/board.js +85 -0
  26. package/dist/layout/debug-menu.d.ts +11 -0
  27. package/dist/layout/debug-menu.js +99 -0
  28. package/dist/layout/drawer-controls.js +18 -6
  29. package/dist/layout/energy-chart.js +2 -3
  30. package/dist/layout/grid-overlay.d.ts +6 -0
  31. package/dist/layout/grid-overlay.js +143 -0
  32. package/dist/layout/page.d.ts +7 -1
  33. package/dist/layout/page.js +43 -6
  34. package/dist/layout/room-header.js +1 -1
  35. package/dist/layout/tile.js +10 -2
  36. package/dist/layout/top-row.d.ts +9 -0
  37. package/dist/layout/top-row.js +10 -0
  38. package/dist/provider.d.ts +7 -3
  39. package/dist/provider.js +39 -21
  40. package/dist/theme/grid.d.ts +30 -0
  41. package/dist/theme/grid.js +38 -0
  42. package/package.json +2 -2
package/dist/provider.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { RemoteClient } from '@hashsome/core';
2
+ import { RemoteClient, } from '@hashsome/core';
3
3
  import { Global, ThemeProvider as EmotionThemeProvider } from '@emotion/react';
4
4
  import { ThemeProvider } from 'e-prim';
5
5
  import { MotionGlobalConfig } from 'motion/react';
6
- import { createContext, useContext, useEffect, useMemo, useState, useSyncExternalStore, } from 'react';
6
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore, } from 'react';
7
+ import { DebugContext, debugFromEnv, useDebugState, useFullscreenKept, } from './debug.js';
8
+ import { DebugMenu } from './layout/debug-menu.js';
7
9
  import { DetailProvider } from './layout/detail-provider.js';
8
10
  import { EntityDrawer } from './layout/entity-drawer.js';
9
11
  import { UNITS_PER_SPACE } from './theme/density.js';
@@ -51,22 +53,13 @@ export function useThemeToggle() {
51
53
  }
52
54
  return value;
53
55
  }
54
- const MOTION_KEY = 'hashsome:motion';
55
- /** What this device chose for motion: `?motion=reduced` or `?motion=full` on the address (which is
56
- * also remembered), or what an earlier visit remembered. `?motion=auto` forgets it. */
57
- function motionFromDevice() {
56
+ /** What the address asks for motion: `?motion=reduced` or `?motion=full`. Nothing is kept, and the
57
+ * app reads it once, when it starts, so moving between pages inside the app keeps it and a reload
58
+ * without it does not. `?motion=auto` (or anything else) leaves it to the prop. */
59
+ function motionFromAddress() {
58
60
  try {
59
61
  const asked = new URLSearchParams(window.location.search).get('motion');
60
- if (asked === 'reduced' || asked === 'full') {
61
- localStorage.setItem(MOTION_KEY, asked);
62
- return asked;
63
- }
64
- if (asked === 'auto') {
65
- localStorage.removeItem(MOTION_KEY);
66
- return undefined;
67
- }
68
- const kept = localStorage.getItem(MOTION_KEY);
69
- return kept === 'reduced' || kept === 'full' ? kept : undefined;
62
+ return asked === 'reduced' || asked === 'full' ? asked : undefined;
70
63
  }
71
64
  catch {
72
65
  return undefined;
@@ -77,7 +70,7 @@ function motionFromDevice() {
77
70
  * transitions and animations too. */
78
71
  function useMotionMode(preference) {
79
72
  const [mode] = useState(() => {
80
- const chosen = motionFromDevice() ?? preference;
73
+ const chosen = motionFromAddress() ?? preference;
81
74
  MotionGlobalConfig.skipAnimations = chosen === 'reduced';
82
75
  if (typeof document !== 'undefined') {
83
76
  if (chosen === 'reduced') {
@@ -102,6 +95,22 @@ function rememberedSun() {
102
95
  return undefined;
103
96
  }
104
97
  }
98
+ /** The sun entity a theme follows, if it is a sun schedule. */
99
+ function sunOf(mode) {
100
+ return typeof mode === 'object' && 'sun' in mode ? mode.sun : undefined;
101
+ }
102
+ /** The theme in effect: what the debug menu chose, if it chose, else what the project configured.
103
+ * The sun is the project's: the one its theme follows, or the `sun` it gave. Without one there is no
104
+ * sun to follow, so that choice (kept from a visit that had one) leaves the configured theme as it was. */
105
+ function chosenTheme(configured, choice, sun) {
106
+ if (choice === null) {
107
+ return configured;
108
+ }
109
+ if (choice === 'sun') {
110
+ return sun === undefined ? configured : { sun };
111
+ }
112
+ return choice;
113
+ }
105
114
  /** Resolves the configured mode: `'system'` against the live OS/browser preference (updating if it
106
115
  * changes while open — a kiosk tablet left running overnight should follow a scheduled OS-level dark
107
116
  * mode, for example), a time range against the clock (re-checked at each boundary and whenever the
@@ -151,7 +160,12 @@ function useThemeMode(mode, client) {
151
160
  document.removeEventListener('visibilitychange', onVisible);
152
161
  };
153
162
  }, [from, to]);
154
- const sun = useSyncExternalStore((onChange) => (sunRef ? client.subscribe(sunRef, onChange) : () => undefined), () => (sunRef ? client.getEntity(sunRef) : undefined), () => undefined);
163
+ // Memoized, like every other `subscribe` given to `useSyncExternalStore` (see hooks.ts): a new
164
+ // function each render makes React resubscribe, which against a remote runtime is an unsubscribe and
165
+ // a subscribe whose reply is a new entity object, which renders again, without end.
166
+ const subscribeSun = useCallback((onChange) => (sunRef ? client.subscribe(sunRef, onChange) : () => undefined), [client, sunRef]);
167
+ const readSun = useCallback(() => (sunRef ? client.getEntity(sunRef) : undefined), [client, sunRef]);
168
+ const sun = useSyncExternalStore(subscribeSun, readSun, () => undefined);
155
169
  const sunDown = sunIsDown(sun);
156
170
  useEffect(() => {
157
171
  if (sunDown === undefined) {
@@ -181,10 +195,14 @@ function useThemeMode(mode, client) {
181
195
  return { resolved, toggle: () => setOverride(resolved === 'dark' ? 'light' : 'dark') };
182
196
  }
183
197
  /** Connects the tree to the runtime proxy. Render only on the client. */
184
- export function HashsomeProvider({ url, client, clientOptions, theme = 'dark', motion = 'auto', font = DEFAULT_FONT, density = 'comfortable', overrides, children, }) {
198
+ export function HashsomeProvider({ url, client, clientOptions, theme = 'dark', motion = 'auto', font = DEFAULT_FONT, density = 'comfortable', overrides, debug = debugFromEnv(), sun, children, }) {
185
199
  const instance = useMemo(() => client ?? new RemoteClient({ url: url ?? defaultUrl(), ...clientOptions }), [client, url, clientOptions]);
186
200
  useMotionMode(motion);
187
- const themeMode = useThemeMode(theme, instance);
201
+ const debugState = useDebugState();
202
+ useFullscreenKept(debugState.fullscreen, debugState.setFullscreen);
203
+ // The debug menu can choose a theme over the configured one; the sun is the project's own entity.
204
+ const sunEntity = sunOf(theme) ?? sun;
205
+ const themeMode = useThemeMode(chosenTheme(theme, debug ? debugState.themeChoice : null, sunEntity), instance);
188
206
  // Memoized: Emotion recomputes the merged theme (and every `css` prop) when the function changes.
189
207
  const withDensity = useMemo(() => (outer) => ({ ...outer, density: densityTokens(density, overrides) }), [density, overrides]);
190
208
  useGoogleFont(font);
@@ -197,7 +215,7 @@ export function HashsomeProvider({ url, client, clientOptions, theme = 'dark', m
197
215
  instance.connect();
198
216
  return () => instance.close();
199
217
  }, [instance]);
200
- return (_jsx(HashsomeContext.Provider, { value: instance, children: _jsx(ThemeModeContext.Provider, { value: themeMode, children: _jsx(ThemeProvider, { theme: resolvedTheme, children: _jsxs(EmotionThemeProvider, { theme: withDensity, children: [_jsx(Global, { styles: globalStyles }), _jsxs(DetailProvider, { children: [children, _jsx(EntityDrawer, {})] })] }) }) }) }));
218
+ return (_jsx(HashsomeContext.Provider, { value: instance, children: _jsx(ThemeModeContext.Provider, { value: themeMode, children: _jsx(ThemeProvider, { theme: resolvedTheme, children: _jsxs(EmotionThemeProvider, { theme: withDensity, children: [_jsx(Global, { styles: globalStyles }), _jsx(DebugContext.Provider, { value: debugState, children: _jsxs(DetailProvider, { children: [children, _jsx(EntityDrawer, {}), debug ? _jsx(DebugMenu, { configured: theme, sun: sunEntity }) : null] }) })] }) }) }) }));
201
219
  }
202
220
  export function useClient() {
203
221
  const client = useContext(HashsomeContext);
@@ -0,0 +1,30 @@
1
+ /** The icon badge a tile starts with, in px: the one size in a tile that does not scale with density. */
2
+ export declare const BADGE = 32;
3
+ /** The height of a room header, in px. */
4
+ export declare const HEADER = 32;
5
+ /** The height of the top row, in px: the clock's line and the tallest things beside it (the switcher, the
6
+ * scene buttons), with no padding of its own, so its content sits as far from the page's edge as
7
+ * everything else does. Sizes that do not scale with density are multiples of 8, so they are whole
8
+ * modules in both (4px and 2.67px). */
9
+ export declare const TOP_ROW = 40;
10
+ /** The vertical grid every card sits on. The module is the theme's spacing unit (a third of a
11
+ * space: 4px in comfortable density, 2.67px in compact), so the gap between cards is always 3
12
+ * modules, and a tile and a header are whole numbers of them in either density. Sizes are in px;
13
+ * `pitch` is a card's height plus the gap that follows it, and a card spanning n pitches is n heights
14
+ * and n-1 gaps tall. */
15
+ export interface GridMetrics {
16
+ module: number;
17
+ gap: number;
18
+ tile: number;
19
+ header: number;
20
+ tilePitch: number;
21
+ headerPitch: number;
22
+ }
23
+ export declare function gridMetrics(space: number): GridMetrics;
24
+ /** Whether a box's top edge, measured from the grid's origin, or its height is off the module grid. Browsers round to the pixel, so a little under a pixel is still on it. */
25
+ export declare function offGrid(top: number, height: number, module: number): boolean;
26
+ /** How a height's leftover (what is under one module after as many whole ones as fit) is shared between the top and the bottom padding: half each, in whole pixels, the odd one to the bottom. `available` is the height inside the page's padding. */
27
+ export declare function centeringOffsets(available: number, module: number): {
28
+ top: number;
29
+ bottom: number;
30
+ };
@@ -0,0 +1,38 @@
1
+ import { UNITS_PER_SPACE } from './density.js';
2
+ /** The icon badge a tile starts with, in px: the one size in a tile that does not scale with density. */
3
+ export const BADGE = 32;
4
+ /** The height of a room header, in px. */
5
+ export const HEADER = 32;
6
+ /** The height of the top row, in px: the clock's line and the tallest things beside it (the switcher, the
7
+ * scene buttons), with no padding of its own, so its content sits as far from the page's edge as
8
+ * everything else does. Sizes that do not scale with density are multiples of 8, so they are whole
9
+ * modules in both (4px and 2.67px). */
10
+ export const TOP_ROW = 40;
11
+ export function gridMetrics(space) {
12
+ const module = space / UNITS_PER_SPACE;
13
+ // A tile is its badge between two spacing-unit-2 paddings.
14
+ const tile = BADGE + 4 * module;
15
+ return {
16
+ module,
17
+ gap: space,
18
+ tile,
19
+ header: HEADER,
20
+ tilePitch: tile + space,
21
+ headerPitch: HEADER + space,
22
+ };
23
+ }
24
+ /** Whether a box's top edge, measured from the grid's origin, or its height is off the module grid. Browsers round to the pixel, so a little under a pixel is still on it. */
25
+ export function offGrid(top, height, module) {
26
+ const tolerance = Math.max(0.5, module * 0.15);
27
+ const off = (value) => {
28
+ const rest = Math.abs(value) % module;
29
+ return Math.min(rest, module - rest) > tolerance;
30
+ };
31
+ return off(top) || off(height);
32
+ }
33
+ /** How a height's leftover (what is under one module after as many whole ones as fit) is shared between the top and the bottom padding: half each, in whole pixels, the odd one to the bottom. `available` is the height inside the page's padding. */
34
+ export function centeringOffsets(available, module) {
35
+ const leftover = available > 0 ? Math.floor(available % module) : 0;
36
+ const top = Math.floor(leftover / 2);
37
+ return { top, bottom: leftover - top };
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hashsome/ui",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Design system components and hooks",
5
5
  "type": "module",
6
6
  "exports": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@emotion/react": "11.14.0",
19
- "@hashsome/core": "0.5.0",
19
+ "@hashsome/core": "0.7.0",
20
20
  "e-prim": "2.0.1",
21
21
  "motion": "13.4.5",
22
22
  "react": "19.3.0",