@hashsome/ui 0.1.0 → 0.2.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 +1 -1
- 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 +15 -3
- package/dist/entities/media-player-bar.js +2 -2
- package/dist/entities/media-player-column.js +2 -2
- package/dist/entities/media-player-full.d.ts +20 -0
- package/dist/entities/{media-player-body.js → media-player-full.js} +6 -5
- 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 +161 -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.js +7 -1
- package/dist/gallery/gallery.js +6 -5
- package/dist/gallery/props-data.js +49 -8
- package/dist/icon.d.ts +3 -1
- package/dist/icon.js +6 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -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/series-chart.d.ts +5 -1
- package/dist/layout/series-chart.js +3 -3
- package/dist/layout/tile.d.ts +9 -1
- package/dist/layout/tile.js +8 -4
- package/dist/use-entity-logbook.d.ts +13 -0
- package/dist/use-entity-logbook.js +42 -0
- package/dist/use-weather-forecast.d.ts +12 -0
- package/dist/use-weather-forecast.js +41 -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.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
|
@@ -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
|
+
}
|
package/dist/gallery/fixtures.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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`;
|
|
@@ -62,6 +62,12 @@ export function createGalleryIntegration() {
|
|
|
62
62
|
'sensor.lamp_energy': energy('1.64'),
|
|
63
63
|
'sensor.led_power': power('7'),
|
|
64
64
|
'sensor.led_energy': energy('3.2'),
|
|
65
|
+
'weather.home': mockWeather({
|
|
66
|
+
name: 'Home',
|
|
67
|
+
condition: 'partlycloudy',
|
|
68
|
+
temperature: 12.3,
|
|
69
|
+
humidity: 71,
|
|
70
|
+
}),
|
|
65
71
|
'sensor.temperature': mockSensor({ value: '18.04', unit: '°C', measurement: 'temperature' }),
|
|
66
72
|
'sensor.humidity': mockSensor({ value: '64.41', unit: '%', measurement: 'humidity' }),
|
|
67
73
|
'sensor.bedroom_humidity': mockSensor({
|
package/dist/gallery/gallery.js
CHANGED
|
@@ -9,7 +9,8 @@ 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 { WeatherForecast } from '../entities/weather-forecast.js';
|
|
13
14
|
import { NavDock } from '../entities/nav-dock.js';
|
|
14
15
|
import { NavRail } from '../entities/nav-rail.js';
|
|
15
16
|
import { Page } from '../layout/page.js';
|
|
@@ -80,8 +81,8 @@ function TileFillDemo() {
|
|
|
80
81
|
function SubPreview({ label, height, children, }) {
|
|
81
82
|
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
83
|
}
|
|
83
|
-
//
|
|
84
|
-
//
|
|
84
|
+
// Hand-written activity, to show the drawer's History section through the `history` prop. Tiles
|
|
85
|
+
// without it (and with a backend that keeps a record) fetch their own.
|
|
85
86
|
const lampHistory = [
|
|
86
87
|
{
|
|
87
88
|
id: '1',
|
|
@@ -347,9 +348,9 @@ export function Gallery({ density = 'comfortable' }) {
|
|
|
347
348
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',
|
|
348
349
|
transform: mode === 'dark' ? 'translate(28px, -50%)' : 'translateY(-50%)',
|
|
349
350
|
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: [
|
|
351
|
+
}) }), _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'], description: "The big player: the player centered, and its library below it, so songs are picked right there instead of in a drawer. The media cards' drawers show it 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, 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.", 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 (tap it for the forecast), presence, and a clock.", children: _jsx(TopBar, { title: "Living Room", dashboards: [
|
|
351
352
|
{ id: 'living-room', title: 'Living Room', icon: 'lu:sofa' },
|
|
352
353
|
{ id: 'bedroom', title: 'Bedroom', icon: 'lu:bed' },
|
|
353
354
|
{ id: 'bathroom', title: 'Bathroom', icon: 'tb:bath' },
|
|
354
|
-
], scenes: [{ entity: 'ha:scene.tv_time', icon: 'lu:tv' }], weather: "ha:
|
|
355
|
+
], 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
356
|
}
|
|
@@ -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,33 @@ 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: 'Calls the player this instead of the name it reports.',
|
|
473
491
|
name: 'name',
|
|
474
492
|
optional: true,
|
|
475
493
|
type: 'string',
|
|
476
494
|
},
|
|
495
|
+
{
|
|
496
|
+
doc: 'Gives the library the whole width, for one laid out to use it (the theater layout). Defaults to whether the drawer is expanded.',
|
|
497
|
+
name: 'wide',
|
|
498
|
+
optional: true,
|
|
499
|
+
type: 'boolean',
|
|
500
|
+
},
|
|
477
501
|
],
|
|
478
502
|
},
|
|
479
503
|
MediaBrowser: {
|
|
@@ -567,7 +591,7 @@ export const COMPONENT_PROPS = {
|
|
|
567
591
|
type: '(EntityRef | TopBarScene)[]',
|
|
568
592
|
},
|
|
569
593
|
{
|
|
570
|
-
doc:
|
|
594
|
+
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
595
|
name: 'weather',
|
|
572
596
|
optional: true,
|
|
573
597
|
type: 'EntityRef',
|
|
@@ -662,6 +686,23 @@ export const COMPONENT_PROPS = {
|
|
|
662
686
|
},
|
|
663
687
|
],
|
|
664
688
|
},
|
|
689
|
+
WeatherForecast: {
|
|
690
|
+
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.",
|
|
691
|
+
props: [
|
|
692
|
+
{
|
|
693
|
+
doc: 'A weather entity, as a ref like `ha:weather.forecast_home`. The forecasts it offers (hourly, daily) are read through the runtime.',
|
|
694
|
+
name: 'entity',
|
|
695
|
+
optional: false,
|
|
696
|
+
type: 'EntityRef',
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
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.',
|
|
700
|
+
name: 'expanded',
|
|
701
|
+
optional: true,
|
|
702
|
+
type: 'boolean',
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
},
|
|
665
706
|
HashsomeProvider: {
|
|
666
707
|
doc: 'Connects the tree to the runtime proxy. Render only on the client.',
|
|
667
708
|
props: [
|
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,7 +22,8 @@ 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';
|
|
27
28
|
export { formatDuration } from './entities/media-progress.ts';
|
|
28
29
|
export * from './entities/nav-rail.tsx';
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,8 @@ 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';
|
|
21
22
|
export { formatDuration } from './entities/media-progress.js';
|
|
22
23
|
export * from './entities/nav-rail.js';
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -14,12 +14,12 @@ export function formatTick(range, iso) {
|
|
|
14
14
|
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
15
15
|
}
|
|
16
16
|
/** 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, }) {
|
|
17
|
+
export function SeriesChart({ samples, range, expanded, unit, fitDomain = false, height, }) {
|
|
18
18
|
const accent = useColorByKey('accent') ?? '';
|
|
19
19
|
const textMuted = useColorByKey('textMuted') ?? '';
|
|
20
20
|
const line = useColorByKey('line') ?? '';
|
|
21
21
|
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 }) : null, expanded ? (_jsx(XAxis, { dataKey: "timestamp", tickFormatter: (value) => formatTick(range, value), tick: { fontSize: tickSize, fill: textMuted }, axisLine: false, 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
|
|
22
|
+
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, strokeDasharray: "3 3", vertical: false }) : null, expanded ? (_jsx(XAxis, { dataKey: "timestamp", tickFormatter: (value) => formatTick(range, value), tick: { fontSize: tickSize, fill: textMuted }, axisLine: false, 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
23
|
? {}
|
|
24
24
|
: { 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
25
|
}
|
|
@@ -42,5 +42,5 @@ function ChartTooltip({ active, payload, unit, }) {
|
|
|
42
42
|
day: 'numeric',
|
|
43
43
|
hour: '2-digit',
|
|
44
44
|
minute: '2-digit',
|
|
45
|
-
}) }), _jsxs(Typography, { as: "span", variant: "bodyStrong", children: [Math.round(sample.value * 10) / 10, " ", unit] })] }) }));
|
|
45
|
+
}) }), _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
46
|
}
|
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;
|
package/dist/layout/tile.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Fragment as _Fragment, 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 { AnimatePresence, motion } from 'motion/react';
|
|
5
4
|
import { useEffect, useMemo, useRef, useState, } from 'react';
|
|
@@ -9,6 +8,7 @@ import { statusLabels } from '../status.js';
|
|
|
9
8
|
import { useDrawer } from './use-drawer.js';
|
|
10
9
|
import { EnergyChart } from './energy-chart.js';
|
|
11
10
|
import { HistorySection } from './history-section.js';
|
|
11
|
+
import { LogbookHistory } from './logbook-history.js';
|
|
12
12
|
const DRAG_THRESHOLD = 8;
|
|
13
13
|
const KEY_STEP = 0.05;
|
|
14
14
|
const HOLD_MS = 500;
|
|
@@ -19,11 +19,15 @@ const valueAt = (event) => {
|
|
|
19
19
|
return rect.width > 0 ? clamp01((event.clientX - rect.left) / rect.width) : 0;
|
|
20
20
|
};
|
|
21
21
|
/** Base pill used by all entity tiles. Tap to press; hold (with `detail` set) to open the drawer. */
|
|
22
|
-
export function Tile({ label, icon, secondary, status = 'ready', active = false, feedback, onPress, fill, onFillChange, trailing, overlay, title, detail, kind, energy, history, }) {
|
|
22
|
+
export function Tile({ label, icon, secondary, status = 'ready', active = false, feedback, onPress, fill, onFillChange, trailing, overlay, title, detail, kind, energy, history, logbook, }) {
|
|
23
23
|
const ready = status === 'ready';
|
|
24
24
|
const adjustable = fill !== undefined && onFillChange !== undefined && ready;
|
|
25
|
-
const holdable = (detail !== undefined ||
|
|
26
|
-
|
|
25
|
+
const holdable = (detail !== undefined ||
|
|
26
|
+
energy !== undefined ||
|
|
27
|
+
history !== undefined ||
|
|
28
|
+
logbook !== undefined) &&
|
|
29
|
+
ready;
|
|
30
|
+
const drawerBody = useMemo(() => (_jsxs(_Fragment, { children: [detail, energy ? _jsx(EnergyChart, { ...energy }) : null, history ? (_jsx(HistorySection, { entries: history })) : logbook ? (_jsx(LogbookHistory, { entity: logbook })) : null] })), [detail, energy, history, logbook]);
|
|
27
31
|
const [drag, setDrag] = useState(null);
|
|
28
32
|
const [holding, setHolding] = useState(false);
|
|
29
33
|
const start = useRef(null);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntityRef, LogbookEntry } from '@hashsome/core';
|
|
2
|
+
export interface EntityLogbookState {
|
|
3
|
+
/** `undefined` until the first answer, and always for a backend that keeps no activity. */
|
|
4
|
+
entries: LogbookEntry[] | undefined;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* What happened to an entity lately and who or what caused it, from the backend's own record,
|
|
9
|
+
* fetched when `ref` changes and kept for half a minute. A backend that keeps no activity, or an
|
|
10
|
+
* error, leaves `entries` undefined: activity is an extra, so a component just shows nothing. Pass
|
|
11
|
+
* `undefined` to not ask at all.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useEntityLogbook(ref: EntityRef | undefined): EntityLogbookState;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useClient } from './provider.js';
|
|
3
|
+
/** How long an answer is reused: closing and reopening a drawer does not ask again. */
|
|
4
|
+
const FRESH_MS = 30_000;
|
|
5
|
+
/** How many entries a drawer's History section lists. */
|
|
6
|
+
const LIMIT = 6;
|
|
7
|
+
const cache = new WeakMap();
|
|
8
|
+
function load(client, ref) {
|
|
9
|
+
const answers = cache.get(client) ?? new Map();
|
|
10
|
+
cache.set(client, answers);
|
|
11
|
+
const known = answers.get(ref);
|
|
12
|
+
if (known && Date.now() - known.at < FRESH_MS) {
|
|
13
|
+
return known.entries;
|
|
14
|
+
}
|
|
15
|
+
const entries = client.logbook(ref, { limit: LIMIT }).then((result) => result.entries);
|
|
16
|
+
answers.set(ref, { at: Date.now(), entries });
|
|
17
|
+
// A failed answer is not worth keeping: the next ask tries again.
|
|
18
|
+
entries.catch(() => answers.delete(ref));
|
|
19
|
+
return entries;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What happened to an entity lately and who or what caused it, from the backend's own record,
|
|
23
|
+
* fetched when `ref` changes and kept for half a minute. A backend that keeps no activity, or an
|
|
24
|
+
* error, leaves `entries` undefined: activity is an extra, so a component just shows nothing. Pass
|
|
25
|
+
* `undefined` to not ask at all.
|
|
26
|
+
*/
|
|
27
|
+
export function useEntityLogbook(ref) {
|
|
28
|
+
const client = useClient();
|
|
29
|
+
const [state, setState] = useState();
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!ref) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let current = true;
|
|
35
|
+
load(client, ref).then((entries) => current && setState({ ref, entries }), () => current && setState({ ref, entries: undefined }));
|
|
36
|
+
return () => {
|
|
37
|
+
current = false;
|
|
38
|
+
};
|
|
39
|
+
}, [client, ref]);
|
|
40
|
+
const answered = state !== undefined && state.ref === ref;
|
|
41
|
+
return { entries: answered ? state.entries : undefined, loading: ref !== undefined && !answered };
|
|
42
|
+
}
|