@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
package/dist/entities/top-bar.js
CHANGED
|
@@ -7,7 +7,12 @@ import { fallbackName } from '../entity-handle.js';
|
|
|
7
7
|
import { useConnectionStatus, useEntity, useEntityHandle, useIntegrationStatuses, } from '../hooks.js';
|
|
8
8
|
import { Icon } from '../icon.js';
|
|
9
9
|
import { statusLabels } from '../status.js';
|
|
10
|
+
import { DrawerTrigger } from '../layout/use-drawer.js';
|
|
11
|
+
import { useWeatherForecast } from '../use-weather-forecast.js';
|
|
10
12
|
import { SensorReadout } from './sensor-readout.js';
|
|
13
|
+
import { WeatherForecast } from './weather-forecast.js';
|
|
14
|
+
import { WeatherIcon } from './weather-icon.js';
|
|
15
|
+
import { CONDITION_LOOK } from './weather-look.js';
|
|
11
16
|
/** The colors presence avatars without a picture fall back to when `presenceColors` isn't given. */
|
|
12
17
|
const DEFAULT_PRESENCE_COLORS = ['#8B5CF6', '#34D399', '#60A5FA', '#F472B6', '#FBBF24', '#FB923C'];
|
|
13
18
|
/** Picks one of `colors` for `id`, the same one every time (a presence avatar's color). */
|
|
@@ -54,6 +59,8 @@ export function TopBar({ title, dashboards, scenes, weather, extra, presence, pe
|
|
|
54
59
|
return (_jsx(SceneDot, { entity: entity, ...(color ? { color } : {}), ...(icon ? { icon } : {}) }, entity));
|
|
55
60
|
}) })) : null] }), _jsxs(Flex, { align: "center", gap: 3, children: [showDate ? _jsx(DateChip, {}) : null, weather ? _jsx(WeatherChip, { entity: weather }) : null, extra, presence ? (_jsx(StatusChip, { children: _jsx(SensorReadout, { entity: presence, drawer: false }) })) : null, people?.length ? (_jsx(PresenceStack, { entities: people, ...(presenceColors ? { colors: presenceColors } : {}) })) : null, showClock ? _jsx(Clock, { format: clockFormat }) : null, showStatus ? _jsx(SystemStatus, {}) : null] })] }));
|
|
56
61
|
}
|
|
62
|
+
/** The height of the switcher's button, and of each entry in its list. */
|
|
63
|
+
const SWITCHER_HEIGHT = 40;
|
|
57
64
|
function DashboardSwitcher({ title, dashboards, }) {
|
|
58
65
|
const [open, setOpen] = useState(false);
|
|
59
66
|
const navigate = useNavigate();
|
|
@@ -63,14 +70,16 @@ function DashboardSwitcher({ title, dashboards, }) {
|
|
|
63
70
|
// The dashboard being shown: the one whose route the address is under, else the one the title names.
|
|
64
71
|
const current = dashboards.find((d) => pathname === `/${d.id}` || pathname.startsWith(`/${d.id}/`)) ??
|
|
65
72
|
dashboards.find((d) => d.title === title);
|
|
66
|
-
return (_jsxs(Flex, { ref: ref, position: "relative", children: [_jsxs(Flex, { as: "button", type: "button", onClick: () => setOpen((o) => !o), "aria-haspopup": "listbox", "aria-expanded": open, align: "center", justify: "center", gap: 2, cursor: "pointer", color: "text", background: "surface", border: true, height:
|
|
73
|
+
return (_jsxs(Flex, { ref: ref, position: "relative", children: [_jsxs(Flex, { as: "button", type: "button", onClick: () => setOpen((o) => !o), "aria-haspopup": "listbox", "aria-expanded": open, align: "center", justify: "center", gap: 2, cursor: "pointer", color: "text", background: "surface", border: true, height: SWITCHER_HEIGHT, px: 4, radius: "chrome", children: [current?.icon ? _jsx(Icon, { name: current.icon, size: 20 }) : null, _jsx(Typography, { as: "h1", variant: "title", children: title }), _jsx(Icon, { name: "lu:chevron-down", size: 20 })] }), open ? (_jsx(Flex, { as: "ul", role: "listbox", direction: "column", background: "surfaceRaised", radius: "row", shadow: "drawer", p: 1.5, gap: 0.5, position: "absolute", zIndex: "dropdown", mt: 2, minWidth: "100%", css: { top: '100%', left: 0, listStyle: 'none', whiteSpace: 'nowrap' }, children: dashboards.map((d) => {
|
|
67
74
|
const active = d.id === current?.id;
|
|
68
75
|
return (_jsx("li", { role: "none", children: _jsxs(Flex, { as: "button", type: "button", role: "option", "aria-selected": active,
|
|
69
76
|
// Already here: marked, and nothing to switch to.
|
|
70
77
|
disabled: active, onClick: () => {
|
|
71
78
|
setOpen(false);
|
|
72
79
|
navigate(`/${d.id}`);
|
|
73
|
-
}, align: "center", gap: 2, width: "100%",
|
|
80
|
+
}, align: "center", gap: 2, width: "100%",
|
|
81
|
+
// As tall as the button that opened the list, so an entry is as easy to hit on a touch screen.
|
|
82
|
+
height: SWITCHER_HEIGHT, radius: "small", px: 3, color: "text", background: active ? 'surface' : 'transparent', cursor: active ? 'default' : 'pointer', css: { textAlign: 'left' }, children: [d.icon ? _jsx(Icon, { name: d.icon, size: 16 }) : null, _jsx(Typography, { as: "span", variant: "body", grow: 1, children: d.title }), active ? _jsx(Icon, { name: "lu:check", size: 16 }) : null] }) }, d.id));
|
|
74
83
|
}) })) : null] }));
|
|
75
84
|
}
|
|
76
85
|
const HEALTH_COLORS = {
|
|
@@ -128,24 +137,6 @@ export function SystemStatus({ link: linkOverride, statuses: statusesOverride, d
|
|
|
128
137
|
function StatusChip({ children }) {
|
|
129
138
|
return (_jsx(Flex, { align: "center", gap: 2, background: "surface", border: true, radius: "full", px: 3.5, py: 2.5, children: children }));
|
|
130
139
|
}
|
|
131
|
-
/** What a condition looks like in the pill: an icon, and a color where the weather has one. */
|
|
132
|
-
const CONDITION_LOOK = {
|
|
133
|
-
sunny: { icon: 'lu:sun', color: '#FBBF24' },
|
|
134
|
-
'clear-night': { icon: 'lu:moon', color: '#A5B4FC' },
|
|
135
|
-
partlycloudy: { icon: 'lu:cloud-sun', color: '#FBBF24' },
|
|
136
|
-
cloudy: { icon: 'lu:cloud' },
|
|
137
|
-
fog: { icon: 'lu:cloud-fog' },
|
|
138
|
-
rainy: { icon: 'lu:cloud-rain', color: '#60A5FA' },
|
|
139
|
-
pouring: { icon: 'lu:cloud-rain-wind', color: '#60A5FA' },
|
|
140
|
-
snowy: { icon: 'lu:cloud-snow', color: '#BAE6FD' },
|
|
141
|
-
'snowy-rainy': { icon: 'lu:cloud-snow', color: '#93C5FD' },
|
|
142
|
-
hail: { icon: 'lu:cloud-hail', color: '#93C5FD' },
|
|
143
|
-
lightning: { icon: 'lu:cloud-lightning', color: '#FBBF24' },
|
|
144
|
-
'lightning-rainy': { icon: 'lu:cloud-lightning', color: '#FBBF24' },
|
|
145
|
-
windy: { icon: 'lu:wind' },
|
|
146
|
-
exceptional: { icon: 'lu:triangle-alert', color: '#F2554A' },
|
|
147
|
-
unknown: { icon: 'lu:cloud' },
|
|
148
|
-
};
|
|
149
140
|
/** Current weather, as a pill: an icon for the sky and a rounded whole-degree reading. A `weather`
|
|
150
141
|
* entity gives the real condition; a plain `sensor.*` (an outdoor temperature) gets a fixed sun,
|
|
151
142
|
* since a sensor says nothing about the sky. */
|
|
@@ -156,11 +147,23 @@ function WeatherPill({ entity }) {
|
|
|
156
147
|
const handle = useEntityHandle('weather', entity);
|
|
157
148
|
const { status } = handle;
|
|
158
149
|
const weather = handle.entity;
|
|
159
|
-
const
|
|
150
|
+
const condition = weather?.condition ?? 'unknown';
|
|
151
|
+
const look = CONDITION_LOOK[condition];
|
|
160
152
|
const ready = status === 'ready' && weather?.temperature !== undefined;
|
|
161
|
-
|
|
153
|
+
const forecastable = ready && (weather?.forecasts?.length ?? 0) > 0;
|
|
154
|
+
// Today's high and low sit under the reading, once the forecast has said what they are.
|
|
155
|
+
const daily = useWeatherForecast(weather?.forecasts?.includes('daily') ? entity : undefined, 'daily');
|
|
156
|
+
const today = daily.result?.points.find((point) => new Date(point.timestamp).toDateString() === new Date().toDateString()) ?? daily.result?.points[0];
|
|
157
|
+
const range = today?.temperature !== undefined && today.low !== undefined
|
|
158
|
+
? `${Math.round(today.temperature)}° / ${Math.round(today.low)}°`
|
|
159
|
+
: undefined;
|
|
160
|
+
const chip = (_jsxs(StatusChip, { children: [_jsx(Flex, { as: "span", children: _jsx(WeatherIcon, { condition: condition, size: 14 }) }), _jsx(Typography, { as: "span", variant: "label", children: ready
|
|
162
161
|
? `${Math.round(weather.temperature ?? 0)}°`
|
|
163
|
-
: statusLabels[status] })] }));
|
|
162
|
+
: statusLabels[status] }), ready && range ? (_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: range })) : null] }));
|
|
163
|
+
if (!forecastable) {
|
|
164
|
+
return chip;
|
|
165
|
+
}
|
|
166
|
+
return (_jsx(DrawerTrigger, { icon: look.icon, label: weather?.name ?? 'Weather', kind: "Weather", body: _jsx(WeatherForecast, { entity: entity }), children: (open) => (_jsx(PlainButton, { "aria-label": `${weather?.name ?? 'Weather'}: forecast`, onClick: open, children: chip })) }));
|
|
164
167
|
}
|
|
165
168
|
function SensorWeather({ entity }) {
|
|
166
169
|
const handle = useEntityHandle('sensor', entity);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
import type { EntityRef } from '@hashsome/core';
|
|
3
|
+
export interface WeatherForecastProps {
|
|
4
|
+
/** A weather entity, as a ref like `ha:weather.forecast_home`. The forecasts it offers (hourly, daily) are read through the runtime. */
|
|
5
|
+
entity: EntityRef;
|
|
6
|
+
/** 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. */
|
|
7
|
+
expanded?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** A weather entity's forecast: the weather now, the next 24 hours as a chart (hover it for everything
|
|
10
|
+
* about an hour) and the days ahead. The top bar's weather chip shows it in a drawer; a dashboard can
|
|
11
|
+
* put it in a page of its own, with `expanded` for the full-size layout. Wide, it adds the current
|
|
12
|
+
* readings and a card for each hour. A source with no forecast shows only the weather now. */
|
|
13
|
+
export declare function WeatherForecast({ entity, expanded: layout }: WeatherForecastProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
2
|
+
import { Box, Flex, Typography } from 'e-prim';
|
|
3
|
+
import { useEntityHandle } from '../hooks.js';
|
|
4
|
+
import { Icon } from '../icon.js';
|
|
5
|
+
import { useDetail } from '../layout/detail-provider.js';
|
|
6
|
+
import { SeriesChart } from '../layout/series-chart.js';
|
|
7
|
+
import { useWeatherForecast } from '../use-weather-forecast.js';
|
|
8
|
+
import { WeatherIcon } from './weather-icon.js';
|
|
9
|
+
import { compass, CONDITION_LABEL, uvLevel } from './weather-look.js';
|
|
10
|
+
/** How many hours the chart and the hourly cards cover. */
|
|
11
|
+
const HOURS = 24;
|
|
12
|
+
/** Hours between the labelled points under the chart in the narrow drawer. */
|
|
13
|
+
const EVERY = 4;
|
|
14
|
+
const degrees = (value) => value === undefined ? '–' : `${Math.round(value)}°`;
|
|
15
|
+
const tenths = (value) => Math.round(value * 10) / 10;
|
|
16
|
+
const sameDay = (a, b) => a.toDateString() === b.toDateString();
|
|
17
|
+
function dayName(iso, now) {
|
|
18
|
+
const date = new Date(iso);
|
|
19
|
+
if (sameDay(date, now)) {
|
|
20
|
+
return 'Today';
|
|
21
|
+
}
|
|
22
|
+
const tomorrow = new Date(now);
|
|
23
|
+
tomorrow.setDate(now.getDate() + 1);
|
|
24
|
+
return sameDay(date, tomorrow) ? 'Tomorrow' : date.toLocaleDateString([], { weekday: 'long' });
|
|
25
|
+
}
|
|
26
|
+
const hourName = (iso) => new Date(iso).toLocaleTimeString([], { hour: 'numeric' });
|
|
27
|
+
/** `15 km/h from WNW`, with what there is of it. */
|
|
28
|
+
function windText(speed, bearing, unit) {
|
|
29
|
+
return `${Math.round(speed)}${unit ? ` ${unit}` : ''}${bearing !== undefined ? ` from ${compass(bearing)}` : ''}`;
|
|
30
|
+
}
|
|
31
|
+
/** `60% · 0.4 mm`, for the rain a step has: a chance, an amount, or both. Nothing for a dry step. */
|
|
32
|
+
function rainText(point, unit) {
|
|
33
|
+
const parts = [
|
|
34
|
+
...(point.precipitationProbability ? [`${Math.round(point.precipitationProbability)}%`] : []),
|
|
35
|
+
...(point.precipitation ? [`${tenths(point.precipitation)} ${unit ?? 'mm'}`] : []),
|
|
36
|
+
];
|
|
37
|
+
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
38
|
+
}
|
|
39
|
+
/** Everything a step knows, a line each, for the chart's hover: the sky, the wind, the rain, the
|
|
40
|
+
* air, the sun and the pressure. What the source did not give is left out. */
|
|
41
|
+
function describeStep(point, result) {
|
|
42
|
+
const rain = rainText(point, result?.precipitationUnit);
|
|
43
|
+
const air = [
|
|
44
|
+
...(point.humidity !== undefined ? [`Humidity ${Math.round(point.humidity)}%`] : []),
|
|
45
|
+
...(point.cloudCoverage !== undefined
|
|
46
|
+
? [`Cloud cover ${Math.round(point.cloudCoverage)}%`]
|
|
47
|
+
: []),
|
|
48
|
+
];
|
|
49
|
+
const sun = [
|
|
50
|
+
...(point.uvIndex !== undefined
|
|
51
|
+
? [`UV ${tenths(point.uvIndex)} (${uvLevel(point.uvIndex)})`]
|
|
52
|
+
: []),
|
|
53
|
+
...(point.apparentTemperature !== undefined
|
|
54
|
+
? [`Feels like ${degrees(point.apparentTemperature)}`]
|
|
55
|
+
: []),
|
|
56
|
+
];
|
|
57
|
+
return [
|
|
58
|
+
CONDITION_LABEL[point.condition],
|
|
59
|
+
...(point.windSpeed !== undefined
|
|
60
|
+
? [
|
|
61
|
+
`Wind ${windText(point.windSpeed, point.windBearing, result?.windUnit)}${point.windGustSpeed !== undefined ? `, gusts ${Math.round(point.windGustSpeed)}` : ''}`,
|
|
62
|
+
]
|
|
63
|
+
: []),
|
|
64
|
+
...(rain ? [`Rain ${rain}`] : []),
|
|
65
|
+
...(air.length > 0 ? [air.join(' · ')] : []),
|
|
66
|
+
...(sun.length > 0 ? [sun.join(' · ')] : []),
|
|
67
|
+
...(point.pressure !== undefined
|
|
68
|
+
? [
|
|
69
|
+
`Pressure ${Math.round(point.pressure)}${result?.pressureUnit ? ` ${result.pressureUnit}` : ''}`,
|
|
70
|
+
]
|
|
71
|
+
: []),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
function ConditionIcon({ condition, size, }) {
|
|
75
|
+
return (_jsx(Flex, { as: "span", children: _jsx(WeatherIcon, { condition: condition, size: size }) }));
|
|
76
|
+
}
|
|
77
|
+
/** An arrow that points the way the wind blows (it comes from `bearing`, so it points the other way). */
|
|
78
|
+
function WindArrow({ bearing, size }) {
|
|
79
|
+
return (_jsx(Flex, { as: "span", css: { transform: `rotate(${bearing + 180}deg)` }, children: _jsx(Icon, { name: "lu:arrow-up", size: size }) }));
|
|
80
|
+
}
|
|
81
|
+
/** One small reading with an icon in front of it. */
|
|
82
|
+
function Metric({ icon, children }) {
|
|
83
|
+
return (_jsxs(Flex, { align: "center", gap: 1, color: "textMuted", children: [typeof icon === 'string' ? _jsx(Icon, { name: icon, size: 12 }) : icon, _jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", noWrap: true, children: children })] }));
|
|
84
|
+
}
|
|
85
|
+
/** The wind as a metric: an arrow the way it blows, and its speed. */
|
|
86
|
+
function WindMetric({ point, unit }) {
|
|
87
|
+
return point.windSpeed === undefined ? null : (_jsxs(Metric, { icon: point.windBearing !== undefined ? (_jsx(WindArrow, { bearing: point.windBearing, size: 12 })) : ('lu:wind'), children: [Math.round(point.windSpeed), unit ? ` ${unit}` : ''] }));
|
|
88
|
+
}
|
|
89
|
+
/** A tile with a label and a value, for the current readings of the expanded drawer. */
|
|
90
|
+
function Stat({ label, children }) {
|
|
91
|
+
return (_jsxs(Flex, { direction: "column", gap: 0.5, border: true, radius: "row", px: 3, py: 2.5, children: [_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: label }), _jsx(Typography, { as: "span", variant: "bodyStrong", children: children })] }));
|
|
92
|
+
}
|
|
93
|
+
/** The readings the weather has now, from the entity and, for what it lacks, the hour that is
|
|
94
|
+
* under way (a source may give UV or cloud cover only in its forecast). */
|
|
95
|
+
function currentReadings(weather, soon) {
|
|
96
|
+
return {
|
|
97
|
+
uv: weather?.uvIndex ?? soon?.uvIndex,
|
|
98
|
+
cloud: weather?.cloudCoverage ?? soon?.cloudCoverage,
|
|
99
|
+
feels: weather?.apparentTemperature ?? soon?.apparentTemperature,
|
|
100
|
+
pressure: weather?.pressure ?? soon?.pressure,
|
|
101
|
+
humidity: weather?.humidity ?? soon?.humidity,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** A weather entity's forecast: the weather now, the next 24 hours as a chart (hover it for everything
|
|
105
|
+
* about an hour) and the days ahead. The top bar's weather chip shows it in a drawer; a dashboard can
|
|
106
|
+
* put it in a page of its own, with `expanded` for the full-size layout. Wide, it adds the current
|
|
107
|
+
* readings and a card for each hour. A source with no forecast shows only the weather now. */
|
|
108
|
+
export function WeatherForecast({ entity, expanded: layout }) {
|
|
109
|
+
const handle = useEntityHandle('weather', entity);
|
|
110
|
+
const weather = handle.entity;
|
|
111
|
+
const hourly = useWeatherForecast(weather?.forecasts?.includes('hourly') ? entity : undefined, 'hourly');
|
|
112
|
+
const daily = useWeatherForecast(weather?.forecasts?.includes('daily') ? entity : undefined, 'daily');
|
|
113
|
+
const { detail } = useDetail();
|
|
114
|
+
const expanded = layout ?? detail?.expanded ?? false;
|
|
115
|
+
const now = new Date();
|
|
116
|
+
const hours = (hourly.result?.points ?? []).slice(0, HOURS);
|
|
117
|
+
const along = hours.filter((_, index) => index % EVERY === 0);
|
|
118
|
+
const reading = currentReadings(weather, hours[0]);
|
|
119
|
+
const windUnit = hourly.result?.windUnit ?? weather?.windUnit;
|
|
120
|
+
const days = daily.result?.points ?? [];
|
|
121
|
+
const lows = days.flatMap((day) => (day.low !== undefined ? [day.low] : []));
|
|
122
|
+
const highs = days.flatMap((day) => (day.temperature !== undefined ? [day.temperature] : []));
|
|
123
|
+
const coldest = Math.min(...lows);
|
|
124
|
+
const warmest = Math.max(...highs);
|
|
125
|
+
const span = warmest - coldest;
|
|
126
|
+
return (_jsxs(Flex, { direction: "column", gap: 5, children: [_jsxs(Flex, { align: "center", gap: 3, children: [_jsx(ConditionIcon, { condition: weather?.condition ?? 'unknown', size: expanded ? 56 : 40 }), _jsxs(Flex, { direction: "column", children: [_jsx(Typography, { as: "span", variant: "heading", children: degrees(weather?.temperature) }), _jsxs(Typography, { as: "span", variant: "secondary", color: "textMuted", children: [CONDITION_LABEL[weather?.condition ?? 'unknown'], reading.humidity !== undefined ? ` · ${Math.round(reading.humidity)}% humidity` : '', reading.uv !== undefined ? ` · UV ${tenths(reading.uv)} (${uvLevel(reading.uv)})` : ''] }), weather?.windSpeed !== undefined ? (_jsxs(Flex, { align: "center", gap: 1, color: "textMuted", children: [weather.windBearing !== undefined ? (_jsx(WindArrow, { bearing: weather.windBearing, size: 12 })) : null, _jsxs(Typography, { as: "span", variant: "secondary", color: "textMuted", children: ["Wind ", windText(weather.windSpeed, weather.windBearing, weather.windUnit)] })] })) : null] })] }), expanded ? (_jsxs(Box, { css: {
|
|
127
|
+
display: 'grid',
|
|
128
|
+
gap: 12,
|
|
129
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
|
|
130
|
+
}, children: [reading.feels !== undefined ? (_jsx(Stat, { label: "Feels like", children: degrees(reading.feels) })) : null, reading.humidity !== undefined ? (_jsxs(Stat, { label: "Humidity", children: [Math.round(reading.humidity), "%"] })) : null, weather?.windSpeed !== undefined ? (_jsx(Stat, { label: "Wind", children: windText(weather.windSpeed, weather.windBearing, weather.windUnit) })) : null, reading.uv !== undefined ? (_jsxs(Stat, { label: "UV index", children: [tenths(reading.uv), " \u00B7 ", uvLevel(reading.uv)] })) : null, reading.cloud !== undefined ? (_jsxs(Stat, { label: "Cloud cover", children: [Math.round(reading.cloud), "%"] })) : null, reading.pressure !== undefined ? (_jsxs(Stat, { label: "Pressure", children: [Math.round(reading.pressure), weather?.pressureUnit ? ` ${weather.pressureUnit}` : ''] })) : null] })) : null, hours.length > 0 ? (_jsxs(Flex, { direction: "column", gap: 1, children: [_jsx(Typography, { as: "h3", variant: "label", color: "textMuted", m: 0, children: "Next 24 hours" }), _jsx(SeriesChart, { samples: hours.flatMap((hour) => hour.temperature !== undefined
|
|
131
|
+
? [
|
|
132
|
+
{
|
|
133
|
+
timestamp: hour.timestamp,
|
|
134
|
+
value: hour.temperature,
|
|
135
|
+
notes: describeStep(hour, hourly.result),
|
|
136
|
+
},
|
|
137
|
+
]
|
|
138
|
+
: []), range: "1d", expanded: expanded, unit: hourly.result?.unit ?? '°', fitDomain: true, height: expanded ? 180 : 88 }), expanded ? (_jsx(Box, { mt: 2, children: _jsx(Flex, { css: { overflowX: 'auto', paddingBottom: 4 }, children: hours.map((hour) => {
|
|
139
|
+
const rain = rainText(hour, hourly.result?.precipitationUnit);
|
|
140
|
+
return (_jsxs(Flex, { direction: "column", align: "center", gap: 1.5, px: 2, py: 1, css: { flex: '1 0 84px' }, children: [_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: hourName(hour.timestamp) }), _jsx(ConditionIcon, { condition: hour.condition, size: 26 }), _jsx(Typography, { as: "span", variant: "bodyStrong", children: degrees(hour.temperature) }), _jsxs(Flex, { direction: "column", align: "center", gap: 0.5, children: [rain ? _jsx(Metric, { icon: "lu:umbrella", children: rain }) : null, _jsx(WindMetric, { point: hour, unit: windUnit }), hour.humidity !== undefined ? (_jsxs(Metric, { icon: "lu:droplets", children: [Math.round(hour.humidity), "%"] })) : null, hour.uvIndex !== undefined && hour.uvIndex > 0 ? (_jsxs(Metric, { icon: "lu:sun", children: ["UV ", tenths(hour.uvIndex)] })) : null] })] }, hour.timestamp));
|
|
141
|
+
}) }) })) : (_jsx(Flex, { justify: "space-between", mt: 1, children: along.map((hour) => (_jsxs(Flex, { direction: "column", align: "center", gap: 1, grow: 1, css: { flexBasis: 0 }, children: [_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: hourName(hour.timestamp) }), _jsx(ConditionIcon, { condition: hour.condition, size: 18 }), _jsx(Typography, { as: "span", variant: "label", children: degrees(hour.temperature) })] }, hour.timestamp))) }))] })) : null, days.length > 0 ? (_jsxs(Flex, { direction: "column", gap: 2, children: [_jsxs(Typography, { as: "h3", variant: "label", color: "textMuted", m: 0, children: [days.length, " days"] }), _jsx(Flex, { as: "ul", direction: "column", gap: 1, m: 0, p: 0, css: { listStyle: 'none' }, children: days.map((day) => {
|
|
142
|
+
const rain = rainText(day, daily.result?.precipitationUnit);
|
|
143
|
+
return (_jsxs(Flex, { as: "li", align: "center", gap: 3, py: 1.5, children: [_jsx(Typography, { as: "span", variant: "body", width: 104, noWrap: true, children: dayName(day.timestamp, now) }), _jsx(ConditionIcon, { condition: day.condition, size: 20 }), _jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", width: expanded ? 90 : 40, noWrap: true, children: rain ?? '' }), _jsx(Typography, { as: "span", variant: "label", color: "textMuted", width: 36, css: { textAlign: 'right' }, children: degrees(day.low) }), _jsx(Box, { grow: 1, height: 6, radius: "full", background: "surfaceRaised", position: "relative", children: span > 0 && day.low !== undefined && day.temperature !== undefined ? (_jsx(Box, { position: "absolute", height: 6, radius: "full", background: "accent", css: {
|
|
144
|
+
left: `${((day.low - coldest) / span) * 100}%`,
|
|
145
|
+
width: `${((day.temperature - day.low) / span) * 100}%`,
|
|
146
|
+
} })) : null }), _jsx(Typography, { as: "span", variant: "label", width: 36, children: degrees(day.temperature) }), expanded ? (_jsxs(Flex, { gap: 4, css: { flex: 'none' }, children: [_jsx(Box, { width: 84, children: _jsx(WindMetric, { point: day, unit: daily.result?.windUnit }) }), _jsx(Box, { width: 64, children: day.humidity !== undefined ? (_jsxs(Metric, { icon: "lu:droplets", children: [Math.round(day.humidity), "%"] })) : null }), _jsx(Box, { width: 72, children: day.uvIndex !== undefined ? (_jsxs(Metric, { icon: "lu:sun", children: ["UV ", tenths(day.uvIndex)] })) : null })] })) : null] }, day.timestamp));
|
|
147
|
+
}) })] })) : null] }));
|
|
148
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
import type { WeatherCondition } from '@hashsome/core';
|
|
3
|
+
/** The icon for a sky condition, in its own colors: the sun yellow, a cloud blue, the rain a deeper blue. */
|
|
4
|
+
export declare function WeatherIcon({ condition, size }: {
|
|
5
|
+
condition: WeatherCondition;
|
|
6
|
+
size?: number;
|
|
7
|
+
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
2
|
+
import { ICON_NODES } from '../icon-data.js';
|
|
3
|
+
import { Icon } from '../icon.js';
|
|
4
|
+
import { CONDITION_LOOK } from './weather-look.js';
|
|
5
|
+
/** The icon for a sky condition, in its own colors: the sun yellow, a cloud blue, the rain a deeper blue. */
|
|
6
|
+
export function WeatherIcon({ condition, size }) {
|
|
7
|
+
const look = CONDITION_LOOK[condition];
|
|
8
|
+
const parts = ICON_NODES[look.icon]?.length ?? 0;
|
|
9
|
+
const colors = Array.from({ length: parts }, (_, i) => look.parts?.[i] ?? look.color);
|
|
10
|
+
return _jsx(Icon, { name: look.icon, colors: colors, ...(size !== undefined ? { size } : {}) });
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WeatherCondition } from '@hashsome/core';
|
|
2
|
+
import type { IconName } from '../icon-data.ts';
|
|
3
|
+
/** What a condition looks like: an icon, and the colors it is drawn in. */
|
|
4
|
+
export interface ConditionLook {
|
|
5
|
+
icon: IconName;
|
|
6
|
+
/** The color of every part of the icon that `parts` does not name; the surrounding text color when left out. */
|
|
7
|
+
color?: string;
|
|
8
|
+
/** Colors by part of the icon (its `i`th shape), for one drawn in two colors: the cloud blue and the sun yellow. */
|
|
9
|
+
parts?: Record<number, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare const CONDITION_LOOK: Record<WeatherCondition, ConditionLook>;
|
|
12
|
+
/** A condition in words. */
|
|
13
|
+
export declare const CONDITION_LABEL: Record<WeatherCondition, string>;
|
|
14
|
+
/** The compass point for a bearing in degrees: `225` is `SW`. */
|
|
15
|
+
export declare function compass(degrees: number): string;
|
|
16
|
+
/** How strong a UV index is, in words. */
|
|
17
|
+
export declare function uvLevel(index: number): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const SUN = '#FBBF24';
|
|
2
|
+
const CLOUD = '#60A5FA';
|
|
3
|
+
const RAIN = '#3B82F6';
|
|
4
|
+
const SNOW = '#7DD3FC';
|
|
5
|
+
export const CONDITION_LOOK = {
|
|
6
|
+
sunny: { icon: 'lu:sun', color: SUN },
|
|
7
|
+
'clear-night': { icon: 'lu:moon', color: '#A5B4FC' },
|
|
8
|
+
// The cloud is the last shape of `cloud-sun`; the rest is the sun.
|
|
9
|
+
partlycloudy: { icon: 'lu:cloud-sun', color: SUN, parts: { 5: CLOUD } },
|
|
10
|
+
cloudy: { icon: 'lu:cloud', color: CLOUD },
|
|
11
|
+
fog: { icon: 'lu:cloud-fog', parts: { 0: CLOUD } },
|
|
12
|
+
rainy: { icon: 'lu:cloud-rain', color: RAIN, parts: { 0: CLOUD } },
|
|
13
|
+
pouring: { icon: 'lu:cloud-rain-wind', color: RAIN, parts: { 0: CLOUD } },
|
|
14
|
+
snowy: { icon: 'lu:cloud-snow', color: SNOW, parts: { 0: CLOUD } },
|
|
15
|
+
'snowy-rainy': { icon: 'lu:cloud-snow', color: RAIN, parts: { 0: CLOUD } },
|
|
16
|
+
hail: { icon: 'lu:cloud-hail', color: SNOW, parts: { 0: CLOUD } },
|
|
17
|
+
lightning: { icon: 'lu:cloud-lightning', color: SUN, parts: { 0: CLOUD } },
|
|
18
|
+
'lightning-rainy': { icon: 'lu:cloud-lightning', color: SUN, parts: { 0: CLOUD } },
|
|
19
|
+
windy: { icon: 'lu:wind' },
|
|
20
|
+
exceptional: { icon: 'lu:triangle-alert', color: '#F2554A' },
|
|
21
|
+
unknown: { icon: 'lu:cloud' },
|
|
22
|
+
};
|
|
23
|
+
/** A condition in words. */
|
|
24
|
+
export const CONDITION_LABEL = {
|
|
25
|
+
sunny: 'Sunny',
|
|
26
|
+
'clear-night': 'Clear',
|
|
27
|
+
partlycloudy: 'Partly cloudy',
|
|
28
|
+
cloudy: 'Cloudy',
|
|
29
|
+
fog: 'Fog',
|
|
30
|
+
rainy: 'Rain',
|
|
31
|
+
pouring: 'Heavy rain',
|
|
32
|
+
snowy: 'Snow',
|
|
33
|
+
'snowy-rainy': 'Sleet',
|
|
34
|
+
hail: 'Hail',
|
|
35
|
+
lightning: 'Thunderstorm',
|
|
36
|
+
'lightning-rainy': 'Thunderstorm with rain',
|
|
37
|
+
windy: 'Windy',
|
|
38
|
+
exceptional: 'Exceptional weather',
|
|
39
|
+
unknown: 'Unknown',
|
|
40
|
+
};
|
|
41
|
+
const COMPASS = [
|
|
42
|
+
'N',
|
|
43
|
+
'NNE',
|
|
44
|
+
'NE',
|
|
45
|
+
'ENE',
|
|
46
|
+
'E',
|
|
47
|
+
'ESE',
|
|
48
|
+
'SE',
|
|
49
|
+
'SSE',
|
|
50
|
+
'S',
|
|
51
|
+
'SSW',
|
|
52
|
+
'SW',
|
|
53
|
+
'WSW',
|
|
54
|
+
'W',
|
|
55
|
+
'WNW',
|
|
56
|
+
'NW',
|
|
57
|
+
'NNW',
|
|
58
|
+
];
|
|
59
|
+
/** The compass point for a bearing in degrees: `225` is `SW`. */
|
|
60
|
+
export function compass(degrees) {
|
|
61
|
+
return COMPASS[Math.round((((degrees % 360) + 360) % 360) / 22.5) % 16];
|
|
62
|
+
}
|
|
63
|
+
/** How strong a UV index is, in words. */
|
|
64
|
+
export function uvLevel(index) {
|
|
65
|
+
if (index < 3) {
|
|
66
|
+
return 'low';
|
|
67
|
+
}
|
|
68
|
+
if (index < 6) {
|
|
69
|
+
return 'moderate';
|
|
70
|
+
}
|
|
71
|
+
if (index < 8) {
|
|
72
|
+
return 'high';
|
|
73
|
+
}
|
|
74
|
+
return index < 11 ? 'very high' : 'extreme';
|
|
75
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { LocalClient, MockIntegration } from '@hashsome/core';
|
|
1
|
+
import { LocalClient, MockIntegration, type BrowseQuery, type BrowseResult } from '@hashsome/core';
|
|
2
|
+
/** The gallery's backend: the mock, with one player whose shelves never finish loading, to show the
|
|
3
|
+
* loading state of the library. */
|
|
4
|
+
declare class GalleryIntegration extends MockIntegration {
|
|
5
|
+
browse(entityId: string, query: BrowseQuery): Promise<BrowseResult>;
|
|
6
|
+
}
|
|
2
7
|
/** Mock entities covering every state the components handle. Refs are `ha:<id>`. */
|
|
3
|
-
export declare function createGalleryIntegration():
|
|
8
|
+
export declare function createGalleryIntegration(): GalleryIntegration;
|
|
4
9
|
export declare function createGalleryClient(): LocalClient;
|
|
10
|
+
export {};
|
package/dist/gallery/fixtures.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { LocalClient, mockAction, mockClimate, mockLibrary, mockLight, mockMediaPlayer, mockPerson, mockSensor, MockIntegration, } from '@hashsome/core';
|
|
1
|
+
import { LocalClient, mockAction, mockClimate, mockLibrary, mockLight, mockMediaPlayer, mockPerson, mockSensor, mockWeather, MockIntegration, } from '@hashsome/core';
|
|
2
2
|
/** Album art for the gallery's playing demo, served by the docs dev server from `docs/public/`. */
|
|
3
3
|
// Under the base path the site is served from (`/hash/` on GitHub Pages), not the domain's root.
|
|
4
4
|
const DEMO_ARTWORK = `${import.meta.env.BASE_URL}artwork.jpg`;
|
|
5
5
|
const power = (watts) => mockSensor({ name: 'Power', value: watts, unit: 'W', measurement: 'power' });
|
|
6
6
|
const energy = (kwh) => mockSensor({ name: 'Energy', value: kwh, unit: 'kWh', measurement: 'energy' });
|
|
7
|
+
/** The gallery's backend: the mock, with one player whose shelves never finish loading, to show the
|
|
8
|
+
* loading state of the library. */
|
|
9
|
+
class GalleryIntegration extends MockIntegration {
|
|
10
|
+
browse(entityId, query) {
|
|
11
|
+
return entityId === 'media_player.loading' && query.path !== undefined
|
|
12
|
+
? new Promise(() => { })
|
|
13
|
+
: super.browse(entityId, query);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
7
16
|
/** Mock entities covering every state the components handle. Refs are `ha:<id>`. */
|
|
8
17
|
export function createGalleryIntegration() {
|
|
9
|
-
return new
|
|
18
|
+
return new GalleryIntegration({
|
|
10
19
|
library: mockLibrary(),
|
|
11
20
|
entities: {
|
|
12
21
|
'light.plain_on': mockLight({ name: 'Lamp', on: true }),
|
|
@@ -62,6 +71,12 @@ export function createGalleryIntegration() {
|
|
|
62
71
|
'sensor.lamp_energy': energy('1.64'),
|
|
63
72
|
'sensor.led_power': power('7'),
|
|
64
73
|
'sensor.led_energy': energy('3.2'),
|
|
74
|
+
'weather.home': mockWeather({
|
|
75
|
+
name: 'Home',
|
|
76
|
+
condition: 'partlycloudy',
|
|
77
|
+
temperature: 12.3,
|
|
78
|
+
humidity: 71,
|
|
79
|
+
}),
|
|
65
80
|
'sensor.temperature': mockSensor({ value: '18.04', unit: '°C', measurement: 'temperature' }),
|
|
66
81
|
'sensor.humidity': mockSensor({ value: '64.41', unit: '%', measurement: 'humidity' }),
|
|
67
82
|
'sensor.bedroom_humidity': mockSensor({
|
|
@@ -102,7 +117,11 @@ export function createGalleryIntegration() {
|
|
|
102
117
|
duration: 231,
|
|
103
118
|
positionUpdatedAt: new Date().toISOString(),
|
|
104
119
|
shuffle: false,
|
|
105
|
-
capabilities: { browse: true, search: true, seek: true, shuffle: true },
|
|
120
|
+
capabilities: { browse: true, search: true, seek: true, shuffle: true, queue: true },
|
|
121
|
+
}),
|
|
122
|
+
'media_player.loading': mockMediaPlayer({
|
|
123
|
+
name: 'Slow library',
|
|
124
|
+
capabilities: { browse: true, search: true },
|
|
106
125
|
}),
|
|
107
126
|
'media_player.off': mockMediaPlayer({
|
|
108
127
|
name: 'Kitchen Speaker',
|
package/dist/gallery/gallery.js
CHANGED
|
@@ -9,7 +9,9 @@ import { LightTile } from '../entities/light-tile.js';
|
|
|
9
9
|
import { MediaBrowser } from '../entities/media-browser.js';
|
|
10
10
|
import { MediaPlayerBar } from '../entities/media-player-bar.js';
|
|
11
11
|
import { MediaPlayerColumn } from '../entities/media-player-column.js';
|
|
12
|
-
import {
|
|
12
|
+
import { MediaPlayerFull } from '../entities/media-player-full.js';
|
|
13
|
+
import { MediaQueue } from '../entities/media-queue.js';
|
|
14
|
+
import { WeatherForecast } from '../entities/weather-forecast.js';
|
|
13
15
|
import { NavDock } from '../entities/nav-dock.js';
|
|
14
16
|
import { NavRail } from '../entities/nav-rail.js';
|
|
15
17
|
import { Page } from '../layout/page.js';
|
|
@@ -80,8 +82,8 @@ function TileFillDemo() {
|
|
|
80
82
|
function SubPreview({ label, height, children, }) {
|
|
81
83
|
return (_jsxs(Flex, { direction: "column", gap: 2, children: [_jsx(Typography, { as: "span", variant: "eyebrow", uppercase: true, color: "textMuted", children: label }), _jsx(Flex, { position: "relative", overflow: "hidden", radius: "row", border: true, height: height, css: { transform: 'translateZ(0)' }, children: children })] }));
|
|
82
84
|
}
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
+
// Hand-written activity, to show the drawer's History section through the `history` prop. Tiles
|
|
86
|
+
// without it (and with a backend that keeps a record) fetch their own.
|
|
85
87
|
const lampHistory = [
|
|
86
88
|
{
|
|
87
89
|
id: '1',
|
|
@@ -347,9 +349,9 @@ export function Gallery({ density = 'comfortable' }) {
|
|
|
347
349
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',
|
|
348
350
|
transform: mode === 'dark' ? 'translate(28px, -50%)' : 'translateY(-50%)',
|
|
349
351
|
transition: 'transform 0.2s ease',
|
|
350
|
-
}) }), _jsx(Icon, { name: "lu:sun", size: 14 }), _jsx(Icon, { name: "lu:moon", size: 14 })] })] }), _jsx(ComponentDoc, { title: "Grid", components: ['Grid'], description: "Lays tiles out in equal-width columns (`columns`, default 2). Cells share the width and never overflow it.", children: _jsxs(Flex, { direction: "column", gap: 5, children: [_jsxs(Grid, { columns: 2, children: [_jsx(Tile, { label: "Two", icon: "lu:layout-grid", secondary: "columns={2}" }), _jsx(Tile, { label: "Columns", icon: "lu:layout-grid", secondary: "equal width" })] }), _jsxs(Grid, { columns: 3, children: [_jsx(Tile, { label: "Three", icon: "lu:layout-grid", secondary: "columns={3}" }), _jsx(Tile, { label: "Columns", icon: "lu:layout-grid", secondary: "equal width" }), _jsx(Tile, { label: "Wide", icon: "lu:layout-grid", secondary: "fits the row" })] })] }) }), _jsx(ComponentDoc, { title: "Room Header", components: ['RoomHeader'], description: "The header that starts a room: a muted icon and title, a hairline and readouts on the right, placed above a Grid (or any tiles). It adds its own space above, so consecutive rooms read as groups.", children: _jsxs(Flex, { direction: "column", gap: 4, children: [_jsx(RoomHeader, { title: "Kitchen", icon: "lu:utensils-crossed" }), _jsx(RoomHeader, { title: "Porch", icon: "lu:house", readouts: _jsxs(_Fragment, { children: [_jsx(SensorReadout, { entity: "ha:sensor.temperature" }), _jsx(SensorReadout, { entity: "ha:sensor.humidity" })] }) })] }) }), _jsx(ComponentDoc, { title: "Tile", components: ['Tile'], description: "The base card every entity tile is built from: icon, label, status line, optional fill bar (drag to change it), trailing controls, a hold-to-open drawer and pending / done / error feedback. Use it directly for something that isn't an entity.", children: _jsxs(Grid, { children: [_jsx(Tile, { label: "Plain", icon: "lu:lamp", secondary: "Off" }), _jsx(Tile, { label: "Active", icon: "lu:lamp", active: true }), _jsx(TileFillDemo, {}), _jsx(Tile, { label: "With controls", icon: "lu:fan", trailing: _jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: "trailing" }) }), _jsx(Tile, { label: "Pending", icon: "lu:lamp", feedback: "pending" }), _jsx(Tile, { label: "Error", icon: "lu:lamp", feedback: "error" }), _jsx(Tile, { label: "Unavailable", icon: "lu:lamp", status: "unavailable" }), _jsx(Tile, { label: "Not found", icon: "lu:lamp", status: "missing" })] }) }), _jsx(ComponentDoc, { title: "Icon", components: ['Icon'], description: "A plain prefixed string, no import: `lu:` is Lucide, `tb:` is Tabler outline. It takes the surrounding text color and scales with `size` (default 24px).", children: _jsxs(Flex, { direction: "column", gap: 4, children: [_jsxs(Flex, { align: "center", gap: 4, children: [_jsx(Icon, { name: "lu:lightbulb" }), _jsx(Icon, { name: "lu:thermometer", size: 16 }), _jsx(Icon, { name: "lu:sofa", size: 32 }), _jsx(Icon, { name: "tb:vacuum-cleaner" }), _jsx(Icon, { name: "tb:car-fan", size: 16 })] }), _jsxs(Flex, { align: "center", gap: 4, css: { flexWrap: 'wrap' }, children: [_jsx(Typography, { as: "span", variant: "body", color: "textMuted", children: "Find an icon:" }), ICON_PACKS.map((pack) => (_jsx(Typography, { as: "span", variant: "body", children: _jsxs("a", { css: ({ palette }) => ({ color: palette.accent, textUnderlineOffset: 3 }), href: pack.href, target: "_blank", rel: "noreferrer", children: ["Search ", pack.name, " icons (", _jsxs("span", { css: codeStyles, children: [pack.prefix, ":"] }), ")"] }) }, pack.prefix)))] })] }) }), _jsx(ComponentDoc, { title: "Energy Chart", components: ['EnergyChart'], description: "A power section for a drawer, which tiles pass through `energy`: a live power reading, usage tiles and a sparkline. Expanded in a drawer it becomes the full chart with a range switcher.", children: _jsx(EnergyChart, { power: "ha:sensor.heater_power", energy: "ha:sensor.heater_energy", usage: heaterUsage, seriesByRange: heaterSeries }) }), _jsx(ComponentDoc, { title: "History Section", components: ['HistorySection'], description: "Recent activity for a drawer, which tiles pass through `history`: who or what changed the entity, and when. An empty list renders nothing.", children: _jsx(HistorySection, { entries: lampHistory }) }), _jsx(ComponentDoc, { title: "Light Tile", components: ['LightTile'], description: "Tap to toggle, hold to open its detail drawer. Dimmable lights drag to set brightness; color-capable lights add a palette button that opens swatches in the card (white temperatures by default, overridable with `colors`), and the card's icon shows the light's current color. The drawer has the full temperature and hue bars.", children: _jsxs(Grid, { children: [_jsx(LightTile, { entity: "ha:light.plain_on", energy: lampEnergy, history: lampHistory }), _jsx(LightTile, { entity: "ha:light.plain_off", history: stairsHistory }), _jsx(LightTile, { entity: "ha:light.dimmable", icon: "lu:sparkles", energy: ledEnergy, history: ledHistory }), _jsx(LightTile, { entity: "ha:light.color", icon: "lu:lamp-desk", history: nightLampHistory }), _jsx(LightTile, { entity: "ha:light.cct", icon: "lu:lamp-desk" }), _jsx(LightTile, { entity: "ha:light.unavailable" }), _jsx(LightTile, { entity: "ha:light.missing" })] }) }), _jsx(ComponentDoc, { title: "Climate Tile", components: ['ClimateTile'], description: "A heater/thermostat tile: a mode button that opens mode swatches in the card (like light colors), plus a target-temperature stepper. Hold for the drawer: modes, a target bar with fine +/- (hold to repeat), presets, energy and history.", children: _jsxs(Grid, { columns: 1, children: [_jsx(ClimateTile, { entity: "ha:climate.heater", energy: heaterEnergy, history: heaterHistory }), _jsx(ClimateTile, { entity: "ha:climate.off", energy: thermostatEnergy, history: heaterHistory }), _jsx(ClimateTile, { entity: "ha:climate.unavailable" })] }) }), _jsx(ComponentDoc, { title: "Sensor Readout", components: ['SensorReadout'], description: "An inline value with icon and unit, defaulting its icon by device class. Readings outside a safe range get a warning (humidity defaults to 30\u201360 %; override with `safeRange`). Pass `history` to make it tappable: the drawer charts it with min/max for today, this week and this month, and a custom date and time range when expanded.", children: _jsxs(Grid, { columns: 3, children: [_jsx(SensorReadout, { entity: "ha:sensor.temperature", history: temperatureHistory }), _jsx(SensorReadout, { entity: "ha:sensor.humidity", history: humidityHistory }), _jsx(SensorReadout, { entity: "ha:sensor.bedroom_humidity", history: bedroomHumidityHistory }), _jsx(SensorReadout, { entity: "ha:sensor.office_humidity", history: officeHumidityHistory }), _jsx(SensorReadout, { entity: "ha:sensor.unavailable" }), _jsx(SensorReadout, { entity: "ha:sensor.unknown" })] }) }), _jsx(ComponentDoc, { title: "Scene Button & Action Button", components: ['SceneButton', 'ActionButton'], description: "A scene is just an action entity. An action button triggers an action (or flips a switch) and shows pending / done / error feedback after a press; `onPress` runs your own logic instead.", children: _jsxs(Grid, { columns: 3, children: [_jsx(SceneButton, { entity: "ha:scene.tv_time" }), _jsx(ActionButton, { label: "Shower", icon: "tb:bath", entity: "ha:script.shower" }), _jsx(ActionButton, { label: "Always Fails", onPress: () => Promise.reject(new Error('That went wrong')) })] }) }), _jsx(ComponentDoc, { title: "Media Player Bar", components: ['MediaPlayerBar'], description: "Track info (with a small shuffle toggle by the title) and the time beside it, then volume, browse, previous, play/pause and next (the artwork opens the browser too), with a thin progress line along the bottom while something plays. The browse button opens the player's own library in the drawer; pass `browse` content to replace it, or `false` for no button. The volume button opens a volume slider in the bar (with a mute swatch, like the light colors and climate modes). Shows album art, or a note glyph when there is none.", children: _jsxs(Flex, { direction: "column", gap: 3, children: [_jsx(MediaPlayerBar, { entity: "ha:media_player.living_room" }), _jsx(MediaPlayerBar, { entity: "ha:media_player.off" })] }) }), _jsx(ComponentDoc, { title: "Media Player Column", components: ['MediaPlayerColumn'], description: "The player as an upright card for a narrow column beside a dashboard: the artwork in a ring that shows how far along playback is (drag around it to seek), title, transport, and a volume bar that is always visible. The browse button opens the library in the drawer.", children: _jsx(Flex, { width: 320, children: _jsx(MediaPlayerColumn, { entity: "ha:media_player.living_room" }) }) }), _jsx(ComponentDoc, { title: "Media Player Page", components: ['MediaPlayerPage'], description: "The player as a whole page: the player centered, and its library below it, so songs are picked right there instead of in a drawer. The expanded drawer is this same layout.", children: _jsx(Flex, { direction: "column", children: _jsx(MediaPlayerPage, { entity: "ha:media_player.living_room" }) }) }), _jsx(ComponentDoc, { title: "Media Browser", components: ['MediaBrowser'], description: "A player's own library in the app's style: tap a folder to open it, a track or station to play it, or the play button on an album, playlist or artist for all of it. The search box appears when the library can be searched. The `theater` layout (second demo) is one row of large cards that scrolls sideways, for a wide space; `list` is the compact default.", children: _jsxs(Flex, { direction: "column", gap: 6, children: [_jsx(Flex, { direction: "column", maxHeight: 420, overflow: "auto", children: _jsx(MediaBrowser, { entity: "ha:media_player.living_room" }) }), _jsx(MediaBrowser, { entity: "ha:media_player.living_room", layout: "theater" })] }) }), _jsx(ComponentDoc, { title: "Navigation", components: ['NavRail', 'NavDock', 'Page'], description: "Routes within one dashboard. A dashboard includes one itself, usually in its layout route: NavRail is fixed to the left and NavDock floats at the bottom, and both reserve the space they take in the Page around them, which pads so content never sits under them.", children: _jsxs(Flex, { direction: "column", gap: 5, children: [_jsx(SubPreview, { label: "NavRail, in a Page", height: 240, children: _jsxs(Page, { height: "100%", children: [_jsx(NavRail, { base: "/gallery", items: NAV_ITEMS }), _jsx(Typography, { as: "p", variant: "body", color: "textMuted", children: "Page content, padded clear of the rail." })] }) }), _jsx(SubPreview, { label: "NavDock", height: 140, children: _jsx(NavDock, { base: "/gallery", items: NAV_ITEMS }) })] }) }), _jsx(ComponentDoc, { title: "Top Bar", components: ['TopBar'], description: "Page chrome above a dashboard's content: a title (a dropdown to switch dashboards, once there's more than one to switch to), scene shortcuts, weather, presence, and a clock.", children: _jsx(TopBar, { title: "Living Room", dashboards: [
|
|
352
|
+
}) }), _jsx(Icon, { name: "lu:sun", size: 14 }), _jsx(Icon, { name: "lu:moon", size: 14 })] })] }), _jsx(ComponentDoc, { title: "Grid", components: ['Grid'], description: "Lays tiles out in equal-width columns (`columns`, default 2). Cells share the width and never overflow it.", children: _jsxs(Flex, { direction: "column", gap: 5, children: [_jsxs(Grid, { columns: 2, children: [_jsx(Tile, { label: "Two", icon: "lu:layout-grid", secondary: "columns={2}" }), _jsx(Tile, { label: "Columns", icon: "lu:layout-grid", secondary: "equal width" })] }), _jsxs(Grid, { columns: 3, children: [_jsx(Tile, { label: "Three", icon: "lu:layout-grid", secondary: "columns={3}" }), _jsx(Tile, { label: "Columns", icon: "lu:layout-grid", secondary: "equal width" }), _jsx(Tile, { label: "Wide", icon: "lu:layout-grid", secondary: "fits the row" })] })] }) }), _jsx(ComponentDoc, { title: "Room Header", components: ['RoomHeader'], description: "The header that starts a room: a muted icon and title, a hairline and readouts on the right, placed above a Grid (or any tiles). It adds its own space above, so consecutive rooms read as groups.", children: _jsxs(Flex, { direction: "column", gap: 4, children: [_jsx(RoomHeader, { title: "Kitchen", icon: "lu:utensils-crossed" }), _jsx(RoomHeader, { title: "Porch", icon: "lu:house", readouts: _jsxs(_Fragment, { children: [_jsx(SensorReadout, { entity: "ha:sensor.temperature" }), _jsx(SensorReadout, { entity: "ha:sensor.humidity" })] }) })] }) }), _jsx(ComponentDoc, { title: "Tile", components: ['Tile'], description: "The base card every entity tile is built from: icon, label, status line, optional fill bar (drag to change it), trailing controls, a hold-to-open drawer and pending / done / error feedback. Use it directly for something that isn't an entity.", children: _jsxs(Grid, { children: [_jsx(Tile, { label: "Plain", icon: "lu:lamp", secondary: "Off" }), _jsx(Tile, { label: "Active", icon: "lu:lamp", active: true }), _jsx(TileFillDemo, {}), _jsx(Tile, { label: "With controls", icon: "lu:fan", trailing: _jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: "trailing" }) }), _jsx(Tile, { label: "Pending", icon: "lu:lamp", feedback: "pending" }), _jsx(Tile, { label: "Error", icon: "lu:lamp", feedback: "error" }), _jsx(Tile, { label: "Unavailable", icon: "lu:lamp", status: "unavailable" }), _jsx(Tile, { label: "Not found", icon: "lu:lamp", status: "missing" })] }) }), _jsx(ComponentDoc, { title: "Icon", components: ['Icon'], description: "A plain prefixed string, no import: `lu:` is Lucide, `tb:` is Tabler outline. It takes the surrounding text color and scales with `size` (default 24px).", children: _jsxs(Flex, { direction: "column", gap: 4, children: [_jsxs(Flex, { align: "center", gap: 4, children: [_jsx(Icon, { name: "lu:lightbulb" }), _jsx(Icon, { name: "lu:thermometer", size: 16 }), _jsx(Icon, { name: "lu:sofa", size: 32 }), _jsx(Icon, { name: "tb:vacuum-cleaner" }), _jsx(Icon, { name: "tb:car-fan", size: 16 })] }), _jsxs(Flex, { align: "center", gap: 4, css: { flexWrap: 'wrap' }, children: [_jsx(Typography, { as: "span", variant: "body", color: "textMuted", children: "Find an icon:" }), ICON_PACKS.map((pack) => (_jsx(Typography, { as: "span", variant: "body", children: _jsxs("a", { css: ({ palette }) => ({ color: palette.accent, textUnderlineOffset: 3 }), href: pack.href, target: "_blank", rel: "noreferrer", children: ["Search ", pack.name, " icons (", _jsxs("span", { css: codeStyles, children: [pack.prefix, ":"] }), ")"] }) }, pack.prefix)))] })] }) }), _jsx(ComponentDoc, { title: "Energy Chart", components: ['EnergyChart'], description: "A power section for a drawer, which tiles pass through `energy`: a live power reading, usage tiles and a sparkline. Expanded in a drawer it becomes the full chart with a range switcher.", children: _jsx(EnergyChart, { power: "ha:sensor.heater_power", energy: "ha:sensor.heater_energy", usage: heaterUsage, seriesByRange: heaterSeries }) }), _jsx(ComponentDoc, { title: "History Section", components: ['HistorySection'], description: "Recent activity for a drawer, which tiles pass through `history`: who or what changed the entity, and when. An empty list renders nothing.", children: _jsx(HistorySection, { entries: lampHistory }) }), _jsx(ComponentDoc, { title: "Light Tile", components: ['LightTile'], description: "Tap to toggle, hold to open its detail drawer. Dimmable lights drag to set brightness; color-capable lights add a palette button that opens swatches in the card (white temperatures by default, overridable with `colors`), and the card's icon shows the light's current color. The drawer has the full temperature and hue bars.", children: _jsxs(Grid, { children: [_jsx(LightTile, { entity: "ha:light.plain_on", energy: lampEnergy, history: lampHistory }), _jsx(LightTile, { entity: "ha:light.plain_off", history: stairsHistory }), _jsx(LightTile, { entity: "ha:light.dimmable", icon: "lu:sparkles", energy: ledEnergy, history: ledHistory }), _jsx(LightTile, { entity: "ha:light.color", icon: "lu:lamp-desk", history: nightLampHistory }), _jsx(LightTile, { entity: "ha:light.cct", icon: "lu:lamp-desk" }), _jsx(LightTile, { entity: "ha:light.unavailable" }), _jsx(LightTile, { entity: "ha:light.missing" })] }) }), _jsx(ComponentDoc, { title: "Climate Tile", components: ['ClimateTile'], description: "A heater/thermostat tile: a mode button that opens mode swatches in the card (like light colors), plus a target-temperature stepper. Hold for the drawer: modes, a target bar with fine +/- (hold to repeat), presets, energy and history.", children: _jsxs(Grid, { columns: 1, children: [_jsx(ClimateTile, { entity: "ha:climate.heater", energy: heaterEnergy, history: heaterHistory }), _jsx(ClimateTile, { entity: "ha:climate.off", energy: thermostatEnergy, history: heaterHistory }), _jsx(ClimateTile, { entity: "ha:climate.unavailable" })] }) }), _jsx(ComponentDoc, { title: "Sensor Readout", components: ['SensorReadout'], description: "An inline value with icon and unit, defaulting its icon by device class. Readings outside a safe range get a warning (humidity defaults to 30\u201360 %; override with `safeRange`). Pass `history` to make it tappable: the drawer charts it with min/max for today, this week and this month, and a custom date and time range when expanded.", children: _jsxs(Grid, { columns: 3, children: [_jsx(SensorReadout, { entity: "ha:sensor.temperature", history: temperatureHistory }), _jsx(SensorReadout, { entity: "ha:sensor.humidity", history: humidityHistory }), _jsx(SensorReadout, { entity: "ha:sensor.bedroom_humidity", history: bedroomHumidityHistory }), _jsx(SensorReadout, { entity: "ha:sensor.office_humidity", history: officeHumidityHistory }), _jsx(SensorReadout, { entity: "ha:sensor.unavailable" }), _jsx(SensorReadout, { entity: "ha:sensor.unknown" })] }) }), _jsx(ComponentDoc, { title: "Scene Button & Action Button", components: ['SceneButton', 'ActionButton'], description: "A scene is just an action entity. An action button triggers an action (or flips a switch) and shows pending / done / error feedback after a press; `onPress` runs your own logic instead.", children: _jsxs(Grid, { columns: 3, children: [_jsx(SceneButton, { entity: "ha:scene.tv_time" }), _jsx(ActionButton, { label: "Shower", icon: "tb:bath", entity: "ha:script.shower" }), _jsx(ActionButton, { label: "Always Fails", onPress: () => Promise.reject(new Error('That went wrong')) })] }) }), _jsx(ComponentDoc, { title: "Media Player Bar", components: ['MediaPlayerBar'], description: "Track info (with a small shuffle toggle by the title) and the time beside it, then volume, browse, previous, play/pause and next (the artwork opens the browser too), with a thin progress line along the bottom while something plays. The browse button opens the player's own library in the drawer; pass `browse` content to replace it, or `false` for no button. The volume button opens a volume slider in the bar (with a mute swatch, like the light colors and climate modes). Shows album art, or a note glyph when there is none.", children: _jsxs(Flex, { direction: "column", gap: 3, children: [_jsx(MediaPlayerBar, { entity: "ha:media_player.living_room" }), _jsx(MediaPlayerBar, { entity: "ha:media_player.off" })] }) }), _jsx(ComponentDoc, { title: "Media Player Column", components: ['MediaPlayerColumn'], description: "The player as an upright card for a narrow column beside a dashboard: the artwork in a ring that shows how far along playback is (drag around it to seek), title, transport, and a volume bar that is always visible. The browse button opens the library in the drawer.", children: _jsx(Flex, { width: 320, children: _jsx(MediaPlayerColumn, { entity: "ha:media_player.living_room" }) }) }), _jsx(ComponentDoc, { title: "Media Player Full", components: ['MediaPlayerFull', 'MediaQueue'], description: "The big player: the player centered with its queue beside it (`MediaQueue`: tap a track to jump to it, the cross takes it out, Clear empties it), and its library below, where every track can be played, played next or added to the queue, and an open album or playlist can be played, shuffled or added whole. The media cards' drawers show the player and library too. A dashboard puts it in a page of its own, around it whatever it likes.", children: _jsx(Flex, { direction: "column", children: _jsx(MediaPlayerFull, { entity: "ha:media_player.living_room", wide: true, queue: _jsx(MediaQueue, { entity: "ha:media_player.living_room" }), browser: _jsx(MediaBrowser, { entity: "ha:media_player.living_room", layout: "theater" }) }) }) }), _jsx(ComponentDoc, { title: "Weather Forecast", components: ['WeatherForecast'], description: "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. Narrow, then with `expanded` (readings tiles, a card for each hour).", children: _jsxs(Flex, { direction: "column", gap: 6, children: [_jsx(Flex, { direction: "column", width: 380, children: _jsx(WeatherForecast, { entity: "ha:weather.home", expanded: false }) }), _jsx(WeatherForecast, { entity: "ha:weather.home", expanded: true })] }) }), _jsx(ComponentDoc, { title: "Media Browser", components: ['MediaBrowser'], description: "A player's own library in the app's style: tap a folder to open it, a track or station to play it, or the play button on an album, playlist or artist for all of it. The search box appears when the library can be searched. The `theater` layout (second demo) is one row of large cards that scrolls sideways, for a wide space; `list` is the compact default. While a shelf loads, placeholders take the room the cards will (third demo, a shelf that never arrives), so nothing moves.", children: _jsxs(Flex, { direction: "column", gap: 6, children: [_jsx(Flex, { direction: "column", maxHeight: 420, overflow: "auto", children: _jsx(MediaBrowser, { entity: "ha:media_player.living_room" }) }), _jsx(MediaBrowser, { entity: "ha:media_player.living_room", layout: "theater" }), _jsx(MediaBrowser, { entity: "ha:media_player.loading", layout: "theater" })] }) }), _jsx(ComponentDoc, { title: "Navigation", components: ['NavRail', 'NavDock', 'Page'], description: "Routes within one dashboard. A dashboard includes one itself, usually in its layout route: NavRail is fixed to the left and NavDock floats at the bottom, and both reserve the space they take in the Page around them, which pads so content never sits under them.", children: _jsxs(Flex, { direction: "column", gap: 5, children: [_jsx(SubPreview, { label: "NavRail, in a Page", height: 240, children: _jsxs(Page, { height: "100%", children: [_jsx(NavRail, { base: "/gallery", items: NAV_ITEMS }), _jsx(Typography, { as: "p", variant: "body", color: "textMuted", children: "Page content, padded clear of the rail." })] }) }), _jsx(SubPreview, { label: "NavDock", height: 140, children: _jsx(NavDock, { base: "/gallery", items: NAV_ITEMS }) })] }) }), _jsx(ComponentDoc, { title: "Top Bar", components: ['TopBar'], description: "Page chrome above a dashboard's content: a title (a dropdown to switch dashboards, once there's more than one to switch to), scene shortcuts, weather (tap it for the forecast), presence, and a clock.", children: _jsx(TopBar, { title: "Living Room", dashboards: [
|
|
351
353
|
{ id: 'living-room', title: 'Living Room', icon: 'lu:sofa' },
|
|
352
354
|
{ id: 'bedroom', title: 'Bedroom', icon: 'lu:bed' },
|
|
353
355
|
{ id: 'bathroom', title: 'Bathroom', icon: 'tb:bath' },
|
|
354
|
-
], scenes: [{ entity: 'ha:scene.tv_time', icon: 'lu:tv' }], weather: "ha:
|
|
356
|
+
], scenes: [{ entity: 'ha:scene.tv_time', icon: 'lu:tv' }], weather: "ha:weather.home", people: ['ha:person.dan', 'ha:person.alex'] }) }), _jsx(ComponentDoc, { title: "Presence Stack", components: ['PresenceStack'], description: "Overlapping avatar circles for a handful of person entities (the Top Bar's presence), each with a picture or a colored initial.", children: _jsx(PresenceStack, { entities: ['ha:person.dan', 'ha:person.alex'] }) }), _jsx(ComponentDoc, { title: "System Status", components: ['SystemStatus'], description: "The dot at the top bar's right edge. Green when the runtime and every integration are up, amber while something is connecting, red when something is down; click it for per-system detail. Shown here open, in each state.", children: _jsxs(Flex, { gap: 6, px: 6, align: "flex-start", justify: "space-around", minHeight: 32, children: [_jsx(SystemStatus, { defaultOpen: true, link: "open", statuses: { ha: 'connected', ma: 'connected' } }), _jsx(SystemStatus, { defaultOpen: true, link: "open", statuses: { ha: 'connected', ma: 'connecting' } }), _jsx(SystemStatus, { defaultOpen: true, link: "open", statuses: { ha: 'error', ma: 'disconnected' } })] }) }), _jsx(ComponentDoc, { title: "Hashsome Provider", description: "The app provider: connects to the runtime, installs the theme, font and density globally, and renders the shared detail drawer. Everything on this page runs inside one \u2014 use the toggle above to flip its theme.", components: ['HashsomeProvider'], children: _jsx(Typography, { as: "span", variant: "body", color: "textMuted", children: `theme="${mode}" · density="${density}"` }) })] }) }) }));
|
|
355
357
|
}
|