@hashsome/ui 0.1.0 → 0.3.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/entities/artwork-ring.d.ts +1 -1
- package/dist/entities/artwork-ring.js +40 -14
- package/dist/entities/climate-tile.d.ts +1 -1
- package/dist/entities/climate-tile.js +1 -1
- package/dist/entities/light-tile.d.ts +1 -1
- package/dist/entities/light-tile.js +1 -1
- package/dist/entities/media-browser.js +76 -20
- package/dist/entities/media-player-bar.js +3 -2
- package/dist/entities/media-player-column.js +3 -2
- package/dist/entities/media-player-full.d.ts +22 -0
- package/dist/entities/media-player-full.js +19 -0
- package/dist/entities/media-queue.d.ts +14 -0
- package/dist/entities/media-queue.js +151 -0
- package/dist/entities/now-playing.d.ts +1 -1
- package/dist/entities/now-playing.js +1 -1
- package/dist/entities/top-bar.d.ts +1 -1
- package/dist/entities/top-bar.js +26 -23
- package/dist/entities/weather-forecast.d.ts +13 -0
- package/dist/entities/weather-forecast.js +148 -0
- package/dist/entities/weather-icon.d.ts +7 -0
- package/dist/entities/weather-icon.js +11 -0
- package/dist/entities/weather-look.d.ts +17 -0
- package/dist/entities/weather-look.js +75 -0
- package/dist/gallery/fixtures.d.ts +8 -2
- package/dist/gallery/fixtures.js +22 -3
- package/dist/gallery/gallery.js +7 -5
- package/dist/gallery/props-data.js +66 -8
- package/dist/hooks.d.ts +6 -0
- package/dist/hooks.js +18 -1
- package/dist/icon.d.ts +3 -1
- package/dist/icon.js +6 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/layout/drawer-controls.d.ts +2 -0
- package/dist/layout/drawer-controls.js +11 -1
- package/dist/layout/history-section.d.ts +3 -13
- package/dist/layout/history-section.js +15 -5
- package/dist/layout/logbook-history.d.ts +6 -0
- package/dist/layout/logbook-history.js +9 -0
- package/dist/layout/marquee-text.d.ts +11 -0
- package/dist/layout/marquee-text.js +67 -0
- package/dist/layout/reveal.d.ts +11 -0
- package/dist/layout/reveal.js +19 -0
- package/dist/layout/series-chart.d.ts +5 -1
- package/dist/layout/series-chart.js +9 -4
- package/dist/layout/skeleton.d.ts +11 -0
- package/dist/layout/skeleton.js +16 -0
- package/dist/layout/tile.d.ts +9 -1
- package/dist/layout/tile.js +8 -4
- package/dist/use-entity-history.js +5 -1
- package/dist/use-entity-logbook.d.ts +13 -0
- package/dist/use-entity-logbook.js +46 -0
- package/dist/use-media-browser.d.ts +4 -0
- package/dist/use-media-browser.js +11 -3
- package/dist/use-media-queue.d.ts +15 -0
- package/dist/use-media-queue.js +44 -0
- package/dist/use-weather-forecast.d.ts +12 -0
- package/dist/use-weather-forecast.js +45 -0
- package/package.json +2 -2
- package/dist/entities/action-button.js.map +0 -1
- package/dist/entities/artwork-ring.js.map +0 -1
- package/dist/entities/climate-tile.js.map +0 -1
- package/dist/entities/light-color.js.map +0 -1
- package/dist/entities/light-tile.js.map +0 -1
- package/dist/entities/media-browser.js.map +0 -1
- package/dist/entities/media-controls.js.map +0 -1
- package/dist/entities/media-player-bar.js.map +0 -1
- package/dist/entities/media-player-body.d.ts +0 -16
- package/dist/entities/media-player-body.js +0 -18
- package/dist/entities/media-player-body.js.map +0 -1
- package/dist/entities/media-player-column.js.map +0 -1
- package/dist/entities/media-player-page.d.ts +0 -12
- package/dist/entities/media-player-page.js +0 -13
- package/dist/entities/media-player-page.js.map +0 -1
- package/dist/entities/media-progress.js.map +0 -1
- package/dist/entities/media-seek.js.map +0 -1
- package/dist/entities/nav-dock.js.map +0 -1
- package/dist/entities/nav-rail.js.map +0 -1
- package/dist/entities/now-playing.js.map +0 -1
- package/dist/entities/scene-button.js.map +0 -1
- package/dist/entities/seek-line.js.map +0 -1
- package/dist/entities/sensor-readout.js.map +0 -1
- package/dist/entities/top-bar.js.map +0 -1
- package/dist/entity-handle.js.map +0 -1
- package/dist/gallery/fixtures.js.map +0 -1
- package/dist/gallery/gallery.js.map +0 -1
- package/dist/gallery/index.js.map +0 -1
- package/dist/gallery/props-data.js.map +0 -1
- package/dist/hooks.js.map +0 -1
- package/dist/icon-data.js.map +0 -1
- package/dist/icon.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/layout/cover.js.map +0 -1
- package/dist/layout/date-time-field.js.map +0 -1
- package/dist/layout/detail-provider.js.map +0 -1
- package/dist/layout/drawer-controls.js.map +0 -1
- package/dist/layout/energy-chart.js.map +0 -1
- package/dist/layout/energy-usage.js.map +0 -1
- package/dist/layout/entity-drawer.js.map +0 -1
- package/dist/layout/fade-scroll.js.map +0 -1
- package/dist/layout/grid.js.map +0 -1
- package/dist/layout/history-section.js.map +0 -1
- package/dist/layout/hold-progress.js.map +0 -1
- package/dist/layout/icon-button-styles.js.map +0 -1
- package/dist/layout/page.js.map +0 -1
- package/dist/layout/plain-button.js.map +0 -1
- package/dist/layout/room-header.js.map +0 -1
- package/dist/layout/round-button.js.map +0 -1
- package/dist/layout/sensor-history.js.map +0 -1
- package/dist/layout/sensor-stats.js.map +0 -1
- package/dist/layout/series-chart.js.map +0 -1
- package/dist/layout/stepper.js.map +0 -1
- package/dist/layout/swatch-row.js.map +0 -1
- package/dist/layout/tile.js.map +0 -1
- package/dist/layout/use-drawer.js.map +0 -1
- package/dist/layout/use-hold.js.map +0 -1
- package/dist/provider.js.map +0 -1
- package/dist/status.js.map +0 -1
- package/dist/theme/density.js.map +0 -1
- package/dist/theme/global-styles.js.map +0 -1
- package/dist/theme/index.js.map +0 -1
- package/dist/theme/overrides.js.map +0 -1
- package/dist/theme/tokens.js.map +0 -1
- package/dist/theme/use-typography.js.map +0 -1
- package/dist/use-entity-history.js.map +0 -1
- package/dist/use-media-browser.js.map +0 -1
|
@@ -157,6 +157,12 @@ export const COMPONENT_PROPS = {
|
|
|
157
157
|
optional: true,
|
|
158
158
|
type: 'LogbookEntry[]',
|
|
159
159
|
},
|
|
160
|
+
{
|
|
161
|
+
doc: "An entity whose recent activity to fetch from the backend when the drawer opens, for the same History section (Home Assistant's logbook). `history`, when given, is used instead. A backend that keeps none shows no section.",
|
|
162
|
+
name: 'logbook',
|
|
163
|
+
optional: true,
|
|
164
|
+
type: 'EntityRef',
|
|
165
|
+
},
|
|
160
166
|
],
|
|
161
167
|
},
|
|
162
168
|
IconButton: {
|
|
@@ -230,6 +236,12 @@ export const COMPONENT_PROPS = {
|
|
|
230
236
|
optional: true,
|
|
231
237
|
type: 'number',
|
|
232
238
|
},
|
|
239
|
+
{
|
|
240
|
+
doc: 'For an icon drawn in more than one color: part `i` of the icon (its `i`th shape) is drawn in `colors[i]`, and any part without one, in the surrounding text color. Only for outline icons.',
|
|
241
|
+
name: 'colors',
|
|
242
|
+
optional: true,
|
|
243
|
+
type: 'readonly (string | undefined)[]',
|
|
244
|
+
},
|
|
233
245
|
],
|
|
234
246
|
},
|
|
235
247
|
LightTile: {
|
|
@@ -260,7 +272,7 @@ export const COMPONENT_PROPS = {
|
|
|
260
272
|
type: 'EnergyChartProps',
|
|
261
273
|
},
|
|
262
274
|
{
|
|
263
|
-
doc:
|
|
275
|
+
doc: "Recent activity, shown in the drawer: who or what changed it, and when. Left out, it comes from the backend's own record when it keeps one (Home Assistant's logbook), fetched when the drawer opens.",
|
|
264
276
|
name: 'history',
|
|
265
277
|
optional: true,
|
|
266
278
|
type: 'LogbookEntry[]',
|
|
@@ -301,7 +313,7 @@ export const COMPONENT_PROPS = {
|
|
|
301
313
|
type: 'EnergyChartProps',
|
|
302
314
|
},
|
|
303
315
|
{
|
|
304
|
-
doc:
|
|
316
|
+
doc: "Recent activity, shown in the drawer: who or what changed it, and when. Left out, it comes from the backend's own record when it keeps one (Home Assistant's logbook), fetched when the drawer opens.",
|
|
305
317
|
name: 'history',
|
|
306
318
|
optional: true,
|
|
307
319
|
type: 'LogbookEntry[]',
|
|
@@ -459,21 +471,39 @@ export const COMPONENT_PROPS = {
|
|
|
459
471
|
},
|
|
460
472
|
],
|
|
461
473
|
},
|
|
462
|
-
|
|
463
|
-
doc: "The player
|
|
474
|
+
MediaPlayerFull: {
|
|
475
|
+
doc: "The big player: the player centered at the top, and the library (or whatever `browser` is) below it. It is what the media cards' drawers show, at every width, and it is the widget a dashboard puts in a page of its own, around it whatever it likes (a surface that fills the space and scrolls, a heading, other cards beside it). It fills the height it is given, so the page that holds it decides how tall that is.",
|
|
464
476
|
props: [
|
|
465
477
|
{
|
|
466
|
-
doc: '
|
|
478
|
+
doc: 'The media player, as a ref like `ma:living_room`.',
|
|
467
479
|
name: 'entity',
|
|
468
480
|
optional: false,
|
|
469
|
-
type:
|
|
481
|
+
type: "EntityRef | EntityHandle<'mediaPlayer'>",
|
|
470
482
|
},
|
|
471
483
|
{
|
|
472
|
-
doc:
|
|
484
|
+
doc: 'What goes below the player, usually the player\'s library: `<MediaBrowser entity="ma:living_room" layout="theater" />`. Left out, only the player is drawn.',
|
|
485
|
+
name: 'browser',
|
|
486
|
+
optional: true,
|
|
487
|
+
type: 'ReactNode',
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
doc: 'What goes beside the player in a wide space, usually the player\'s queue: `<MediaQueue entity="ma:living_room" />`. Only used when `wide` and the player has a queue; the narrow layout has no room for it.',
|
|
491
|
+
name: 'queue',
|
|
492
|
+
optional: true,
|
|
493
|
+
type: 'ReactNode',
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
doc: 'Calls the player this instead of the name it reports.',
|
|
473
497
|
name: 'name',
|
|
474
498
|
optional: true,
|
|
475
499
|
type: 'string',
|
|
476
500
|
},
|
|
501
|
+
{
|
|
502
|
+
doc: 'Gives the library the whole width, for one laid out to use it (the theater layout). Defaults to whether the drawer is expanded.',
|
|
503
|
+
name: 'wide',
|
|
504
|
+
optional: true,
|
|
505
|
+
type: 'boolean',
|
|
506
|
+
},
|
|
477
507
|
],
|
|
478
508
|
},
|
|
479
509
|
MediaBrowser: {
|
|
@@ -499,6 +529,17 @@ export const COMPONENT_PROPS = {
|
|
|
499
529
|
},
|
|
500
530
|
],
|
|
501
531
|
},
|
|
532
|
+
MediaQueue: {
|
|
533
|
+
doc: "A player's queue: the tracks that have just played (dimmed), the one playing, and what comes next, as a list that scrolls. Tap a track to jump to it, the cross takes it out, and Clear takes out everything after the track that is playing, which carries on. What comes next can be put in another order by dragging a track by its handle (or with the arrow keys on the handle). Once the last track has played and the player has stopped, that track is shown as played, not as playing. It is made to sit beside the player in a wide space (see `MediaPlayerFull`'s `queue`).",
|
|
534
|
+
props: [
|
|
535
|
+
{
|
|
536
|
+
doc: 'The media player, as a ref like `ma:living_room`. Shows nothing for a player with no queue.',
|
|
537
|
+
name: 'entity',
|
|
538
|
+
optional: false,
|
|
539
|
+
type: 'EntityRef',
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
},
|
|
502
543
|
NavRail: {
|
|
503
544
|
doc: "Fixed left sidebar for switching between a dashboard's pages. Must be rendered inside a router. It is `position: fixed` and reserves its width in the surrounding `Page`, which pads for it so content never sits under it.",
|
|
504
545
|
props: [
|
|
@@ -567,7 +608,7 @@ export const COMPONENT_PROPS = {
|
|
|
567
608
|
type: '(EntityRef | TopBarScene)[]',
|
|
568
609
|
},
|
|
569
610
|
{
|
|
570
|
-
doc:
|
|
611
|
+
doc: "Current weather as a pill with an icon for the sky and a rounded whole-degree reading. A weather entity, like `ha:weather.forecast_home`, shows its real condition, today's high and low once its forecast has loaded, and opens the forecast (the next 24 hours and the days ahead) when tapped, if its source has one; a plain sensor, e.g. an outdoor temperature, gets a fixed sun and no forecast.",
|
|
571
612
|
name: 'weather',
|
|
572
613
|
optional: true,
|
|
573
614
|
type: 'EntityRef',
|
|
@@ -662,6 +703,23 @@ export const COMPONENT_PROPS = {
|
|
|
662
703
|
},
|
|
663
704
|
],
|
|
664
705
|
},
|
|
706
|
+
WeatherForecast: {
|
|
707
|
+
doc: "A weather entity's forecast: the weather now, the next 24 hours as a chart (hover it for everything about an hour) and the days ahead. The top bar's weather chip shows it in a drawer; a dashboard can put it in a page of its own, with `expanded` for the full-size layout. Wide, it adds the current readings and a card for each hour. A source with no forecast shows only the weather now.",
|
|
708
|
+
props: [
|
|
709
|
+
{
|
|
710
|
+
doc: 'A weather entity, as a ref like `ha:weather.forecast_home`. The forecasts it offers (hourly, daily) are read through the runtime.',
|
|
711
|
+
name: 'entity',
|
|
712
|
+
optional: false,
|
|
713
|
+
type: 'EntityRef',
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
doc: 'Lay it out for a wide space (readings tiles, a card for each hour, more columns for the days) or for a narrow one. Left out, it follows the drawer it is in.',
|
|
717
|
+
name: 'expanded',
|
|
718
|
+
optional: true,
|
|
719
|
+
type: 'boolean',
|
|
720
|
+
},
|
|
721
|
+
],
|
|
722
|
+
},
|
|
665
723
|
HashsomeProvider: {
|
|
666
724
|
doc: 'Connects the tree to the runtime proxy. Render only on the client.',
|
|
667
725
|
props: [
|
package/dist/hooks.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export declare function useCommand(ref: EntityRef | undefined): CommandSender;
|
|
|
11
11
|
export declare function useEntityHandle<K extends EntityKind>(kind: K, source: EntityRef | EntityHandle<K>): EntityHandle<K>;
|
|
12
12
|
/** Connection between this browser and the runtime. */
|
|
13
13
|
export declare function useConnectionStatus(): LinkStatus;
|
|
14
|
+
/**
|
|
15
|
+
* How many times the connection to the runtime has opened. A hook that asked for something while it
|
|
16
|
+
* was down (and got an error) depends on this, so it asks again when the connection is back: a read
|
|
17
|
+
* that failed because the page was ahead of the connection, or because it dropped, is not final.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useReconnects(): number;
|
|
14
20
|
/** Status of the runtime's connection to a backend (`ha`, `ma`, ...). */
|
|
15
21
|
export declare function useIntegrationStatus(integration: string): ConnectionStatus | undefined;
|
|
16
22
|
/** Status of every backend integration the runtime reports, keyed by integration id. */
|
package/dist/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
2
|
import { toHandle } from './entity-handle.js';
|
|
3
3
|
import { useClient } from './provider.js';
|
|
4
4
|
// Every `subscribe` handed to `useSyncExternalStore` below is memoized: React resubscribes whenever
|
|
@@ -33,6 +33,23 @@ export function useConnectionStatus() {
|
|
|
33
33
|
const subscribe = useCallback((onChange) => client.onLinkChange(onChange), [client]);
|
|
34
34
|
return useSyncExternalStore(subscribe, () => client.link, () => 'closed');
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* How many times the connection to the runtime has opened. A hook that asked for something while it
|
|
38
|
+
* was down (and got an error) depends on this, so it asks again when the connection is back: a read
|
|
39
|
+
* that failed because the page was ahead of the connection, or because it dropped, is not final.
|
|
40
|
+
*/
|
|
41
|
+
export function useReconnects() {
|
|
42
|
+
const link = useConnectionStatus();
|
|
43
|
+
const [opened, setOpened] = useState(0);
|
|
44
|
+
const was = useRef(link);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (link === 'open' && was.current !== 'open') {
|
|
47
|
+
setOpened((count) => count + 1);
|
|
48
|
+
}
|
|
49
|
+
was.current = link;
|
|
50
|
+
}, [link]);
|
|
51
|
+
return opened;
|
|
52
|
+
}
|
|
36
53
|
/** Status of the runtime's connection to a backend (`ha`, `ma`, ...). */
|
|
37
54
|
export function useIntegrationStatus(integration) {
|
|
38
55
|
const client = useClient();
|
package/dist/icon.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface IconProps {
|
|
|
4
4
|
name: IconName;
|
|
5
5
|
/** Overrides the density's icon size, in px. */
|
|
6
6
|
size?: number;
|
|
7
|
+
/** For an icon drawn in more than one color: part `i` of the icon (its `i`th shape) is drawn in `colors[i]`, and any part without one, in the surrounding text color. Only for outline icons. */
|
|
8
|
+
colors?: readonly (string | undefined)[];
|
|
7
9
|
}
|
|
8
10
|
/** 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;
|
|
11
|
+
export declare function Icon({ name, size, colors }: IconProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
package/dist/icon.js
CHANGED
|
@@ -7,7 +7,7 @@ const STROKE_WIDTH = 1.5;
|
|
|
7
7
|
* here (e.g. a future filled pack) renders solid instead. */
|
|
8
8
|
const STROKE_PACKS = new Set(['lu', 'tb']);
|
|
9
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 }) {
|
|
10
|
+
export function Icon({ name, size, colors }) {
|
|
11
11
|
const stroke = STROKE_PACKS.has(name.slice(0, name.indexOf(':')));
|
|
12
12
|
const nodes = ICON_NODES[name];
|
|
13
13
|
if (!nodes) {
|
|
@@ -24,5 +24,9 @@ export function Icon({ name, size }) {
|
|
|
24
24
|
strokeLinecap: 'round',
|
|
25
25
|
strokeLinejoin: 'round',
|
|
26
26
|
}
|
|
27
|
-
: {}), "aria-hidden": "true", children: (nodes ?? []).map(([tag, attrs], i) => createElement(tag, {
|
|
27
|
+
: {}), "aria-hidden": "true", children: (nodes ?? []).map(([tag, attrs], i) => createElement(tag, {
|
|
28
|
+
key: i,
|
|
29
|
+
...attrs,
|
|
30
|
+
...(stroke && colors?.[i] ? { stroke: colors[i] } : {}),
|
|
31
|
+
})) }));
|
|
28
32
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,8 +22,10 @@ export * from './entities/action-button.tsx';
|
|
|
22
22
|
export * from './entities/scene-button.tsx';
|
|
23
23
|
export * from './entities/media-player-bar.tsx';
|
|
24
24
|
export * from './entities/media-player-column.tsx';
|
|
25
|
-
export * from './entities/media-player-
|
|
25
|
+
export * from './entities/media-player-full.tsx';
|
|
26
|
+
export * from './entities/weather-forecast.tsx';
|
|
26
27
|
export * from './entities/media-browser.tsx';
|
|
28
|
+
export * from './entities/media-queue.tsx';
|
|
27
29
|
export { formatDuration } from './entities/media-progress.ts';
|
|
28
30
|
export * from './entities/nav-rail.tsx';
|
|
29
31
|
export * from './entities/nav-dock.tsx';
|
package/dist/index.js
CHANGED
|
@@ -16,8 +16,10 @@ export * from './entities/action-button.js';
|
|
|
16
16
|
export * from './entities/scene-button.js';
|
|
17
17
|
export * from './entities/media-player-bar.js';
|
|
18
18
|
export * from './entities/media-player-column.js';
|
|
19
|
-
export * from './entities/media-player-
|
|
19
|
+
export * from './entities/media-player-full.js';
|
|
20
|
+
export * from './entities/weather-forecast.js';
|
|
20
21
|
export * from './entities/media-browser.js';
|
|
22
|
+
export * from './entities/media-queue.js';
|
|
21
23
|
export { formatDuration } from './entities/media-progress.js';
|
|
22
24
|
export * from './entities/nav-rail.js';
|
|
23
25
|
export * from './entities/nav-dock.js';
|
|
@@ -31,6 +31,8 @@ export declare function PowerButton({ on, onToggle }: {
|
|
|
31
31
|
on: boolean;
|
|
32
32
|
onToggle: () => void;
|
|
33
33
|
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
34
|
+
/** The height of every chip, whatever its label holds. */
|
|
35
|
+
export declare const CHIP_HEIGHT = 28;
|
|
34
36
|
/** A wrapping row of pill choices (modes, presets) with the selected one filled. With `tabs` it is
|
|
35
37
|
* a tab list (the selected chip is the open tab) that never wraps: too many to fit scroll sideways. */
|
|
36
38
|
export declare function ChipRow({ options, value, onChange, tabs, }: {
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
|
2
2
|
/** @jsxImportSource @emotion/react */
|
|
3
3
|
import { Box, Flex, Typography } from 'e-prim';
|
|
4
4
|
import { motion } from 'motion/react';
|
|
5
|
+
import { MarqueeText } from './marquee-text.js';
|
|
5
6
|
import { useEffect, useRef } from 'react';
|
|
6
7
|
import { Icon } from '../icon.js';
|
|
7
8
|
import { FadeScroll } from './fade-scroll.js';
|
|
@@ -79,6 +80,8 @@ export function ValueBar({ label, value, min, max, keyStep, round = 1, gradient,
|
|
|
79
80
|
export function PowerButton({ on, onToggle }) {
|
|
80
81
|
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
|
}
|
|
83
|
+
/** The height of every chip, whatever its label holds. */
|
|
84
|
+
export const CHIP_HEIGHT = 28;
|
|
82
85
|
/** A wrapping row of pill choices (modes, presets) with the selected one filled. With `tabs` it is
|
|
83
86
|
* a tab list (the selected chip is the open tab) that never wraps: too many to fit scroll sideways. */
|
|
84
87
|
export function ChipRow({ options, value, onChange, tabs = false, }) {
|
|
@@ -88,6 +91,13 @@ export function ChipRow({ options, value, onChange, tabs = false, }) {
|
|
|
88
91
|
? { flexShrink: 0, scrollbarWidth: 'none', '&::-webkit-scrollbar': { display: 'none' } }
|
|
89
92
|
: { flexWrap: 'wrap' }, children: options.map((option) => {
|
|
90
93
|
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",
|
|
94
|
+
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",
|
|
95
|
+
// A fixed height, not the text's: an emoji is drawn from another font with a taller line,
|
|
96
|
+
// which would make this one chip taller than the rest.
|
|
97
|
+
height: CHIP_HEIGHT, px: 3, background: selected ? 'accent' : 'surface', color: selected ? 'accentText' : 'text', css: {
|
|
98
|
+
flex: 'none',
|
|
99
|
+
whiteSpace: 'nowrap',
|
|
100
|
+
transition: 'background-color 160ms ease, color 160ms ease',
|
|
101
|
+
}, children: [option.icon ? _jsx(Icon, { name: option.icon, size: 14 }) : null, _jsx(Typography, { as: "span", variant: "body", css: { minWidth: 0, maxWidth: 220 }, children: _jsx(MarqueeText, { children: option.label }) })] }, option.value));
|
|
92
102
|
}) }));
|
|
93
103
|
}
|
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* there's no `actor` to lead with ("Turned on"), left lowercase after one ("Dan turned on"). */
|
|
5
|
-
message: string;
|
|
6
|
-
/** ISO 8601. */
|
|
7
|
-
timestamp: string;
|
|
8
|
-
/** Who or what triggered it, e.g. "Dan" or "automation.bedtime". Omit for the system. */
|
|
9
|
-
actor?: string;
|
|
10
|
-
/** Only matters when `actor` is set: a person gets an initials avatar, an automation a robot
|
|
11
|
-
* icon. Default `'person'`. */
|
|
12
|
-
actorKind?: 'person' | 'automation';
|
|
13
|
-
}
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
import type { LogbookEntry } from '@hashsome/core';
|
|
3
|
+
export type { LogbookEntry };
|
|
14
4
|
export interface HistorySectionProps {
|
|
15
5
|
/** Recent activity, newest first. An empty list renders nothing. */
|
|
16
6
|
entries: LogbookEntry[];
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
2
|
-
/** @jsxImportSource @emotion/react */
|
|
3
2
|
import { Flex, Typography } from 'e-prim';
|
|
4
3
|
import { Icon } from '../icon.js';
|
|
5
4
|
import { capitalize } from '../status.js';
|
|
@@ -26,14 +25,25 @@ function initials(name) {
|
|
|
26
25
|
.map((word) => word.charAt(0).toUpperCase())
|
|
27
26
|
.join('');
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
/** The chip for a row nobody caused: a switch thrown the way it went for on and off, and a chip for
|
|
29
|
+
* the device coming or going or another mode. */
|
|
30
|
+
const QUIET_ICON = {
|
|
31
|
+
on: 'lu:toggle-right',
|
|
32
|
+
off: 'lu:toggle-left',
|
|
33
|
+
state: 'lu:cpu',
|
|
34
|
+
availability: 'lu:cpu',
|
|
35
|
+
};
|
|
36
|
+
/** The round picture at the start of a row: initials for a person, a workflow icon for an automation, and
|
|
37
|
+
* for nobody a quiet chip, so every row's text lines up: a switch for on and off (often someone at
|
|
38
|
+
* the device's own switch), a chip for the device coming or going. */
|
|
39
|
+
function Avatar({ actor, kind, change, }) {
|
|
40
|
+
const person = actor !== undefined && kind === 'person';
|
|
41
|
+
return (_jsx(Flex, { role: "img", "aria-label": actor ?? 'System', align: "center", justify: "center", background: person ? 'accent' : 'surfaceRaised', color: person ? 'accentText' : actor === undefined ? 'textMuted' : 'text', radius: "full", width: 32, height: 32, css: { flex: 'none' }, children: person ? (_jsx(Typography, { as: "span", variant: "eyebrow", children: initials(actor) })) : (_jsx(Icon, { name: actor !== undefined ? 'lu:workflow' : QUIET_ICON[change ?? 'availability'], size: 16 })) }));
|
|
32
42
|
}
|
|
33
43
|
/** A short activity list for an entity's detail drawer. Generic — any entity type can supply it. */
|
|
34
44
|
export function HistorySection({ entries }) {
|
|
35
45
|
if (entries.length === 0) {
|
|
36
46
|
return null;
|
|
37
47
|
}
|
|
38
|
-
return (_jsxs(Flex, { direction: "column", gap: 4, pt: 4, mt: "auto", children: [_jsx(Typography, { as: "span", variant: "eyebrow", uppercase: true, color: "textMuted", children: "History" }), _jsx(Flex, { direction: "column", children: entries.map((entry) => (_jsxs(Flex, { align: "center", gap: 3, py: 2, children: [
|
|
48
|
+
return (_jsxs(Flex, { direction: "column", gap: 4, pt: 4, mt: "auto", children: [_jsx(Typography, { as: "span", variant: "eyebrow", uppercase: true, color: "textMuted", children: "History" }), _jsx(Flex, { direction: "column", children: entries.map((entry) => (_jsxs(Flex, { align: "center", gap: 3, py: 2, children: [_jsx(Avatar, { actor: entry.actor, kind: entry.actorKind ?? 'person', change: entry.change }), _jsxs(Flex, { direction: "column", minWidth: 0, children: [_jsxs(Typography, { as: "span", variant: "body", noWrap: true, textOverflow: "ellipsis", minWidth: 0, children: [entry.actor ? (_jsxs(Typography, { as: "span", variant: "bodyStrong", children: [entry.actor, ' '] })) : null, entry.actor ? entry.message : capitalize(entry.message)] }), _jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: relativeTime(entry.timestamp) })] })] }, entry.id))) })] }));
|
|
39
49
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EntityRef } from '@hashsome/core';
|
|
2
|
+
/** A drawer's History section filled from the backend: it asks for the entity's recent activity when
|
|
3
|
+
* it is drawn (so only when the drawer opens), and shows nothing until there is some. */
|
|
4
|
+
export declare function LogbookHistory({ entity }: {
|
|
5
|
+
entity: EntityRef;
|
|
6
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEntityLogbook } from '../use-entity-logbook.js';
|
|
3
|
+
import { HistorySection } from './history-section.js';
|
|
4
|
+
/** A drawer's History section filled from the backend: it asks for the entity's recent activity when
|
|
5
|
+
* it is drawn (so only when the drawer opens), and shows nothing until there is some. */
|
|
6
|
+
export function LogbookHistory({ entity }) {
|
|
7
|
+
const { entries } = useEntityLogbook(entity);
|
|
8
|
+
return entries ? _jsx(HistorySection, { entries: entries }) : null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* One line of text that fits the width it is given: when it is too long it rolls sideways, resting at
|
|
4
|
+
* each end, so all of it can be read; when it fits it just sits there. Whoever it is in decides the
|
|
5
|
+
* width (a `max-width` on it or on a parent), so a long title can never push its tile out of shape,
|
|
6
|
+
* whatever it holds (an emoji at the end included). Where the person asks their device for less
|
|
7
|
+
* motion it stays still and ends in an ellipsis.
|
|
8
|
+
*/
|
|
9
|
+
export declare function MarqueeText({ children }: {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import { motion, useReducedMotion } from 'motion/react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
/** How fast the text rolls, in px a second. */
|
|
6
|
+
const SPEED = 36;
|
|
7
|
+
/** How long it waits at each end, in seconds. */
|
|
8
|
+
const PAUSE = 1.4;
|
|
9
|
+
/**
|
|
10
|
+
* One line of text that fits the width it is given: when it is too long it rolls sideways, resting at
|
|
11
|
+
* each end, so all of it can be read; when it fits it just sits there. Whoever it is in decides the
|
|
12
|
+
* width (a `max-width` on it or on a parent), so a long title can never push its tile out of shape,
|
|
13
|
+
* whatever it holds (an emoji at the end included). Where the person asks their device for less
|
|
14
|
+
* motion it stays still and ends in an ellipsis.
|
|
15
|
+
*/
|
|
16
|
+
export function MarqueeText({ children }) {
|
|
17
|
+
const outer = useRef(null);
|
|
18
|
+
const inner = useRef(null);
|
|
19
|
+
const [overflow, setOverflow] = useState(0);
|
|
20
|
+
const still = useReducedMotion();
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const frame = outer.current;
|
|
23
|
+
const text = inner.current;
|
|
24
|
+
if (!frame || !text) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const measure = () => setOverflow(Math.max(0, Math.ceil(text.scrollWidth - frame.clientWidth)));
|
|
28
|
+
measure();
|
|
29
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// The frame changes with the room it has; the text with what it says.
|
|
33
|
+
const watch = new ResizeObserver(measure);
|
|
34
|
+
watch.observe(frame);
|
|
35
|
+
watch.observe(text);
|
|
36
|
+
return () => watch.disconnect();
|
|
37
|
+
}, []);
|
|
38
|
+
const rolling = overflow > 0 && !still;
|
|
39
|
+
const seconds = overflow / SPEED;
|
|
40
|
+
return (_jsx("span", { ref: outer, css: {
|
|
41
|
+
display: 'block',
|
|
42
|
+
// A line height of its own, not the font's: an emoji comes from another font with a taller
|
|
43
|
+
// line, and would make a row with one in its title taller than the rest.
|
|
44
|
+
lineHeight: 1.3,
|
|
45
|
+
minWidth: 0,
|
|
46
|
+
maxWidth: '100%',
|
|
47
|
+
overflow: 'hidden',
|
|
48
|
+
whiteSpace: 'nowrap',
|
|
49
|
+
textOverflow: rolling ? 'clip' : 'ellipsis',
|
|
50
|
+
}, children: _jsx(motion.span, { ref: inner, css: { display: 'inline-block' }, ...(rolling
|
|
51
|
+
? {
|
|
52
|
+
animate: { x: [0, 0, -overflow, -overflow, 0] },
|
|
53
|
+
transition: {
|
|
54
|
+
duration: 2 * seconds + 2 * PAUSE,
|
|
55
|
+
times: [
|
|
56
|
+
0,
|
|
57
|
+
PAUSE / (2 * seconds + 2 * PAUSE),
|
|
58
|
+
(PAUSE + seconds) / (2 * seconds + 2 * PAUSE),
|
|
59
|
+
(2 * PAUSE + seconds) / (2 * seconds + 2 * PAUSE),
|
|
60
|
+
1,
|
|
61
|
+
],
|
|
62
|
+
ease: 'linear',
|
|
63
|
+
repeat: Infinity,
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
: {}), children: children }) }));
|
|
67
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Fades its content in as it appears, rising a few pixels, a little later for each one after the
|
|
4
|
+
* first (the first handful only, so a long list does not take long to arrive). It only moves what is
|
|
5
|
+
* drawn inside it, never the room it takes, so nothing around it shifts. Where the person asks their
|
|
6
|
+
* device for less motion it just appears.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Reveal({ index, children }: {
|
|
9
|
+
index?: number;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import { motion, useReducedMotion } from 'motion/react';
|
|
4
|
+
/** How much later each following item starts, in seconds, and how many are staggered at all. */
|
|
5
|
+
const STAGGER = 0.03;
|
|
6
|
+
const STAGGERED = 8;
|
|
7
|
+
/**
|
|
8
|
+
* Fades its content in as it appears, rising a few pixels, a little later for each one after the
|
|
9
|
+
* first (the first handful only, so a long list does not take long to arrive). It only moves what is
|
|
10
|
+
* drawn inside it, never the room it takes, so nothing around it shifts. Where the person asks their
|
|
11
|
+
* device for less motion it just appears.
|
|
12
|
+
*/
|
|
13
|
+
export function Reveal({ index = 0, children }) {
|
|
14
|
+
const still = useReducedMotion();
|
|
15
|
+
if (still) {
|
|
16
|
+
return _jsx(_Fragment, { children: children });
|
|
17
|
+
}
|
|
18
|
+
return (_jsx(motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.22, ease: 'easeOut', delay: Math.min(index, STAGGERED) * STAGGER }, children: children }));
|
|
19
|
+
}
|
|
@@ -2,18 +2,22 @@ export interface SeriesSample {
|
|
|
2
2
|
/** ISO 8601. */
|
|
3
3
|
timestamp: string;
|
|
4
4
|
value: number;
|
|
5
|
+
/** Extra lines the tooltip shows under the value, e.g. the sky and the wind at that hour. */
|
|
6
|
+
notes?: string[];
|
|
5
7
|
}
|
|
6
8
|
/** The window a chart covers; `custom` is a from/to pair the caller has already applied to the samples. */
|
|
7
9
|
export type SeriesRange = '1h' | '1d' | '1w' | '1m' | 'custom';
|
|
8
10
|
export declare function formatTick(range: SeriesRange, iso: string): string;
|
|
9
11
|
/** A bare sparkline, or — when `expanded` — the full chart with axes, grid and a tooltip. Shared by the power and sensor drawers. */
|
|
10
|
-
export declare function SeriesChart({ samples, range, expanded, unit, fitDomain, }: {
|
|
12
|
+
export declare function SeriesChart({ samples, range, expanded, unit, fitDomain, height, }: {
|
|
11
13
|
samples: SeriesSample[];
|
|
12
14
|
range: SeriesRange;
|
|
13
15
|
expanded: boolean;
|
|
14
16
|
unit: string;
|
|
15
17
|
/** Fit the y axis to the data instead of starting at 0 — right for temperature/humidity, wrong for power. */
|
|
16
18
|
fitDomain?: boolean;
|
|
19
|
+
/** The chart's height in px. Default 48 for the sparkline, 220 when expanded. */
|
|
20
|
+
height?: number;
|
|
17
21
|
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
18
22
|
export declare function RangeSwitcher<T extends string>({ range, options, onChange, }: {
|
|
19
23
|
range: T;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
2
|
-
/** @jsxImportSource @emotion/react */
|
|
3
2
|
import { Box, Flex, Typography, useColorByKey } from 'e-prim';
|
|
4
3
|
import { useTypographySize } from '../theme/use-typography.js';
|
|
5
4
|
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts';
|
|
@@ -13,13 +12,19 @@ export function formatTick(range, iso) {
|
|
|
13
12
|
}
|
|
14
13
|
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
15
14
|
}
|
|
15
|
+
/** The grid's dashed lines, but not one along the chart's bottom edge: that is the solid axis line. */
|
|
16
|
+
const dashedExceptBottom = (stroke) => ({ x1, x2, y1, offset, }) => y1 >= offset.top + offset.height - 1 ? (_jsx("g", {})) : (_jsx("line", { x1: x1, x2: x2, y1: y1, y2: y1, stroke: stroke, strokeOpacity: 0.3, strokeDasharray: "3 3" }));
|
|
16
17
|
/** A bare sparkline, or — when `expanded` — the full chart with axes, grid and a tooltip. Shared by the power and sensor drawers. */
|
|
17
|
-
export function SeriesChart({ samples, range, expanded, unit, fitDomain = false, }) {
|
|
18
|
+
export function SeriesChart({ samples, range, expanded, unit, fitDomain = false, height, }) {
|
|
18
19
|
const accent = useColorByKey('accent') ?? '';
|
|
19
20
|
const textMuted = useColorByKey('textMuted') ?? '';
|
|
20
21
|
const line = useColorByKey('line') ?? '';
|
|
21
22
|
const tickSize = useTypographySize('secondary');
|
|
22
|
-
return (_jsx(Box, { height: expanded ? 220 : 48, mt: 3, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(AreaChart, { data: samples, margin: { top: 4, right: 0, bottom: 0, left: 0 }, children: [expanded ? _jsx(CartesianGrid, { stroke: line, strokeDasharray: "3 3", vertical: false
|
|
23
|
+
return (_jsx(Box, { height: height ?? (expanded ? 220 : 48), mt: 3, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(AreaChart, { data: samples, margin: { top: 4, right: 0, bottom: 0, left: 0 }, children: [expanded ? (_jsx(CartesianGrid, { stroke: line, strokeOpacity: 0.3, strokeDasharray: "3 3", vertical: false,
|
|
24
|
+
// The chart's bottom edge is the solid axis line, so no dashed line goes under it.
|
|
25
|
+
horizontal: dashedExceptBottom(line) })) : null, expanded ? (_jsx(XAxis, { dataKey: "timestamp", tickFormatter: (value) => formatTick(range, value), tick: { fontSize: tickSize, fill: textMuted },
|
|
26
|
+
// The bottom of the chart is a solid line; the grid above it is dashed and lighter.
|
|
27
|
+
axisLine: { stroke: line, strokeOpacity: 0.5, strokeWidth: 0.5 }, tickLine: false, minTickGap: 24 })) : null, expanded ? (_jsx(YAxis, { width: 36, ...(fitDomain ? { domain: ['auto', 'auto'] } : {}), tick: { fontSize: tickSize, fill: textMuted }, axisLine: false, tickLine: false })) : null, _jsx(Tooltip, { content: _jsx(ChartTooltip, { unit: unit }), ...(expanded
|
|
23
28
|
? {}
|
|
24
29
|
: { allowEscapeViewBox: { x: false, y: true }, position: { y: -52 } }) }), _jsx(Area, { type: "monotone", dataKey: "value", stroke: accent, fill: accent, fillOpacity: 0.2, strokeWidth: 2, dot: false, isAnimationActive: false })] }) }) }));
|
|
25
30
|
}
|
|
@@ -42,5 +47,5 @@ function ChartTooltip({ active, payload, unit, }) {
|
|
|
42
47
|
day: 'numeric',
|
|
43
48
|
hour: '2-digit',
|
|
44
49
|
minute: '2-digit',
|
|
45
|
-
}) }), _jsxs(Typography, { as: "span", variant: "bodyStrong", children: [Math.round(sample.value * 10) / 10, " ", unit] })] }) }));
|
|
50
|
+
}) }), _jsxs(Typography, { as: "span", variant: "bodyStrong", children: [Math.round(sample.value * 10) / 10, " ", unit] }), sample.notes?.map((note) => (_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: note }, note)))] }) }));
|
|
46
51
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
import { type BoxProps } from 'e-prim';
|
|
3
|
+
/** A placeholder block for something that is on its way, in the size it will have, so what is around
|
|
4
|
+
* it does not move when the real thing arrives. It pulses gently, and holds still where the person
|
|
5
|
+
* asks their device for less motion. Decorative: say what is loading around it, with `aria-busy`. */
|
|
6
|
+
export declare function Skeleton({ width, height, radius, }: {
|
|
7
|
+
/** px, or any CSS length. */
|
|
8
|
+
width: number | string;
|
|
9
|
+
height: number | string;
|
|
10
|
+
radius?: BoxProps['radius'];
|
|
11
|
+
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import { Box } from 'e-prim';
|
|
4
|
+
import { motion, useReducedMotion } from 'motion/react';
|
|
5
|
+
/** A placeholder block for something that is on its way, in the size it will have, so what is around
|
|
6
|
+
* it does not move when the real thing arrives. It pulses gently, and holds still where the person
|
|
7
|
+
* asks their device for less motion. Decorative: say what is loading around it, with `aria-busy`. */
|
|
8
|
+
export function Skeleton({ width, height, radius = 'small', }) {
|
|
9
|
+
const still = useReducedMotion();
|
|
10
|
+
return (_jsx(Box, { "aria-hidden": "true", background: "surfaceRaised", radius: radius, css: { width, height, flex: 'none', overflow: 'hidden' }, children: _jsx(motion.span, { css: { display: 'block', width: '100%', height: '100%', background: 'currentColor' }, initial: { opacity: 0.1 }, ...(still
|
|
11
|
+
? {}
|
|
12
|
+
: {
|
|
13
|
+
animate: { opacity: [0.04, 0.14, 0.04] },
|
|
14
|
+
transition: { duration: 1.4, repeat: Infinity, ease: 'easeInOut' },
|
|
15
|
+
}) }) }));
|
|
16
|
+
}
|
package/dist/layout/tile.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
import type { EntityRef } from '@hashsome/core';
|
|
1
3
|
import { type ReactNode } from 'react';
|
|
2
4
|
import type { IconName } from '../icon-data.ts';
|
|
3
5
|
import { type EntityStatus } from '../status.ts';
|
|
@@ -44,9 +46,15 @@ export interface TileProps {
|
|
|
44
46
|
* type — whichever component has a way to fetch it just passes it through.
|
|
45
47
|
*/
|
|
46
48
|
history?: LogbookEntry[];
|
|
49
|
+
/**
|
|
50
|
+
* An entity whose recent activity to fetch from the backend when the drawer opens, for the same
|
|
51
|
+
* History section (Home Assistant's logbook). `history`, when given, is used instead. A backend
|
|
52
|
+
* that keeps none shows no section.
|
|
53
|
+
*/
|
|
54
|
+
logbook?: EntityRef;
|
|
47
55
|
}
|
|
48
56
|
/** Base pill used by all entity tiles. Tap to press; hold (with `detail` set) to open the drawer. */
|
|
49
|
-
export declare function Tile({ label, icon, secondary, status, active, feedback, onPress, fill, onFillChange, trailing, overlay, title, detail, kind, energy, history, }: TileProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
57
|
+
export declare function Tile({ label, icon, secondary, status, active, feedback, onPress, fill, onFillChange, trailing, overlay, title, detail, kind, energy, history, logbook, }: TileProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
50
58
|
export declare function IconButton({ icon, label, onClick, disabled, active, pressed, primary, size, glyph, color, feedback, }: {
|
|
51
59
|
icon: IconName;
|
|
52
60
|
label: string;
|