@hashsome/ui 0.3.0 → 0.5.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.js +11 -0
- package/dist/entities/light-tile.d.ts +3 -1
- package/dist/entities/light-tile.js +29 -20
- package/dist/entities/media-player-bar.d.ts +9 -1
- package/dist/entities/media-player-bar.js +122 -12
- package/dist/entities/media-seek.d.ts +4 -1
- package/dist/entities/media-seek.js +22 -3
- package/dist/entities/nav-dock.js +3 -3
- package/dist/entities/seek-line.js +12 -0
- package/dist/entities/top-bar.d.ts +21 -0
- package/dist/entities/top-bar.js +21 -10
- package/dist/gallery/gallery.js +16 -3
- package/dist/gallery/props-data.js +86 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/layout/animated-outlet.d.ts +14 -0
- package/dist/layout/animated-outlet.js +44 -0
- package/dist/layout/detail-provider.d.ts +4 -2
- package/dist/layout/detail-provider.js +2 -2
- package/dist/layout/drawer-controls.js +18 -2
- package/dist/layout/entity-drawer.js +1 -1
- package/dist/layout/hold-progress.d.ts +8 -2
- package/dist/layout/hold-progress.js +20 -4
- package/dist/layout/tile.js +6 -10
- package/dist/layout/use-drawer.d.ts +3 -2
- package/dist/layout/use-drawer.js +5 -3
- package/dist/provider.d.ts +10 -3
- package/dist/provider.js +124 -11
- package/dist/theme/global-styles.js +3 -0
- package/dist/theme/index.js +2 -1
- package/dist/theme/schedule.d.ts +33 -0
- package/dist/theme/schedule.js +44 -0
- package/dist/theme/tokens.d.ts +2 -0
- package/package.json +2 -2
package/dist/gallery/gallery.js
CHANGED
|
@@ -3,11 +3,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "@emotion/reac
|
|
|
3
3
|
import { Flex, Typography } from 'e-prim';
|
|
4
4
|
import { LocalClient } from '@hashsome/core';
|
|
5
5
|
import { useEffect, useMemo, useState } from 'react';
|
|
6
|
+
import { NavLink, Route, Routes } from 'react-router';
|
|
6
7
|
import { ActionButton } from '../entities/action-button.js';
|
|
7
8
|
import { ClimateTile } from '../entities/climate-tile.js';
|
|
8
9
|
import { LightTile } from '../entities/light-tile.js';
|
|
9
10
|
import { MediaBrowser } from '../entities/media-browser.js';
|
|
10
11
|
import { MediaPlayerBar } from '../entities/media-player-bar.js';
|
|
12
|
+
import { AnimatedOutlet } from '../layout/animated-outlet.js';
|
|
11
13
|
import { MediaPlayerColumn } from '../entities/media-player-column.js';
|
|
12
14
|
import { MediaPlayerFull } from '../entities/media-player-full.js';
|
|
13
15
|
import { MediaQueue } from '../entities/media-queue.js';
|
|
@@ -17,7 +19,7 @@ import { NavRail } from '../entities/nav-rail.js';
|
|
|
17
19
|
import { Page } from '../layout/page.js';
|
|
18
20
|
import { SceneButton } from '../entities/scene-button.js';
|
|
19
21
|
import { SensorReadout } from '../entities/sensor-readout.js';
|
|
20
|
-
import { PresenceStack, SystemStatus, TopBar } from '../entities/top-bar.js';
|
|
22
|
+
import { Clock, DateChip, PresenceStack, SystemStatus, TopBar, WeatherChip, } from '../entities/top-bar.js';
|
|
21
23
|
import { Icon } from '../icon.js';
|
|
22
24
|
import { EnergyChart, } from '../layout/energy-chart.js';
|
|
23
25
|
import { Grid } from '../layout/grid.js';
|
|
@@ -79,6 +81,17 @@ function TileFillDemo() {
|
|
|
79
81
|
* chrome). `NavDock` is `position: fixed`, which a plain wrapper can't contain — `transform` on
|
|
80
82
|
* an ancestor makes it establish a containing block for fixed-position descendants instead (a
|
|
81
83
|
* standard CSS escape hatch), confining it to this box instead of the real viewport. */
|
|
84
|
+
const DEMO_PAGES = [
|
|
85
|
+
{ to: '', label: 'Lights', icon: 'lu:lightbulb' },
|
|
86
|
+
{ to: 'climate', label: 'Climate', icon: 'lu:thermometer' },
|
|
87
|
+
{ to: 'music', label: 'Music', icon: 'lu:music' },
|
|
88
|
+
];
|
|
89
|
+
const DEMO_BASE = '/gallery/transition';
|
|
90
|
+
/** Three pages under a route of the gallery's own router, with links to go between them: later ones
|
|
91
|
+
* slide in from the right. Until the address is under the demo's route there is a link to start it. */
|
|
92
|
+
function AnimatedOutletDemo() {
|
|
93
|
+
return (_jsx(Flex, { direction: "column", gap: 3, height: 140, p: 3, children: _jsxs(Routes, { children: [_jsx(Route, { path: DEMO_BASE, element: _jsxs(_Fragment, { children: [_jsx(Flex, { gap: 2, children: DEMO_PAGES.map((page) => (_jsx(NavLink, { to: page.to ? `${DEMO_BASE}/${page.to}` : DEMO_BASE, end: true, children: ({ isActive }) => (_jsx(Flex, { as: "span", align: "center", px: 3, height: 32, radius: "full", background: isActive ? 'accent' : 'surfaceRaised', color: isActive ? 'accentText' : 'text', children: _jsx(Typography, { as: "span", variant: "label", children: page.label }) })) }, page.to))) }), _jsx(AnimatedOutlet, { items: [...DEMO_PAGES], base: DEMO_BASE })] }), children: DEMO_PAGES.map((page) => (_jsx(Route, { ...(page.to ? { path: page.to } : { index: true }), element: _jsx(Flex, { center: true, grow: 1, background: "surfaceRaised", radius: "row", children: _jsx(Typography, { as: "span", variant: "heading", children: page.label }) }) }, page.to))) }), _jsx(Route, { path: "*", element: _jsx(Flex, { center: true, grow: 1, children: _jsx(NavLink, { to: DEMO_BASE, children: "Try the transition" }) }) })] }) }));
|
|
94
|
+
}
|
|
82
95
|
function SubPreview({ label, height, children, }) {
|
|
83
96
|
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 })] }));
|
|
84
97
|
}
|
|
@@ -349,9 +362,9 @@ export function Gallery({ density = 'comfortable' }) {
|
|
|
349
362
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',
|
|
350
363
|
transform: mode === 'dark' ? 'translate(28px, -50%)' : 'translateY(-50%)',
|
|
351
364
|
transition: 'transform 0.2s ease',
|
|
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: [
|
|
365
|
+
}) }), _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. In a space narrower than 560px, or with `rows={2}`, the controls move to a second row under the track.", children: _jsxs(Flex, { direction: "column", gap: 3, children: [_jsx(MediaPlayerBar, { entity: "ha:media_player.living_room" }), _jsx(MediaPlayerBar, { entity: "ha:media_player.off" }), _jsx(Flex, { width: 380, children: _jsx(MediaPlayerBar, { entity: "ha:media_player.living_room", rows: 2 }) })] }) }), _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: "Animated Outlet", components: ['AnimatedOutlet'], description: "A dashboard layout's `Outlet` with a transition between its pages: the page that was showing fades out, then the new one fades in, sliding from the side it sits on in the nav (a later page from the right, an earlier one from the left) when you give it the nav's `items`. Without them the pages only fade. It does nothing for a visitor who prefers reduced motion. Try the links.", children: _jsx(AnimatedOutletDemo, {}) }), _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: [
|
|
353
366
|
{ id: 'living-room', title: 'Living Room', icon: 'lu:sofa' },
|
|
354
367
|
{ id: 'bedroom', title: 'Bedroom', icon: 'lu:bed' },
|
|
355
368
|
{ id: 'bathroom', title: 'Bathroom', icon: 'tb:bath' },
|
|
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}"` }) })] }) }) }));
|
|
369
|
+
], scenes: [{ entity: 'ha:scene.tv_time', icon: 'lu:tv' }], weather: "ha:weather.home", people: ['ha:person.dan', 'ha:person.alex'] }) }), _jsx(ComponentDoc, { title: "Header pieces", components: ['DateChip', 'WeatherChip', 'Clock'], description: "The top bar's date, weather and clock, on their own: for a dashboard that builds its own header (a small room display, say) without the title, switcher and scenes of a whole TopBar. Put them in a row with SystemStatus, PresenceStack or anything else.", children: _jsxs(Flex, { align: "center", gap: 3, px: 4, children: [_jsx(DateChip, {}), _jsx(WeatherChip, { entity: "ha:weather.home" }), _jsx(Clock, { format: "24h" })] }) }), _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}"` }) })] }) }) }));
|
|
357
370
|
}
|
|
@@ -35,6 +35,23 @@ export const COMPONENT_PROPS = {
|
|
|
35
35
|
},
|
|
36
36
|
],
|
|
37
37
|
},
|
|
38
|
+
AnimatedOutlet: {
|
|
39
|
+
doc: "A dashboard layout's `<Outlet />` with a transition between its pages: the page that was showing fades out (sliding a little toward the side it was on), then the new one fades in from the side it is on. It is a drop-in for `Outlet` in a layout route, and does nothing the first time a page shows or for a visitor who prefers reduced motion.",
|
|
40
|
+
props: [
|
|
41
|
+
{
|
|
42
|
+
doc: "The dashboard's pages in the order its nav lists them (the same `items` as `NavRail` or `NavDock`). A page then slides in from the side it sits on: a later page from the right, an earlier one from the left. Left out, pages only fade.",
|
|
43
|
+
name: 'items',
|
|
44
|
+
optional: true,
|
|
45
|
+
type: 'NavItem[]',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
doc: 'The path `items` are relative to, e.g. `/bathroom`. Needed with `items`.',
|
|
49
|
+
name: 'base',
|
|
50
|
+
optional: true,
|
|
51
|
+
type: 'string',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
38
55
|
RoomHeader: {
|
|
39
56
|
doc: 'The header that starts a room: muted icon and title, a hairline, and readouts on the right. Put it above a `Grid` (or any tiles); it adds its own space above, so consecutive rooms read as groups.',
|
|
40
57
|
props: [
|
|
@@ -283,6 +300,12 @@ export const COMPONENT_PROPS = {
|
|
|
283
300
|
optional: true,
|
|
284
301
|
type: 'LightColorPreset[]',
|
|
285
302
|
},
|
|
303
|
+
{
|
|
304
|
+
doc: 'Holding the tile opens a drawer with the full controls, the color bars, the energy chart and the recent activity. `false` leaves the tile as it is without it: a toggle that dims by dragging, with the color swatches if the light has color, and no drawer built, which is lighter on a small display. Default `true`.',
|
|
305
|
+
name: 'drawer',
|
|
306
|
+
optional: true,
|
|
307
|
+
type: 'boolean',
|
|
308
|
+
},
|
|
286
309
|
],
|
|
287
310
|
},
|
|
288
311
|
ClimateTile: {
|
|
@@ -446,6 +469,30 @@ export const COMPONENT_PROPS = {
|
|
|
446
469
|
optional: true,
|
|
447
470
|
type: 'ReactNode | false',
|
|
448
471
|
},
|
|
472
|
+
{
|
|
473
|
+
doc: '`false` builds no drawer: holding the card, pressing the artwork and the browse button then do nothing, which is lighter on a small display. Default `true`. See `onOpen` to send them somewhere else instead.',
|
|
474
|
+
name: 'drawer',
|
|
475
|
+
optional: true,
|
|
476
|
+
type: 'boolean',
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
doc: "Called instead of opening the drawer when the card is held, the artwork pressed or the browse button pressed: for a dashboard with a page of its own for the player (a `MediaPlayerFull`), `onOpen={() => navigate('/bathroom/music')}`. Given, no drawer is built. The browse button stays unless `browse` is `false`.",
|
|
480
|
+
name: 'onOpen',
|
|
481
|
+
optional: true,
|
|
482
|
+
type: '() => void',
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
doc: 'The library and the queue each open as a full-size overlay of their own, from the browse button and a new queue button, instead of one drawer with the whole player in it: for a small display, where the card itself is the player. The library overlay is a `MediaBrowser` as a list (or your `browse` content), the queue overlay a `MediaQueue`; each button is left out when the player has none, or for `browse={false}`. Holding the card and pressing the artwork then do nothing, unless `onOpen` is given. Default `false`.',
|
|
486
|
+
name: 'overlays',
|
|
487
|
+
optional: true,
|
|
488
|
+
type: 'boolean',
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
doc: "`1` puts the controls beside the track, in one pill; `2` puts them on a second row under it, for a narrow space (a small wall display). Default `'auto'`: one row, and two once the bar is narrower than 560 px wide.",
|
|
492
|
+
name: 'rows',
|
|
493
|
+
optional: true,
|
|
494
|
+
type: "1 | 2 | 'auto'",
|
|
495
|
+
},
|
|
449
496
|
],
|
|
450
497
|
},
|
|
451
498
|
MediaPlayerColumn: {
|
|
@@ -686,6 +733,23 @@ export const COMPONENT_PROPS = {
|
|
|
686
733
|
},
|
|
687
734
|
],
|
|
688
735
|
},
|
|
736
|
+
WeatherChip: {
|
|
737
|
+
doc: "Current weather, as a pill: an icon for the sky and a rounded whole-degree reading. A `weather` entity gives the real condition, today's high and low once its forecast has loaded, and opens the forecast when tapped (if its source has one); a plain `sensor.*` (an outdoor temperature) gets a fixed sun and no forecast, since a sensor says nothing about the sky. The top bar's weather pill, for a dashboard that builds its own header instead of using `TopBar`.",
|
|
738
|
+
props: [
|
|
739
|
+
{
|
|
740
|
+
doc: 'A weather entity, like `ha:weather.forecast_home`, or a plain temperature sensor.',
|
|
741
|
+
name: 'entity',
|
|
742
|
+
optional: false,
|
|
743
|
+
type: 'EntityRef',
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
doc: "Tapping a weather entity's pill opens its forecast in a drawer. `false` builds none and the pill is only what it shows, which is lighter on a small display. Default `true`.",
|
|
747
|
+
name: 'drawer',
|
|
748
|
+
optional: true,
|
|
749
|
+
type: 'boolean',
|
|
750
|
+
},
|
|
751
|
+
],
|
|
752
|
+
},
|
|
689
753
|
PresenceStack: {
|
|
690
754
|
doc: 'Overlapping avatar circles for a handful of `person.*` entities.',
|
|
691
755
|
props: [
|
|
@@ -703,6 +767,21 @@ export const COMPONENT_PROPS = {
|
|
|
703
767
|
},
|
|
704
768
|
],
|
|
705
769
|
},
|
|
770
|
+
DateChip: {
|
|
771
|
+
doc: "A short date pill (`Tue · Oct 6`), the top bar's, for a dashboard that builds its own header.",
|
|
772
|
+
props: [],
|
|
773
|
+
},
|
|
774
|
+
Clock: {
|
|
775
|
+
doc: "The top bar's clock: the time in the large clock type, with AM/PM beside it in a 12h format. For a dashboard that builds its own header instead of using `TopBar`.",
|
|
776
|
+
props: [
|
|
777
|
+
{
|
|
778
|
+
doc: "`'auto'` follows the browser's locale (browsers don't read the OS 12/24h setting, so an `en-US` browser shows AM/PM); `'12h'`/`'24h'` force it. Default `'auto'`.",
|
|
779
|
+
name: 'format',
|
|
780
|
+
optional: true,
|
|
781
|
+
type: "'auto' | '12h' | '24h'",
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
},
|
|
706
785
|
WeatherForecast: {
|
|
707
786
|
doc: "A weather entity's forecast: the weather now, the next 24 hours as a chart (hover it for everything about an hour) and the days ahead. The top bar's weather chip shows it in a drawer; a dashboard can put it in a page of its own, with `expanded` for the full-size layout. Wide, it adds the current readings and a card for each hour. A source with no forecast shows only the weather now.",
|
|
708
787
|
props: [
|
|
@@ -742,7 +821,7 @@ export const COMPONENT_PROPS = {
|
|
|
742
821
|
type: "Omit<RemoteClientOptions, 'url'>",
|
|
743
822
|
},
|
|
744
823
|
{
|
|
745
|
-
doc: "`'system'` follows the browser/OS preference and updates live. Default `'dark'`. A `useThemeToggle()` caller (e.g. `NavRail`'s dev toggle) can still override this at runtime.",
|
|
824
|
+
doc: "`'light'`, `'dark'`, `'system'` (follows the browser/OS preference and updates live), or a schedule: `{ dark: { from: '19:00', to: '07:00' } }` is dark between those times on the display's clock, `{ sun: 'ha:sun.sun' }` is dark while that entity, a `daylight` sensor (`on` while the sun is up; the Home Assistant integration makes one of `sun.sun`), is `off`. Default `'dark'`. A schedule and the document shell's first paint use `'system'` until the time or the entity is known. A `useThemeToggle()` caller (e.g. `NavRail`'s dev toggle) can still override this at runtime. Pass a constant defined outside the component.",
|
|
746
825
|
name: 'theme',
|
|
747
826
|
optional: true,
|
|
748
827
|
type: 'ThemeMode',
|
|
@@ -759,6 +838,12 @@ export const COMPONENT_PROPS = {
|
|
|
759
838
|
optional: true,
|
|
760
839
|
type: 'Density',
|
|
761
840
|
},
|
|
841
|
+
{
|
|
842
|
+
doc: "`'reduced'` turns animations and transitions off, for a slow display, `'full'` keeps them and `'auto'` (the default) leaves things as they are. A device can choose for itself with `?motion=reduced` (or `full`) on the address it opens: the choice is kept on the device, so reloads and links inside the app keep it, and `?motion=auto` forgets it. That wins over this prop.",
|
|
843
|
+
name: 'motion',
|
|
844
|
+
optional: true,
|
|
845
|
+
type: 'MotionPreference',
|
|
846
|
+
},
|
|
762
847
|
{
|
|
763
848
|
doc: 'Partial changes to the built-in theme — colors per light/dark, radii, typography, spacing, density sizes. Pass a constant defined outside the component: a new object each render rebuilds the theme each render.',
|
|
764
849
|
name: 'overrides',
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type { IconName } from './icon-data.ts';
|
|
|
9
9
|
export * from './icon.tsx';
|
|
10
10
|
export * from './layout/grid.tsx';
|
|
11
11
|
export * from './layout/page.tsx';
|
|
12
|
+
export * from './layout/animated-outlet.tsx';
|
|
12
13
|
export * from './layout/room-header.tsx';
|
|
13
14
|
export { Tile, type TileProps } from './layout/tile.tsx';
|
|
14
15
|
export * from './layout/energy-chart.tsx';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from './status.js';
|
|
|
5
5
|
export * from './icon.js';
|
|
6
6
|
export * from './layout/grid.js';
|
|
7
7
|
export * from './layout/page.js';
|
|
8
|
+
export * from './layout/animated-outlet.js';
|
|
8
9
|
export * from './layout/room-header.js';
|
|
9
10
|
export { Tile } from './layout/tile.js';
|
|
10
11
|
export * from './layout/energy-chart.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NavItem } from '../entities/nav-rail.tsx';
|
|
2
|
+
export interface AnimatedOutletProps {
|
|
3
|
+
/** The dashboard's pages in the order its nav lists them (the same `items` as `NavRail` or `NavDock`). A page then slides in from the side it sits on: a later page from the right, an earlier one from the left. Left out, pages only fade. */
|
|
4
|
+
items?: NavItem[];
|
|
5
|
+
/** The path `items` are relative to, e.g. `/bathroom`. Needed with `items`. */
|
|
6
|
+
base?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A dashboard layout's `<Outlet />` with a transition between its pages: the page that was showing
|
|
10
|
+
* fades out (sliding a little toward the side it was on), then the new one fades in from the side
|
|
11
|
+
* it is on. It is a drop-in for `Outlet` in a layout route, and does nothing the first time a page
|
|
12
|
+
* shows or for a visitor who prefers reduced motion.
|
|
13
|
+
*/
|
|
14
|
+
export declare function AnimatedOutlet({ items, base }: AnimatedOutletProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import { AnimatePresence, motion, useReducedMotion } from 'motion/react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { useLocation, useOutlet } from 'react-router';
|
|
6
|
+
/** How far a page slides, in px, as it comes and goes. */
|
|
7
|
+
const SLIDE = 24;
|
|
8
|
+
/** The index of the item whose page `pathname` is, or -1. */
|
|
9
|
+
function indexAt(pathname, items, base) {
|
|
10
|
+
if (!items || base === undefined) {
|
|
11
|
+
return -1;
|
|
12
|
+
}
|
|
13
|
+
const here = pathname.replace(/\/$/, '') || '/';
|
|
14
|
+
return items.findIndex((item) => (item.to ? `${base}/${item.to}` : base) === here);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A dashboard layout's `<Outlet />` with a transition between its pages: the page that was showing
|
|
18
|
+
* fades out (sliding a little toward the side it was on), then the new one fades in from the side
|
|
19
|
+
* it is on. It is a drop-in for `Outlet` in a layout route, and does nothing the first time a page
|
|
20
|
+
* shows or for a visitor who prefers reduced motion.
|
|
21
|
+
*/
|
|
22
|
+
export function AnimatedOutlet({ items, base }) {
|
|
23
|
+
const outlet = useOutlet();
|
|
24
|
+
const { pathname } = useLocation();
|
|
25
|
+
const still = useReducedMotion();
|
|
26
|
+
const index = indexAt(pathname, items, base);
|
|
27
|
+
// Moving to a later page is +1, to an earlier one -1; with no order to go by, 0 is just a fade.
|
|
28
|
+
const [seen, setSeen] = useState({ index, direction: 0 });
|
|
29
|
+
if (seen.index !== index) {
|
|
30
|
+
setSeen({
|
|
31
|
+
index,
|
|
32
|
+
direction: index === -1 || seen.index === -1 ? 0 : Math.sign(index - seen.index),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const seconds = still ? 0 : 0.14;
|
|
36
|
+
const slide = still ? 0 : SLIDE;
|
|
37
|
+
return (_jsx(AnimatePresence, { mode: "wait", initial: false, custom: seen.direction, children: _jsx(motion.div, { custom: seen.direction, variants: {
|
|
38
|
+
enter: (side) => ({ opacity: 0, x: side * slide }),
|
|
39
|
+
show: { opacity: 1, x: 0 },
|
|
40
|
+
exit: (side) => ({ opacity: 0, x: -side * slide }),
|
|
41
|
+
}, initial: "enter", animate: "show", exit: "exit", transition: { duration: seconds, ease: 'easeOut' },
|
|
42
|
+
// A page fills the space it is given, as it does without the wrapper.
|
|
43
|
+
css: { display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: 0 }, children: outlet }, pathname) }));
|
|
44
|
+
}
|
|
@@ -4,11 +4,13 @@ interface DetailState {
|
|
|
4
4
|
header: ReactNode;
|
|
5
5
|
body: ReactNode;
|
|
6
6
|
expanded: boolean;
|
|
7
|
+
/** Stays at full size: the drawer has no button to collapse it. */
|
|
8
|
+
locked: boolean;
|
|
7
9
|
}
|
|
8
10
|
interface DetailContextValue {
|
|
9
11
|
detail: DetailState | null;
|
|
10
|
-
/** `expanded` opens it at full size instead of the side panel. */
|
|
11
|
-
openDetail: (id: string, header: ReactNode, body: ReactNode, expanded?: boolean) => void;
|
|
12
|
+
/** `expanded` opens it at full size instead of the side panel; `locked` keeps it there, with no button to collapse it. */
|
|
13
|
+
openDetail: (id: string, header: ReactNode, body: ReactNode, expanded?: boolean, locked?: boolean) => void;
|
|
12
14
|
/** Keeps an already-open drawer's content live; a no-op for any other `id`. */
|
|
13
15
|
updateDetail: (id: string, header: ReactNode, body: ReactNode) => void;
|
|
14
16
|
closeDetail: () => void;
|
|
@@ -4,8 +4,8 @@ const DetailContext = createContext(null);
|
|
|
4
4
|
/** Backs the single shared `EntityDrawer`; instantiated once by `HashsomeProvider`. */
|
|
5
5
|
export function DetailProvider({ children }) {
|
|
6
6
|
const [detail, setDetail] = useState(null);
|
|
7
|
-
const openDetail = useCallback((id, header, body, expanded = false) => {
|
|
8
|
-
setDetail({ id, header, body, expanded });
|
|
7
|
+
const openDetail = useCallback((id, header, body, expanded = false, locked = false) => {
|
|
8
|
+
setDetail({ id, header, body, expanded: expanded || locked, locked });
|
|
9
9
|
}, []);
|
|
10
10
|
const updateDetail = useCallback((id, header, body) => {
|
|
11
11
|
setDetail((current) => (current && current.id === id ? { ...current, header, body } : current));
|
|
@@ -36,6 +36,7 @@ export function StepButton({ icon, label, delta, holdDelta = delta * 10, onStep,
|
|
|
36
36
|
* `fill` shows accent up to the value; a `gradient` track stays vivid up to the value and dims past it instead (no marker or thumb). */
|
|
37
37
|
export function ValueBar({ label, value, min, max, keyStep, round = 1, gradient, height = 28, onDrag, onCommit, }) {
|
|
38
38
|
const dragging = useRef(false);
|
|
39
|
+
const lastDrag = useRef(null);
|
|
39
40
|
const fraction = max === min ? 0 : (value - min) / (max - min);
|
|
40
41
|
const pct = Math.min(1, Math.max(0, fraction)) * 100;
|
|
41
42
|
const clampValue = (v) => Math.min(max, Math.max(min, v));
|
|
@@ -64,16 +65,31 @@ export function ValueBar({ label, value, min, max, keyStep, round = 1, gradient,
|
|
|
64
65
|
width: "100%", height: height, radius: "full", background: "surface", border: !gradient, cursor: "pointer", overflow: "hidden", css: { touchAction: 'none', ...(gradient ? { backgroundImage: gradient } : {}) }, onPointerDown: (event) => {
|
|
65
66
|
dragging.current = true;
|
|
66
67
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
67
|
-
|
|
68
|
+
lastDrag.current = valueAt(event);
|
|
69
|
+
onDrag(lastDrag.current);
|
|
68
70
|
}, onPointerMove: (event) => {
|
|
69
71
|
if (dragging.current) {
|
|
70
|
-
|
|
72
|
+
lastDrag.current = valueAt(event);
|
|
73
|
+
onDrag(lastDrag.current);
|
|
71
74
|
}
|
|
72
75
|
}, onPointerUp: (event) => {
|
|
73
76
|
if (dragging.current) {
|
|
74
77
|
dragging.current = false;
|
|
75
78
|
onCommit(valueAt(event));
|
|
76
79
|
}
|
|
80
|
+
},
|
|
81
|
+
// A touch the browser took or lost never ends with a release. Ending the drag with the last
|
|
82
|
+
// value the finger reached lets whoever holds it let go, instead of leaving it held for good.
|
|
83
|
+
onPointerCancel: () => {
|
|
84
|
+
if (dragging.current) {
|
|
85
|
+
dragging.current = false;
|
|
86
|
+
onCommit(lastDrag.current ?? value);
|
|
87
|
+
}
|
|
88
|
+
}, onLostPointerCapture: () => {
|
|
89
|
+
if (dragging.current) {
|
|
90
|
+
dragging.current = false;
|
|
91
|
+
onCommit(lastDrag.current ?? value);
|
|
92
|
+
}
|
|
77
93
|
}, onKeyDown: onKeyDown, children: gradient ? (_jsx(Box, { as: motion.span, position: "absolute", background: "surface", initial: false, animate: { left: `${Math.max(pct, 4)}%` }, transition: { duration: 0 }, css: { top: 0, bottom: 0, right: 0, opacity: 0.72, pointerEvents: 'none' } })) : (_jsx(Box, { as: motion.span, position: "absolute", background: "accent", initial: false, animate: { width: `${pct}%` }, transition: { duration: 0 }, css: { top: 0, bottom: 0, left: 0 } })) }));
|
|
78
94
|
}
|
|
79
95
|
/** One round power icon button with two states: filled with the accent while on, neutral while off. */
|
|
@@ -68,7 +68,7 @@ export function EntityDrawer() {
|
|
|
68
68
|
// its default `min-height: auto` can win out and push it past the stretched height
|
|
69
69
|
// instead of scrolling internally. `height: '100%'` here is well-defined regardless
|
|
70
70
|
// (the wrapper's `inset: 0` gives it an unambiguous content-box size).
|
|
71
|
-
height: "100%", minHeight: 0, maxHeight: "100%", overflow: "auto", p: 5, css: { pointerEvents: 'auto' }, children: [_jsxs(Flex, { justify: "space-between", align: "center", gap: 2.5, mb: 4, children: [detail.header, _jsxs(Flex, { gap: 1.5, grow: 0, children: [_jsx(DrawerActionButton, { icon: expanded ? 'lu:minimize-2' : 'lu:maximize-2', label: expanded ? 'Collapse' : 'Expand', onClick: toggleExpanded }), _jsx(DrawerActionButton, { icon: "lu:x", label: "Close", onClick: closeDetail })] })] }), detail.body] }, "hash-drawer") })] })) : null }));
|
|
71
|
+
height: "100%", minHeight: 0, maxHeight: "100%", overflow: "auto", p: 5, css: { pointerEvents: 'auto' }, children: [_jsxs(Flex, { justify: "space-between", align: "center", gap: 2.5, mb: 4, children: [detail.header, _jsxs(Flex, { gap: 1.5, grow: 0, children: [detail.locked ? null : (_jsx(DrawerActionButton, { icon: expanded ? 'lu:minimize-2' : 'lu:maximize-2', label: expanded ? 'Collapse' : 'Expand', onClick: toggleExpanded })), _jsx(DrawerActionButton, { icon: "lu:x", label: "Close", onClick: closeDetail })] })] }), detail.body] }, "hash-drawer") })] })) : null }));
|
|
72
72
|
}
|
|
73
73
|
const ACTION_BACKGROUND = 'surfaceRaised';
|
|
74
74
|
const ACTION_COLOR = 'text';
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export interface HoldProgressProps {
|
|
2
|
+
/** Which edge of the card the line runs along. Default `top`. */
|
|
3
|
+
edge?: 'top' | 'bottom';
|
|
4
|
+
/** A literal CSS color for the line, alpha included. Left out, a half-faded muted text color. */
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
/** The thin line along a card's edge that fills while a press is being timed as a hold. */
|
|
8
|
+
export declare function HoldProgress({ edge, color }: HoldProgressProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
2
2
|
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import { keyframes } from '@emotion/react';
|
|
3
4
|
import { Box } from 'e-prim';
|
|
4
|
-
import { motion } from 'motion/react';
|
|
5
5
|
import { HOLD_MS } from './use-hold.js';
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/** Grows the line from its left end by scaling it, which the browser animates on its own, without
|
|
7
|
+
* the page's JavaScript or a layout for each frame: on a slow display, animating the width is what
|
|
8
|
+
* made the hold look stuck. */
|
|
9
|
+
const fill = keyframes({
|
|
10
|
+
from: { transform: 'scaleX(0)' },
|
|
11
|
+
to: { transform: 'scaleX(1)' },
|
|
12
|
+
});
|
|
13
|
+
/** The thin line along a card's edge that fills while a press is being timed as a hold. */
|
|
14
|
+
export function HoldProgress({ edge = 'top', color }) {
|
|
15
|
+
return (_jsx(Box, { as: "span", "data-keep-motion": "", position: "absolute", width: "100%", height: 2, ...(color ? {} : { background: 'textMuted' }), css: {
|
|
16
|
+
[edge]: 0,
|
|
17
|
+
left: 0,
|
|
18
|
+
opacity: color ? 1 : 0.5,
|
|
19
|
+
...(color ? { backgroundColor: color } : {}),
|
|
20
|
+
pointerEvents: 'none',
|
|
21
|
+
transformOrigin: 'left center',
|
|
22
|
+
willChange: 'transform',
|
|
23
|
+
animation: `${fill} ${HOLD_MS}ms linear forwards`,
|
|
24
|
+
} }));
|
|
9
25
|
}
|
package/dist/layout/tile.js
CHANGED
|
@@ -8,6 +8,7 @@ import { statusLabels } from '../status.js';
|
|
|
8
8
|
import { useDrawer } from './use-drawer.js';
|
|
9
9
|
import { EnergyChart } from './energy-chart.js';
|
|
10
10
|
import { HistorySection } from './history-section.js';
|
|
11
|
+
import { HoldProgress } from './hold-progress.js';
|
|
11
12
|
import { LogbookHistory } from './logbook-history.js';
|
|
12
13
|
const DRAG_THRESHOLD = 8;
|
|
13
14
|
const KEY_STEP = 0.05;
|
|
@@ -181,19 +182,14 @@ export function Tile({ label, icon, secondary, status = 'ready', active = false,
|
|
|
181
182
|
// the unavailable/unknown/missing/loading statuses below).
|
|
182
183
|
const dimColorKey = ready ? 'line' : 'textMuted';
|
|
183
184
|
const cardOpacity = !ready || feedback === 'pending' ? 0.7 : 1;
|
|
184
|
-
const iconBadge = icon ? (_jsx(Flex, { as: motion.span, align: "center", justify: "center", position: "relative", radius: "full", width: 32, height: 32, animate: { backgroundColor: iconBg, color: iconColor }, transition: COLOR_TRANSITION, css: { flex: 'none' }, children: _jsx(Icon, { name: icon, size: 16 }) })) : null;
|
|
185
|
-
return (_jsxs(Flex, { as: motion.div, radius: "card", position: "relative", align: "center", overflow: "hidden", minWidth: 0, "data-status": status, "data-active": accented, "data-solid-accent": solidAccent, "data-fill": fill !== undefined, "data-fill-visible": showFill, "data-overlay": overlay !== undefined, "data-pending": feedback === 'pending', "data-feedback": feedback === 'pending' ? undefined : feedback,
|
|
185
|
+
const iconBadge = icon ? (_jsx(Flex, { as: motion.span, align: "center", justify: "center", position: "relative", radius: "full", width: 32, height: 32, initial: false, animate: { backgroundColor: iconBg, color: iconColor }, transition: COLOR_TRANSITION, css: { flex: 'none' }, children: _jsx(Icon, { name: icon, size: 16 }) })) : null;
|
|
186
|
+
return (_jsxs(Flex, { as: motion.div, radius: "card", position: "relative", align: "center", overflow: "hidden", minWidth: 0, "data-status": status, "data-active": accented, "data-solid-accent": solidAccent, "data-fill": fill !== undefined, "data-fill-visible": showFill, "data-overlay": overlay !== undefined, "data-pending": feedback === 'pending', "data-feedback": feedback === 'pending' ? undefined : feedback,
|
|
187
|
+
// A card that mounts (a page opened) is drawn as it is, not animated from nothing.
|
|
188
|
+
initial: false, animate: { backgroundColor: cardBg, color: cardColor, opacity: cardOpacity }, transition: COLOR_TRANSITION, css: ({ palette }) => feedback === 'error' ? { outline: `2px solid ${palette.danger}`, outlineOffset: -2 } : null, children: [hasFill ? (_jsx(Box, { as: motion.span, position: "absolute", background: "accent",
|
|
186
189
|
// Spans the whole card, including behind `trailing` (e.g. a color-capable light's
|
|
187
190
|
// picker button) — it's a sibling of the button and `trailing`, not nested inside the
|
|
188
191
|
// button, specifically so its width isn't capped at the button's own narrower bounds.
|
|
189
|
-
animate: { width: `${(shownFill ?? 0) * 100}%`, opacity: overlaid ? 0 : shownFill }, transition: drag !== null ? { duration: 0 } : COLOR_TRANSITION, css: { inset: '0 auto 0 0', pointerEvents: 'none' } })) : null, holdable && holding ? (_jsx(
|
|
190
|
-
inset: 'auto auto 0 0',
|
|
191
|
-
pointerEvents: 'none',
|
|
192
|
-
// Visible against either background: a light wash normally, a dark wash on an
|
|
193
|
-
// accent-colored (non-dimmable-active) card, where a light or accent bar would
|
|
194
|
-
// disappear or clash.
|
|
195
|
-
backgroundColor: solidAccent ? 'rgba(0, 0, 0, 0.35)' : 'rgba(255, 255, 255, 0.55)',
|
|
196
|
-
} }, "hold-progress")) : null, overlay ? (_jsxs(Flex, { align: "center", gap: 2.5, minWidth: 0, grow: 1, position: "relative", pt: 2, pr: 2, pb: 2, pl: 2, css: { alignSelf: 'stretch' }, children: [iconBadge, _jsx(Flex, { grow: 1, minWidth: 0, justify: "center", align: "center", children: overlay({ openDetail: openDrawer }) })] })) : (_jsxs(Flex, { as: "button", type: "button", disabled: !ready || (!onPress && !adjustable && !holdable), onClick: onClick, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel, onKeyDown: onKeyDown, title: title, "aria-label": label, align: "center", gap: 2.5, minWidth: 0, grow: 1, position: "relative", cursor: ready ? 'pointer' : 'default', pt: 2, pr: 3.5, pb: 2, pl: 2, css: {
|
|
192
|
+
initial: false, animate: { width: `${(shownFill ?? 0) * 100}%`, opacity: overlaid ? 0 : shownFill }, transition: drag !== null ? { duration: 0 } : COLOR_TRANSITION, css: { inset: '0 auto 0 0', pointerEvents: 'none' } })) : null, holdable && holding ? (_jsx(HoldProgress, { edge: "bottom", color: solidAccent ? 'rgba(0, 0, 0, 0.35)' : 'rgba(255, 255, 255, 0.55)' })) : null, overlay ? (_jsxs(Flex, { align: "center", gap: 2.5, minWidth: 0, grow: 1, position: "relative", pt: 2, pr: 2, pb: 2, pl: 2, css: { alignSelf: 'stretch' }, children: [iconBadge, _jsx(Flex, { grow: 1, minWidth: 0, justify: "center", align: "center", children: overlay({ openDetail: openDrawer }) })] })) : (_jsxs(Flex, { as: "button", type: "button", disabled: !ready || (!onPress && !adjustable && !holdable), onClick: onClick, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel, onKeyDown: onKeyDown, title: title, "aria-label": label, align: "center", gap: 2.5, minWidth: 0, grow: 1, position: "relative", cursor: ready ? 'pointer' : 'default', pt: 2, pr: 3.5, pb: 2, pl: 2, css: {
|
|
197
193
|
alignSelf: 'stretch',
|
|
198
194
|
background: 'none',
|
|
199
195
|
textAlign: 'left',
|
|
@@ -17,6 +17,7 @@ export declare function useDrawer({ icon, label, kind, body, }: {
|
|
|
17
17
|
}): {
|
|
18
18
|
open: () => void;
|
|
19
19
|
openExpanded: () => void;
|
|
20
|
+
openFull: () => void;
|
|
20
21
|
isOpen: boolean;
|
|
21
22
|
};
|
|
22
23
|
/** `useDrawer` for an owner that must not itself subscribe to the drawer's context. Re-pushing the
|
|
@@ -28,6 +29,6 @@ export declare function DrawerTrigger({ icon, label, kind, body, children, }: {
|
|
|
28
29
|
label: string;
|
|
29
30
|
kind?: string | undefined;
|
|
30
31
|
body: ReactNode;
|
|
31
|
-
/** Gets `open` (the side panel) and `
|
|
32
|
-
children: (open: () => void, openExpanded: () => void) => ReactNode;
|
|
32
|
+
/** Gets `open` (the side panel), `openExpanded` (full size, which can be collapsed) and `openFull` (full size, with no collapse button). */
|
|
33
|
+
children: (open: () => void, openExpanded: () => void, openFull: () => void) => ReactNode;
|
|
33
34
|
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -22,13 +22,15 @@ export function useDrawer({ icon, label, kind, body, }) {
|
|
|
22
22
|
}, [isOpen, id, icon, label, kind, body, updateDetail]);
|
|
23
23
|
const open = () => openDetail(id, _jsx(DrawerHeader, { icon: icon, label: label, kind: kind }), body);
|
|
24
24
|
const openExpanded = () => openDetail(id, _jsx(DrawerHeader, { icon: icon, label: label, kind: kind }), body, true);
|
|
25
|
-
|
|
25
|
+
// Full size and kept there: for content that is an overlay of its own, with nothing to collapse to.
|
|
26
|
+
const openFull = () => openDetail(id, _jsx(DrawerHeader, { icon: icon, label: label, kind: kind }), body, true, true);
|
|
27
|
+
return { open, openExpanded, openFull, isOpen };
|
|
26
28
|
}
|
|
27
29
|
/** `useDrawer` for an owner that must not itself subscribe to the drawer's context. Re-pushing the
|
|
28
30
|
* body on every context change re-renders whoever called the hook; if that is the same component
|
|
29
31
|
* that built `body`, it makes a fresh element each time and loops. Hosting the hook in this child
|
|
30
32
|
* keeps `body`'s identity tied to the parent's own renders (entity updates), not the drawer's. */
|
|
31
33
|
export function DrawerTrigger({ icon, label, kind, body, children, }) {
|
|
32
|
-
const { open, openExpanded } = useDrawer({ icon, label, kind, body });
|
|
33
|
-
return _jsx(_Fragment, { children: children(open, openExpanded) });
|
|
34
|
+
const { open, openExpanded, openFull } = useDrawer({ icon, label, kind, body });
|
|
35
|
+
return _jsx(_Fragment, { children: children(open, openExpanded, openFull) });
|
|
34
36
|
}
|
package/dist/provider.d.ts
CHANGED
|
@@ -2,7 +2,12 @@ import { type Client, type RemoteClientOptions } from '@hashsome/core';
|
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
3
|
import { type Density } from './theme/density.ts';
|
|
4
4
|
import { type ThemeOverrides } from './theme/overrides.ts';
|
|
5
|
-
|
|
5
|
+
import { type ThemeSchedule } from './theme/schedule.ts';
|
|
6
|
+
export type { ThemeSchedule };
|
|
7
|
+
/** `reduced` turns every animation off, for a slow display; `full` keeps them; `auto` (the default) leaves things as they are. */
|
|
8
|
+
export type MotionPreference = 'auto' | 'full' | 'reduced';
|
|
9
|
+
/** `'system'` follows the display's own light/dark preference; a `ThemeSchedule` changes with the time of day. */
|
|
10
|
+
export type ThemeMode = 'light' | 'dark' | 'system' | ThemeSchedule;
|
|
6
11
|
export interface HashsomeProviderProps {
|
|
7
12
|
/** Runtime WebSocket URL. Defaults to `/ws` on the current origin. */
|
|
8
13
|
url?: string;
|
|
@@ -10,12 +15,14 @@ export interface HashsomeProviderProps {
|
|
|
10
15
|
client?: Client;
|
|
11
16
|
/** Options for the default `RemoteClient` (reconnect timing, a custom socket factory). Ignored when `client` is given. */
|
|
12
17
|
clientOptions?: Omit<RemoteClientOptions, 'url'>;
|
|
13
|
-
/** `'system'` follows the browser/OS preference and updates live. Default `'dark'`. A `useThemeToggle()` caller (e.g. `NavRail`'s dev toggle) can still override this at runtime. */
|
|
18
|
+
/** `'light'`, `'dark'`, `'system'` (follows the browser/OS preference and updates live), or a schedule: `{ dark: { from: '19:00', to: '07:00' } }` is dark between those times on the display's clock, `{ sun: 'ha:sun.sun' }` is dark while that entity, a `daylight` sensor (`on` while the sun is up; the Home Assistant integration makes one of `sun.sun`), is `off`. Default `'dark'`. A schedule and the document shell's first paint use `'system'` until the time or the entity is known. A `useThemeToggle()` caller (e.g. `NavRail`'s dev toggle) can still override this at runtime. Pass a constant defined outside the component. */
|
|
14
19
|
theme?: ThemeMode;
|
|
15
20
|
/** Any Google Fonts family name (e.g. `'Inter'`, `'Roboto'`, `'Poppins'`), loaded dynamically. Default `'Inter'`. */
|
|
16
21
|
font?: string;
|
|
17
22
|
/** `'compact'` for small square displays: tighter spacing, shorter tiles, smaller icon circles. Default `'comfortable'`. */
|
|
18
23
|
density?: Density;
|
|
24
|
+
/** `'reduced'` turns animations and transitions off, for a slow display, `'full'` keeps them and `'auto'` (the default) leaves things as they are. A device can choose for itself with `?motion=reduced` (or `full`) on the address it opens: the choice is kept on the device, so reloads and links inside the app keep it, and `?motion=auto` forgets it. That wins over this prop. */
|
|
25
|
+
motion?: MotionPreference;
|
|
19
26
|
/** Partial changes to the built-in theme — colors per light/dark, radii, typography, spacing, density sizes. Pass a constant defined outside the component: a new object each render rebuilds the theme each render. */
|
|
20
27
|
overrides?: ThemeOverrides;
|
|
21
28
|
/** The app. */
|
|
@@ -33,5 +40,5 @@ export interface ThemeModeState {
|
|
|
33
40
|
* Must be rendered inside `<HashsomeProvider>`. */
|
|
34
41
|
export declare function useThemeToggle(): ThemeModeState;
|
|
35
42
|
/** Connects the tree to the runtime proxy. Render only on the client. */
|
|
36
|
-
export declare function HashsomeProvider({ url, client, clientOptions, theme, font, density, overrides, children, }: HashsomeProviderProps): import("react").JSX.Element;
|
|
43
|
+
export declare function HashsomeProvider({ url, client, clientOptions, theme, motion, font, density, overrides, children, }: HashsomeProviderProps): import("react").JSX.Element;
|
|
37
44
|
export declare function useClient(): Client;
|